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