]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/bootstrap.py
Allow using vendoring when running bootstrap from outside the current working directory
[rust.git] / src / bootstrap / bootstrap.py
1 from __future__ import absolute_import, division, print_function
2 import argparse
3 import contextlib
4 import datetime
5 import distutils.version
6 import hashlib
7 import json
8 import os
9 import re
10 import shutil
11 import subprocess
12 import sys
13 import tarfile
14 import tempfile
15
16 from time import time, sleep
17
18 def support_xz():
19     try:
20         with tempfile.NamedTemporaryFile(delete=False) as temp_file:
21             temp_path = temp_file.name
22         with tarfile.open(temp_path, "w:xz"):
23             pass
24         return True
25     except tarfile.CompressionError:
26         return False
27
28 def get(base, url, path, checksums, verbose=False):
29     with tempfile.NamedTemporaryFile(delete=False) as temp_file:
30         temp_path = temp_file.name
31
32     try:
33         if url not in checksums:
34             raise RuntimeError(("src/stage0.json doesn't contain a checksum for {}. "
35                                 "Pre-built artifacts might not be available for this "
36                                 "target at this time, see https://doc.rust-lang.org/nightly"
37                                 "/rustc/platform-support.html for more information.")
38                                 .format(url))
39         sha256 = checksums[url]
40         if os.path.exists(path):
41             if verify(path, sha256, False):
42                 if verbose:
43                     print("using already-download file", path)
44                 return
45             else:
46                 if verbose:
47                     print("ignoring already-download file",
48                         path, "due to failed verification")
49                 os.unlink(path)
50         download(temp_path, "{}/{}".format(base, url), True, verbose)
51         if not verify(temp_path, sha256, verbose):
52             raise RuntimeError("failed verification")
53         if verbose:
54             print("moving {} to {}".format(temp_path, path))
55         shutil.move(temp_path, path)
56     finally:
57         if os.path.isfile(temp_path):
58             if verbose:
59                 print("removing", temp_path)
60             os.unlink(temp_path)
61
62
63 def download(path, url, probably_big, verbose):
64     for _ in range(0, 4):
65         try:
66             _download(path, url, probably_big, verbose, True)
67             return
68         except RuntimeError:
69             print("\nspurious failure, trying again")
70     _download(path, url, probably_big, verbose, False)
71
72
73 def _download(path, url, probably_big, verbose, exception):
74     # Try to use curl (potentially available on win32
75     #    https://devblogs.microsoft.com/commandline/tar-and-curl-come-to-windows/)
76     # If an error occurs:
77     #  - If we are on win32 fallback to powershell
78     #  - Otherwise raise the error if appropriate
79     if probably_big or verbose:
80         print("downloading {}".format(url))
81
82     platform_is_win32 = sys.platform == 'win32'
83     try:
84         if probably_big or verbose:
85             option = "-#"
86         else:
87             option = "-s"
88         # If curl is not present on Win32, we should not sys.exit
89         #   but raise `CalledProcessError` or `OSError` instead
90         require(["curl", "--version"], exception=platform_is_win32)
91         run(["curl", option,
92              "-L", # Follow redirect.
93              "-y", "30", "-Y", "10",    # timeout if speed is < 10 bytes/sec for > 30 seconds
94              "--connect-timeout", "30",  # timeout if cannot connect within 30 seconds
95              "--retry", "3", "-Sf", "-o", path, url],
96             verbose=verbose,
97             exception=True, # Will raise RuntimeError on failure
98         )
99     except (subprocess.CalledProcessError, OSError, RuntimeError):
100         # see http://serverfault.com/questions/301128/how-to-download
101         if platform_is_win32:
102             run(["PowerShell.exe", "/nologo", "-Command",
103                  "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
104                  "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
105                 verbose=verbose,
106                 exception=exception)
107         # Check if the RuntimeError raised by run(curl) should be silenced
108         elif verbose or exception:
109             raise
110
111
112 def verify(path, expected, verbose):
113     """Check if the sha256 sum of the given path is valid"""
114     if verbose:
115         print("verifying", path)
116     with open(path, "rb") as source:
117         found = hashlib.sha256(source.read()).hexdigest()
118     verified = found == expected
119     if not verified:
120         print("invalid checksum:\n"
121               "    found:    {}\n"
122               "    expected: {}".format(found, expected))
123     return verified
124
125
126 def unpack(tarball, tarball_suffix, dst, verbose=False, match=None):
127     """Unpack the given tarball file"""
128     print("extracting", tarball)
129     fname = os.path.basename(tarball).replace(tarball_suffix, "")
130     with contextlib.closing(tarfile.open(tarball)) as tar:
131         for member in tar.getnames():
132             if "/" not in member:
133                 continue
134             name = member.replace(fname + "/", "", 1)
135             if match is not None and not name.startswith(match):
136                 continue
137             name = name[len(match) + 1:]
138
139             dst_path = os.path.join(dst, name)
140             if verbose:
141                 print("  extracting", member)
142             tar.extract(member, dst)
143             src_path = os.path.join(dst, member)
144             if os.path.isdir(src_path) and os.path.exists(dst_path):
145                 continue
146             shutil.move(src_path, dst_path)
147     shutil.rmtree(os.path.join(dst, fname))
148
149
150 def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
151     """Run a child program in a new process"""
152     if verbose:
153         print("running: " + ' '.join(args))
154     sys.stdout.flush()
155     # Ensure that the .exe is used on Windows just in case a Linux ELF has been
156     # compiled in the same directory.
157     if os.name == 'nt' and not args[0].endswith('.exe'):
158         args[0] += '.exe'
159     # Use Popen here instead of call() as it apparently allows powershell on
160     # Windows to not lock up waiting for input presumably.
161     ret = subprocess.Popen(args, **kwargs)
162     code = ret.wait()
163     if code != 0:
164         err = "failed to run: " + ' '.join(args)
165         if verbose or exception:
166             raise RuntimeError(err)
167         # For most failures, we definitely do want to print this error, or the user will have no
168         # idea what went wrong. But when we've successfully built bootstrap and it failed, it will
169         # have already printed an error above, so there's no need to print the exact command we're
170         # running.
171         if is_bootstrap:
172             sys.exit(1)
173         else:
174             sys.exit(err)
175
176
177 def require(cmd, exit=True, exception=False):
178     '''Run a command, returning its output.
179     On error,
180         If `exception` is `True`, raise the error
181         Otherwise If `exit` is `True`, exit the process
182         Else return None.'''
183     try:
184         return subprocess.check_output(cmd).strip()
185     except (subprocess.CalledProcessError, OSError) as exc:
186         if exception:
187             raise
188         elif exit:
189             print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
190             print("Please make sure it's installed and in the path.")
191             sys.exit(1)
192         return None
193
194
195
196 def format_build_time(duration):
197     """Return a nicer format for build time
198
199     >>> format_build_time('300')
200     '0:05:00'
201     """
202     return str(datetime.timedelta(seconds=int(duration)))
203
204
205 def default_build_triple(verbose):
206     """Build triple as in LLVM"""
207     # If the user already has a host build triple with an existing `rustc`
208     # install, use their preference. This fixes most issues with Windows builds
209     # being detected as GNU instead of MSVC.
210     default_encoding = sys.getdefaultencoding()
211     try:
212         version = subprocess.check_output(["rustc", "--version", "--verbose"],
213                 stderr=subprocess.DEVNULL)
214         version = version.decode(default_encoding)
215         host = next(x for x in version.split('\n') if x.startswith("host: "))
216         triple = host.split("host: ")[1]
217         if verbose:
218             print("detected default triple {} from pre-installed rustc".format(triple))
219         return triple
220     except Exception as e:
221         if verbose:
222             print("pre-installed rustc not detected: {}".format(e))
223             print("falling back to auto-detect")
224
225     required = sys.platform != 'win32'
226     ostype = require(["uname", "-s"], exit=required)
227     cputype = require(['uname', '-m'], exit=required)
228
229     # If we do not have `uname`, assume Windows.
230     if ostype is None or cputype is None:
231         return 'x86_64-pc-windows-msvc'
232
233     ostype = ostype.decode(default_encoding)
234     cputype = cputype.decode(default_encoding)
235
236     # The goal here is to come up with the same triple as LLVM would,
237     # at least for the subset of platforms we're willing to target.
238     ostype_mapper = {
239         'Darwin': 'apple-darwin',
240         'DragonFly': 'unknown-dragonfly',
241         'FreeBSD': 'unknown-freebsd',
242         'Haiku': 'unknown-haiku',
243         'NetBSD': 'unknown-netbsd',
244         'OpenBSD': 'unknown-openbsd'
245     }
246
247     # Consider the direct transformation first and then the special cases
248     if ostype in ostype_mapper:
249         ostype = ostype_mapper[ostype]
250     elif ostype == 'Linux':
251         os_from_sp = subprocess.check_output(
252             ['uname', '-o']).strip().decode(default_encoding)
253         if os_from_sp == 'Android':
254             ostype = 'linux-android'
255         else:
256             ostype = 'unknown-linux-gnu'
257     elif ostype == 'SunOS':
258         ostype = 'pc-solaris'
259         # On Solaris, uname -m will return a machine classification instead
260         # of a cpu type, so uname -p is recommended instead.  However, the
261         # output from that option is too generic for our purposes (it will
262         # always emit 'i386' on x86/amd64 systems).  As such, isainfo -k
263         # must be used instead.
264         cputype = require(['isainfo', '-k']).decode(default_encoding)
265         # sparc cpus have sun as a target vendor
266         if 'sparc' in cputype:
267             ostype = 'sun-solaris'
268     elif ostype.startswith('MINGW'):
269         # msys' `uname` does not print gcc configuration, but prints msys
270         # configuration. so we cannot believe `uname -m`:
271         # msys1 is always i686 and msys2 is always x86_64.
272         # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
273         # MINGW64 on x86_64.
274         ostype = 'pc-windows-gnu'
275         cputype = 'i686'
276         if os.environ.get('MSYSTEM') == 'MINGW64':
277             cputype = 'x86_64'
278     elif ostype.startswith('MSYS'):
279         ostype = 'pc-windows-gnu'
280     elif ostype.startswith('CYGWIN_NT'):
281         cputype = 'i686'
282         if ostype.endswith('WOW64'):
283             cputype = 'x86_64'
284         ostype = 'pc-windows-gnu'
285     elif sys.platform == 'win32':
286         # Some Windows platforms might have a `uname` command that returns a
287         # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
288         # these cases, fall back to using sys.platform.
289         return 'x86_64-pc-windows-msvc'
290     else:
291         err = "unknown OS type: {}".format(ostype)
292         sys.exit(err)
293
294     if cputype in ['powerpc', 'riscv'] and ostype == 'unknown-freebsd':
295         cputype = subprocess.check_output(
296               ['uname', '-p']).strip().decode(default_encoding)
297     cputype_mapper = {
298         'BePC': 'i686',
299         'aarch64': 'aarch64',
300         'amd64': 'x86_64',
301         'arm64': 'aarch64',
302         'i386': 'i686',
303         'i486': 'i686',
304         'i686': 'i686',
305         'i786': 'i686',
306         'm68k': 'm68k',
307         'powerpc': 'powerpc',
308         'powerpc64': 'powerpc64',
309         'powerpc64le': 'powerpc64le',
310         'ppc': 'powerpc',
311         'ppc64': 'powerpc64',
312         'ppc64le': 'powerpc64le',
313         'riscv64': 'riscv64gc',
314         's390x': 's390x',
315         'x64': 'x86_64',
316         'x86': 'i686',
317         'x86-64': 'x86_64',
318         'x86_64': 'x86_64'
319     }
320
321     # Consider the direct transformation first and then the special cases
322     if cputype in cputype_mapper:
323         cputype = cputype_mapper[cputype]
324     elif cputype in {'xscale', 'arm'}:
325         cputype = 'arm'
326         if ostype == 'linux-android':
327             ostype = 'linux-androideabi'
328         elif ostype == 'unknown-freebsd':
329             cputype = subprocess.check_output(
330                 ['uname', '-p']).strip().decode(default_encoding)
331             ostype = 'unknown-freebsd'
332     elif cputype == 'armv6l':
333         cputype = 'arm'
334         if ostype == 'linux-android':
335             ostype = 'linux-androideabi'
336         else:
337             ostype += 'eabihf'
338     elif cputype in {'armv7l', 'armv8l'}:
339         cputype = 'armv7'
340         if ostype == 'linux-android':
341             ostype = 'linux-androideabi'
342         else:
343             ostype += 'eabihf'
344     elif cputype == 'mips':
345         if sys.byteorder == 'big':
346             cputype = 'mips'
347         elif sys.byteorder == 'little':
348             cputype = 'mipsel'
349         else:
350             raise ValueError("unknown byteorder: {}".format(sys.byteorder))
351     elif cputype == 'mips64':
352         if sys.byteorder == 'big':
353             cputype = 'mips64'
354         elif sys.byteorder == 'little':
355             cputype = 'mips64el'
356         else:
357             raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
358         # only the n64 ABI is supported, indicate it
359         ostype += 'abi64'
360     elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
361         pass
362     else:
363         err = "unknown cpu type: {}".format(cputype)
364         sys.exit(err)
365
366     return "{}-{}".format(cputype, ostype)
367
368
369 @contextlib.contextmanager
370 def output(filepath):
371     tmp = filepath + '.tmp'
372     with open(tmp, 'w') as f:
373         yield f
374     try:
375         if os.path.exists(filepath):
376             os.remove(filepath)  # PermissionError/OSError on Win32 if in use
377     except OSError:
378         shutil.copy2(tmp, filepath)
379         os.remove(tmp)
380         return
381     os.rename(tmp, filepath)
382
383
384 class Stage0Toolchain:
385     def __init__(self, stage0_payload):
386         self.date = stage0_payload["date"]
387         self.version = stage0_payload["version"]
388
389     def channel(self):
390         return self.version + "-" + self.date
391
392
393 class RustBuild(object):
394     """Provide all the methods required to build Rust"""
395     def __init__(self):
396         self.checksums_sha256 = {}
397         self.stage0_compiler = None
398         self._download_url = ''
399         self.build = ''
400         self.build_dir = ''
401         self.clean = False
402         self.config_toml = ''
403         self.rust_root = ''
404         self.use_locked_deps = ''
405         self.use_vendored_sources = ''
406         self.verbose = False
407         self.git_version = None
408         self.nix_deps_dir = None
409
410     def download_toolchain(self):
411         """Fetch the build system for Rust, written in Rust
412
413         This method will build a cache directory, then it will fetch the
414         tarball which has the stage0 compiler used to then bootstrap the Rust
415         compiler itself.
416
417         Each downloaded tarball is extracted, after that, the script
418         will move all the content to the right place.
419         """
420         rustc_channel = self.stage0_compiler.version
421         bin_root = self.bin_root()
422
423         key = self.stage0_compiler.date
424         if self.rustc().startswith(bin_root) and \
425                 (not os.path.exists(self.rustc()) or
426                  self.program_out_of_date(self.rustc_stamp(), key)):
427             if os.path.exists(bin_root):
428                 shutil.rmtree(bin_root)
429             tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
430             filename = "rust-std-{}-{}{}".format(
431                 rustc_channel, self.build, tarball_suffix)
432             pattern = "rust-std-{}".format(self.build)
433             self._download_component_helper(filename, pattern, tarball_suffix)
434             filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
435                                               tarball_suffix)
436             self._download_component_helper(filename, "rustc", tarball_suffix)
437             filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
438                                             tarball_suffix)
439             self._download_component_helper(filename, "cargo", tarball_suffix)
440             self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
441
442             self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
443             self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
444             lib_dir = "{}/lib".format(bin_root)
445             for lib in os.listdir(lib_dir):
446                 if lib.endswith(".so"):
447                     self.fix_bin_or_dylib(os.path.join(lib_dir, lib))
448             with output(self.rustc_stamp()) as rust_stamp:
449                 rust_stamp.write(key)
450
451     def _download_component_helper(
452         self, filename, pattern, tarball_suffix,
453     ):
454         key = self.stage0_compiler.date
455         cache_dst = os.path.join(self.build_dir, "cache")
456         rustc_cache = os.path.join(cache_dst, key)
457         if not os.path.exists(rustc_cache):
458             os.makedirs(rustc_cache)
459
460         base = self._download_url
461         url = "dist/{}".format(key)
462         tarball = os.path.join(rustc_cache, filename)
463         if not os.path.exists(tarball):
464             get(
465                 base,
466                 "{}/{}".format(url, filename),
467                 tarball,
468                 self.checksums_sha256,
469                 verbose=self.verbose,
470             )
471         unpack(tarball, tarball_suffix, self.bin_root(), match=pattern, verbose=self.verbose)
472
473     def fix_bin_or_dylib(self, fname):
474         """Modifies the interpreter section of 'fname' to fix the dynamic linker,
475         or the RPATH section, to fix the dynamic library search path
476
477         This method is only required on NixOS and uses the PatchELF utility to
478         change the interpreter/RPATH of ELF executables.
479
480         Please see https://nixos.org/patchelf.html for more information
481         """
482         default_encoding = sys.getdefaultencoding()
483         try:
484             ostype = subprocess.check_output(
485                 ['uname', '-s']).strip().decode(default_encoding)
486         except subprocess.CalledProcessError:
487             return
488         except OSError as reason:
489             if getattr(reason, 'winerror', None) is not None:
490                 return
491             raise reason
492
493         if ostype != "Linux":
494             return
495
496         # If the user has asked binaries to be patched for Nix, then
497         # don't check for NixOS or `/lib`, just continue to the patching.
498         if self.get_toml('patch-binaries-for-nix', 'build') != 'true':
499             # Use `/etc/os-release` instead of `/etc/NIXOS`.
500             # The latter one does not exist on NixOS when using tmpfs as root.
501             try:
502                 with open("/etc/os-release", "r") as f:
503                     if not any(l.strip() in ["ID=nixos", "ID='nixos'", 'ID="nixos"'] for l in f):
504                         return
505             except FileNotFoundError:
506                 return
507             if os.path.exists("/lib"):
508                 return
509
510         # At this point we're pretty sure the user is running NixOS or
511         # using Nix
512         nix_os_msg = "info: you seem to be using Nix. Attempting to patch"
513         print(nix_os_msg, fname)
514
515         # Only build `.nix-deps` once.
516         nix_deps_dir = self.nix_deps_dir
517         if not nix_deps_dir:
518             # Run `nix-build` to "build" each dependency (which will likely reuse
519             # the existing `/nix/store` copy, or at most download a pre-built copy).
520             #
521             # Importantly, we create a gc-root called `.nix-deps` in the `build/`
522             # directory, but still reference the actual `/nix/store` path in the rpath
523             # as it makes it significantly more robust against changes to the location of
524             # the `.nix-deps` location.
525             #
526             # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
527             # zlib: Needed as a system dependency of `libLLVM-*.so`.
528             # patchelf: Needed for patching ELF binaries (see doc comment above).
529             nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
530             nix_expr = '''
531             with (import <nixpkgs> {});
532             symlinkJoin {
533               name = "rust-stage0-dependencies";
534               paths = [
535                 zlib
536                 patchelf
537                 stdenv.cc.bintools
538               ];
539             }
540             '''
541             try:
542                 subprocess.check_output([
543                     "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
544                 ])
545             except subprocess.CalledProcessError as reason:
546                 print("warning: failed to call nix-build:", reason)
547                 return
548             self.nix_deps_dir = nix_deps_dir
549
550         patchelf = "{}/bin/patchelf".format(nix_deps_dir)
551         rpath_entries = [
552             # Relative default, all binary and dynamic libraries we ship
553             # appear to have this (even when `../lib` is redundant).
554             "$ORIGIN/../lib",
555             os.path.join(os.path.realpath(nix_deps_dir), "lib")
556         ]
557         patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
558         if not fname.endswith(".so"):
559             # Finally, set the corret .interp for binaries
560             with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
561                 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
562
563         try:
564             subprocess.check_output([patchelf] + patchelf_args + [fname])
565         except subprocess.CalledProcessError as reason:
566             print("warning: failed to call patchelf:", reason)
567             return
568
569     def rustc_stamp(self):
570         """Return the path for .rustc-stamp at the given stage
571
572         >>> rb = RustBuild()
573         >>> rb.build_dir = "build"
574         >>> rb.rustc_stamp() == os.path.join("build", "stage0", ".rustc-stamp")
575         True
576         """
577         return os.path.join(self.bin_root(), '.rustc-stamp')
578
579     def program_out_of_date(self, stamp_path, key):
580         """Check if the given program stamp is out of date"""
581         if not os.path.exists(stamp_path) or self.clean:
582             return True
583         with open(stamp_path, 'r') as stamp:
584             return key != stamp.read()
585
586     def bin_root(self):
587         """Return the binary root directory for the given stage
588
589         >>> rb = RustBuild()
590         >>> rb.build_dir = "build"
591         >>> rb.bin_root() == os.path.join("build", "stage0")
592         True
593
594         When the 'build' property is given should be a nested directory:
595
596         >>> rb.build = "devel"
597         >>> rb.bin_root() == os.path.join("build", "devel", "stage0")
598         True
599         """
600         subdir = "stage0"
601         return os.path.join(self.build_dir, self.build, subdir)
602
603     def get_toml(self, key, section=None):
604         """Returns the value of the given key in config.toml, otherwise returns None
605
606         >>> rb = RustBuild()
607         >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
608         >>> rb.get_toml("key2")
609         'value2'
610
611         If the key does not exist, the result is None:
612
613         >>> rb.get_toml("key3") is None
614         True
615
616         Optionally also matches the section the key appears in
617
618         >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
619         >>> rb.get_toml('key', 'a')
620         'value1'
621         >>> rb.get_toml('key', 'b')
622         'value2'
623         >>> rb.get_toml('key', 'c') is None
624         True
625
626         >>> rb.config_toml = 'key1 = true'
627         >>> rb.get_toml("key1")
628         'true'
629         """
630
631         cur_section = None
632         for line in self.config_toml.splitlines():
633             section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
634             if section_match is not None:
635                 cur_section = section_match.group(1)
636
637             match = re.match(r'^{}\s*=(.*)$'.format(key), line)
638             if match is not None:
639                 value = match.group(1)
640                 if section is None or section == cur_section:
641                     return self.get_string(value) or value.strip()
642         return None
643
644     def cargo(self):
645         """Return config path for cargo"""
646         return self.program_config('cargo')
647
648     def rustc(self):
649         """Return config path for rustc"""
650         return self.program_config('rustc')
651
652     def program_config(self, program):
653         """Return config path for the given program at the given stage
654
655         >>> rb = RustBuild()
656         >>> rb.config_toml = 'rustc = "rustc"\\n'
657         >>> rb.program_config('rustc')
658         'rustc'
659         >>> rb.config_toml = ''
660         >>> cargo_path = rb.program_config('cargo')
661         >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(),
662         ... "bin", "cargo")
663         True
664         """
665         config = self.get_toml(program)
666         if config:
667             return os.path.expanduser(config)
668         return os.path.join(self.bin_root(), "bin", "{}{}".format(
669             program, self.exe_suffix()))
670
671     @staticmethod
672     def get_string(line):
673         """Return the value between double quotes
674
675         >>> RustBuild.get_string('    "devel"   ')
676         'devel'
677         >>> RustBuild.get_string("    'devel'   ")
678         'devel'
679         >>> RustBuild.get_string('devel') is None
680         True
681         >>> RustBuild.get_string('    "devel   ')
682         ''
683         """
684         start = line.find('"')
685         if start != -1:
686             end = start + 1 + line[start + 1:].find('"')
687             return line[start + 1:end]
688         start = line.find('\'')
689         if start != -1:
690             end = start + 1 + line[start + 1:].find('\'')
691             return line[start + 1:end]
692         return None
693
694     @staticmethod
695     def exe_suffix():
696         """Return a suffix for executables"""
697         if sys.platform == 'win32':
698             return '.exe'
699         return ''
700
701     def bootstrap_binary(self):
702         """Return the path of the bootstrap binary
703
704         >>> rb = RustBuild()
705         >>> rb.build_dir = "build"
706         >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
707         ... "debug", "bootstrap")
708         True
709         """
710         return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
711
712     def build_bootstrap(self, color):
713         """Build bootstrap"""
714         print("Building rustbuild")
715         build_dir = os.path.join(self.build_dir, "bootstrap")
716         if self.clean and os.path.exists(build_dir):
717             shutil.rmtree(build_dir)
718         env = os.environ.copy()
719         # `CARGO_BUILD_TARGET` breaks bootstrap build.
720         # See also: <https://github.com/rust-lang/rust/issues/70208>.
721         if "CARGO_BUILD_TARGET" in env:
722             del env["CARGO_BUILD_TARGET"]
723         env["CARGO_TARGET_DIR"] = build_dir
724         env["RUSTC"] = self.rustc()
725         env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
726             (os.pathsep + env["LD_LIBRARY_PATH"]) \
727             if "LD_LIBRARY_PATH" in env else ""
728         env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
729             (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
730             if "DYLD_LIBRARY_PATH" in env else ""
731         env["LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
732             (os.pathsep + env["LIBRARY_PATH"]) \
733             if "LIBRARY_PATH" in env else ""
734
735         # preserve existing RUSTFLAGS
736         env.setdefault("RUSTFLAGS", "")
737         build_section = "target.{}".format(self.build)
738         target_features = []
739         if self.get_toml("crt-static", build_section) == "true":
740             target_features += ["+crt-static"]
741         elif self.get_toml("crt-static", build_section) == "false":
742             target_features += ["-crt-static"]
743         if target_features:
744             env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
745         target_linker = self.get_toml("linker", build_section)
746         if target_linker is not None:
747             env["RUSTFLAGS"] += " -C linker=" + target_linker
748         env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
749         env["RUSTFLAGS"] += " -Wsemicolon_in_expressions_from_macros"
750         if self.get_toml("deny-warnings", "rust") != "false":
751             env["RUSTFLAGS"] += " -Dwarnings"
752
753         env["PATH"] = os.path.join(self.bin_root(), "bin") + \
754             os.pathsep + env["PATH"]
755         if not os.path.isfile(self.cargo()):
756             raise Exception("no cargo executable found at `{}`".format(
757                 self.cargo()))
758         args = [self.cargo(), "build", "--manifest-path",
759                 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
760         for _ in range(0, self.verbose):
761             args.append("--verbose")
762         if self.use_locked_deps:
763             args.append("--locked")
764         if self.use_vendored_sources:
765             args.append("--frozen")
766         if self.get_toml("metrics", "build"):
767             args.append("--features")
768             args.append("build-metrics")
769         if color == "always":
770             args.append("--color=always")
771         elif color == "never":
772             args.append("--color=never")
773
774         # Run this from the source directory so cargo finds .cargo/config
775         run(args, env=env, verbose=self.verbose, cwd=self.rust_root)
776
777     def build_triple(self):
778         """Build triple as in LLVM
779
780         Note that `default_build_triple` is moderately expensive,
781         so use `self.build` where possible.
782         """
783         config = self.get_toml('build')
784         if config:
785             return config
786         return default_build_triple(self.verbose)
787
788     def set_dist_environment(self, url):
789         """Set download URL for normal environment"""
790         if 'RUSTUP_DIST_SERVER' in os.environ:
791             self._download_url = os.environ['RUSTUP_DIST_SERVER']
792         else:
793             self._download_url = url
794
795     def check_vendored_status(self):
796         """Check that vendoring is configured properly"""
797         # keep this consistent with the equivalent check in rustbuild:
798         # https://github.com/rust-lang/rust/blob/a8a33cf27166d3eabaffc58ed3799e054af3b0c6/src/bootstrap/lib.rs#L399-L405
799         if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
800             if os.getuid() == 0:
801                 self.use_vendored_sources = True
802                 print('info: looks like you\'re trying to run this command as root')
803                 print('      and so in order to preserve your $HOME this will now')
804                 print('      use vendored sources by default.')
805
806         cargo_dir = os.path.join(self.rust_root, '.cargo')
807         if self.use_vendored_sources:
808             vendor_dir = os.path.join(self.rust_root, 'vendor')
809             if not os.path.exists(vendor_dir):
810                 sync_dirs = "--sync ./src/tools/rust-analyzer/Cargo.toml " \
811                             "--sync ./compiler/rustc_codegen_cranelift/Cargo.toml " \
812                             "--sync ./src/bootstrap/Cargo.toml "
813                 print('error: vendoring required, but vendor directory does not exist.')
814                 print('       Run `cargo vendor {}` to initialize the '
815                       'vendor directory.'.format(sync_dirs))
816                 print('Alternatively, use the pre-vendored `rustc-src` dist component.')
817                 raise Exception("{} not found".format(vendor_dir))
818
819             if not os.path.exists(cargo_dir):
820                 print('error: vendoring required, but .cargo/config does not exist.')
821                 raise Exception("{} not found".format(cargo_dir))
822         else:
823             if os.path.exists(cargo_dir):
824                 shutil.rmtree(cargo_dir)
825
826 def bootstrap(help_triggered):
827     """Configure, fetch, build and run the initial bootstrap"""
828
829     # If the user is asking for help, let them know that the whole download-and-build
830     # process has to happen before anything is printed out.
831     if help_triggered:
832         print("info: Downloading and building bootstrap before processing --help")
833         print("      command. See src/bootstrap/README.md for help with common")
834         print("      commands.")
835
836     parser = argparse.ArgumentParser(description='Build rust')
837     parser.add_argument('--config')
838     parser.add_argument('--build-dir')
839     parser.add_argument('--build')
840     parser.add_argument('--color', choices=['always', 'never', 'auto'])
841     parser.add_argument('--clean', action='store_true')
842     parser.add_argument('-v', '--verbose', action='count', default=0)
843
844     args = [a for a in sys.argv if a != '-h' and a != '--help']
845     args, _ = parser.parse_known_args(args)
846
847     # Configure initial bootstrap
848     build = RustBuild()
849     build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
850     build.verbose = args.verbose
851     build.clean = args.clean
852
853     # Read from `--config`, then `RUST_BOOTSTRAP_CONFIG`, then `./config.toml`,
854     # then `config.toml` in the root directory.
855     toml_path = args.config or os.getenv('RUST_BOOTSTRAP_CONFIG')
856     using_default_path = toml_path is None
857     if using_default_path:
858         toml_path = 'config.toml'
859         if not os.path.exists(toml_path):
860             toml_path = os.path.join(build.rust_root, toml_path)
861
862     # Give a hard error if `--config` or `RUST_BOOTSTRAP_CONFIG` are set to a missing path,
863     # but not if `config.toml` hasn't been created.
864     if not using_default_path or os.path.exists(toml_path):
865         with open(toml_path) as config:
866             build.config_toml = config.read()
867
868     profile = build.get_toml('profile')
869     if profile is not None:
870         include_file = 'config.{}.toml'.format(profile)
871         include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
872         include_path = os.path.join(include_dir, include_file)
873         # HACK: This works because `build.get_toml()` returns the first match it finds for a
874         # specific key, so appending our defaults at the end allows the user to override them
875         with open(include_path) as included_toml:
876             build.config_toml += os.linesep + included_toml.read()
877
878     config_verbose = build.get_toml('verbose', 'build')
879     if config_verbose is not None:
880         build.verbose = max(build.verbose, int(config_verbose))
881
882     build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
883
884     build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
885
886     build.check_vendored_status()
887
888     build_dir = args.build_dir or build.get_toml('build-dir', 'build') or 'build'
889     build.build_dir = os.path.abspath(build_dir)
890
891     with open(os.path.join(build.rust_root, "src", "stage0.json")) as f:
892         data = json.load(f)
893     build.checksums_sha256 = data["checksums_sha256"]
894     build.stage0_compiler = Stage0Toolchain(data["compiler"])
895
896     build.set_dist_environment(data["config"]["dist_server"])
897
898     build.build = args.build or build.build_triple()
899
900     if not os.path.exists(build.build_dir):
901         os.makedirs(build.build_dir)
902
903     # Fetch/build the bootstrap
904     build.download_toolchain()
905     sys.stdout.flush()
906     build.build_bootstrap(args.color)
907     sys.stdout.flush()
908
909     # Run the bootstrap
910     args = [build.bootstrap_binary()]
911     args.extend(sys.argv[1:])
912     env = os.environ.copy()
913     env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
914     env["BOOTSTRAP_PYTHON"] = sys.executable
915     run(args, env=env, verbose=build.verbose, is_bootstrap=True)
916
917
918 def main():
919     """Entry point for the bootstrap process"""
920     start_time = time()
921
922     # x.py help <cmd> ...
923     if len(sys.argv) > 1 and sys.argv[1] == 'help':
924         sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
925
926     help_triggered = (
927         '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
928     try:
929         bootstrap(help_triggered)
930         if not help_triggered:
931             print("Build completed successfully in {}".format(
932                 format_build_time(time() - start_time)))
933     except (SystemExit, KeyboardInterrupt) as error:
934         if hasattr(error, 'code') and isinstance(error.code, int):
935             exit_code = error.code
936         else:
937             exit_code = 1
938             print(error)
939         if not help_triggered:
940             print("Build completed unsuccessfully in {}".format(
941                 format_build_time(time() - start_time)))
942         sys.exit(exit_code)
943
944
945 if __name__ == '__main__':
946     main()