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