]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/bootstrap.py
Auto merge of #99556 - davidtwco:collapse-debuginfo, r=wesleywiser
[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(args, env=env, verbose=self.verbose)
775
776     def build_triple(self):
777         """Build triple as in LLVM
778
779         Note that `default_build_triple` is moderately expensive,
780         so use `self.build` where possible.
781         """
782         config = self.get_toml('build')
783         if config:
784             return config
785         return default_build_triple(self.verbose)
786
787     def set_dist_environment(self, url):
788         """Set download URL for normal environment"""
789         if 'RUSTUP_DIST_SERVER' in os.environ:
790             self._download_url = os.environ['RUSTUP_DIST_SERVER']
791         else:
792             self._download_url = url
793
794     def check_vendored_status(self):
795         """Check that vendoring is configured properly"""
796         # keep this consistent with the equivalent check in rustbuild:
797         # https://github.com/rust-lang/rust/blob/a8a33cf27166d3eabaffc58ed3799e054af3b0c6/src/bootstrap/lib.rs#L399-L405
798         if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
799             if os.getuid() == 0:
800                 self.use_vendored_sources = True
801                 print('info: looks like you\'re trying to run this command as root')
802                 print('      and so in order to preserve your $HOME this will now')
803                 print('      use vendored sources by default.')
804
805         cargo_dir = os.path.join(self.rust_root, '.cargo')
806         if self.use_vendored_sources:
807             vendor_dir = os.path.join(self.rust_root, 'vendor')
808             if not os.path.exists(vendor_dir):
809                 sync_dirs = "--sync ./src/tools/rust-analyzer/Cargo.toml " \
810                             "--sync ./compiler/rustc_codegen_cranelift/Cargo.toml " \
811                             "--sync ./src/bootstrap/Cargo.toml "
812                 print('error: vendoring required, but vendor directory does not exist.')
813                 print('       Run `cargo vendor {}` to initialize the '
814                       'vendor directory.'.format(sync_dirs))
815                 print('Alternatively, use the pre-vendored `rustc-src` dist component.')
816                 raise Exception("{} not found".format(vendor_dir))
817
818             if not os.path.exists(cargo_dir):
819                 print('error: vendoring required, but .cargo/config does not exist.')
820                 raise Exception("{} not found".format(cargo_dir))
821         else:
822             if os.path.exists(cargo_dir):
823                 shutil.rmtree(cargo_dir)
824
825 def bootstrap(help_triggered):
826     """Configure, fetch, build and run the initial bootstrap"""
827
828     # If the user is asking for help, let them know that the whole download-and-build
829     # process has to happen before anything is printed out.
830     if help_triggered:
831         print("info: Downloading and building bootstrap before processing --help")
832         print("      command. See src/bootstrap/README.md for help with common")
833         print("      commands.")
834
835     parser = argparse.ArgumentParser(description='Build rust')
836     parser.add_argument('--config')
837     parser.add_argument('--build-dir')
838     parser.add_argument('--build')
839     parser.add_argument('--color', choices=['always', 'never', 'auto'])
840     parser.add_argument('--clean', action='store_true')
841     parser.add_argument('-v', '--verbose', action='count', default=0)
842
843     args = [a for a in sys.argv if a != '-h' and a != '--help']
844     args, _ = parser.parse_known_args(args)
845
846     # Configure initial bootstrap
847     build = RustBuild()
848     build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
849     build.verbose = args.verbose
850     build.clean = args.clean
851
852     # Read from `--config`, then `RUST_BOOTSTRAP_CONFIG`, then `./config.toml`,
853     # then `config.toml` in the root directory.
854     toml_path = args.config or os.getenv('RUST_BOOTSTRAP_CONFIG')
855     using_default_path = toml_path is None
856     if using_default_path:
857         toml_path = 'config.toml'
858         if not os.path.exists(toml_path):
859             toml_path = os.path.join(build.rust_root, toml_path)
860
861     # Give a hard error if `--config` or `RUST_BOOTSTRAP_CONFIG` are set to a missing path,
862     # but not if `config.toml` hasn't been created.
863     if not using_default_path or os.path.exists(toml_path):
864         with open(toml_path) as config:
865             build.config_toml = config.read()
866
867     profile = build.get_toml('profile')
868     if profile is not None:
869         include_file = 'config.{}.toml'.format(profile)
870         include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
871         include_path = os.path.join(include_dir, include_file)
872         # HACK: This works because `build.get_toml()` returns the first match it finds for a
873         # specific key, so appending our defaults at the end allows the user to override them
874         with open(include_path) as included_toml:
875             build.config_toml += os.linesep + included_toml.read()
876
877     config_verbose = build.get_toml('verbose', 'build')
878     if config_verbose is not None:
879         build.verbose = max(build.verbose, int(config_verbose))
880
881     build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
882
883     build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
884
885     build.check_vendored_status()
886
887     build_dir = args.build_dir or build.get_toml('build-dir', 'build') or 'build'
888     build.build_dir = os.path.abspath(build_dir)
889
890     with open(os.path.join(build.rust_root, "src", "stage0.json")) as f:
891         data = json.load(f)
892     build.checksums_sha256 = data["checksums_sha256"]
893     build.stage0_compiler = Stage0Toolchain(data["compiler"])
894
895     build.set_dist_environment(data["config"]["dist_server"])
896
897     build.build = args.build or build.build_triple()
898
899     if not os.path.exists(build.build_dir):
900         os.makedirs(build.build_dir)
901
902     # Fetch/build the bootstrap
903     build.download_toolchain()
904     sys.stdout.flush()
905     build.build_bootstrap(args.color)
906     sys.stdout.flush()
907
908     # Run the bootstrap
909     args = [build.bootstrap_binary()]
910     args.extend(sys.argv[1:])
911     env = os.environ.copy()
912     env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
913     env["BOOTSTRAP_PYTHON"] = sys.executable
914     run(args, env=env, verbose=build.verbose, is_bootstrap=True)
915
916
917 def main():
918     """Entry point for the bootstrap process"""
919     start_time = time()
920
921     # x.py help <cmd> ...
922     if len(sys.argv) > 1 and sys.argv[1] == 'help':
923         sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
924
925     help_triggered = (
926         '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
927     try:
928         bootstrap(help_triggered)
929         if not help_triggered:
930             print("Build completed successfully in {}".format(
931                 format_build_time(time() - start_time)))
932     except (SystemExit, KeyboardInterrupt) as error:
933         if hasattr(error, 'code') and isinstance(error.code, int):
934             exit_code = error.code
935         else:
936             exit_code = 1
937             print(error)
938         if not help_triggered:
939             print("Build completed unsuccessfully in {}".format(
940                 format_build_time(time() - start_time)))
941         sys.exit(exit_code)
942
943
944 if __name__ == '__main__':
945     main()