]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/bootstrap.py
switch stage0.txt to stage0.json and add a tool to generate it
[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
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(url, path, verbose=False, do_verify=True):
29     suffix = '.sha256'
30     sha_url = url + suffix
31     with tempfile.NamedTemporaryFile(delete=False) as temp_file:
32         temp_path = temp_file.name
33     with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as sha_file:
34         sha_path = sha_file.name
35
36     try:
37         if do_verify:
38             download(sha_path, sha_url, False, verbose)
39             if os.path.exists(path):
40                 if verify(path, sha_path, 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, url, True, verbose)
50         if do_verify and not verify(temp_path, sha_path, 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         delete_if_present(sha_path, verbose)
57         delete_if_present(temp_path, verbose)
58
59
60 def delete_if_present(path, verbose):
61     """Remove the given file if present"""
62     if os.path.isfile(path):
63         if verbose:
64             print("removing", path)
65         os.unlink(path)
66
67
68 def download(path, url, probably_big, verbose):
69     for _ in range(0, 4):
70         try:
71             _download(path, url, probably_big, verbose, True)
72             return
73         except RuntimeError:
74             print("\nspurious failure, trying again")
75     _download(path, url, probably_big, verbose, False)
76
77
78 def _download(path, url, probably_big, verbose, exception):
79     if probably_big or verbose:
80         print("downloading {}".format(url))
81     # see https://serverfault.com/questions/301128/how-to-download
82     if sys.platform == 'win32':
83         run(["PowerShell.exe", "/nologo", "-Command",
84              "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
85              "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
86             verbose=verbose,
87             exception=exception)
88     else:
89         if probably_big or verbose:
90             option = "-#"
91         else:
92             option = "-s"
93         require(["curl", "--version"])
94         run(["curl", option,
95              "-y", "30", "-Y", "10",    # timeout if speed is < 10 bytes/sec for > 30 seconds
96              "--connect-timeout", "30",  # timeout if cannot connect within 30 seconds
97              "--retry", "3", "-Sf", "-o", path, url],
98             verbose=verbose,
99             exception=exception)
100
101
102 def verify(path, sha_path, verbose):
103     """Check if the sha256 sum of the given path is valid"""
104     if verbose:
105         print("verifying", path)
106     with open(path, "rb") as source:
107         found = hashlib.sha256(source.read()).hexdigest()
108     with open(sha_path, "r") as sha256sum:
109         expected = sha256sum.readline().split()[0]
110     verified = found == expected
111     if not verified:
112         print("invalid checksum:\n"
113               "    found:    {}\n"
114               "    expected: {}".format(found, expected))
115     return verified
116
117
118 def unpack(tarball, tarball_suffix, dst, verbose=False, match=None):
119     """Unpack the given tarball file"""
120     print("extracting", tarball)
121     fname = os.path.basename(tarball).replace(tarball_suffix, "")
122     with contextlib.closing(tarfile.open(tarball)) as tar:
123         for member in tar.getnames():
124             if "/" not in member:
125                 continue
126             name = member.replace(fname + "/", "", 1)
127             if match is not None and not name.startswith(match):
128                 continue
129             name = name[len(match) + 1:]
130
131             dst_path = os.path.join(dst, name)
132             if verbose:
133                 print("  extracting", member)
134             tar.extract(member, dst)
135             src_path = os.path.join(dst, member)
136             if os.path.isdir(src_path) and os.path.exists(dst_path):
137                 continue
138             shutil.move(src_path, dst_path)
139     shutil.rmtree(os.path.join(dst, fname))
140
141
142 def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
143     """Run a child program in a new process"""
144     if verbose:
145         print("running: " + ' '.join(args))
146     sys.stdout.flush()
147     # Use Popen here instead of call() as it apparently allows powershell on
148     # Windows to not lock up waiting for input presumably.
149     ret = subprocess.Popen(args, **kwargs)
150     code = ret.wait()
151     if code != 0:
152         err = "failed to run: " + ' '.join(args)
153         if verbose or exception:
154             raise RuntimeError(err)
155         # For most failures, we definitely do want to print this error, or the user will have no
156         # idea what went wrong. But when we've successfully built bootstrap and it failed, it will
157         # have already printed an error above, so there's no need to print the exact command we're
158         # running.
159         if is_bootstrap:
160             sys.exit(1)
161         else:
162             sys.exit(err)
163
164
165 def require(cmd, exit=True):
166     '''Run a command, returning its output.
167     On error,
168         If `exit` is `True`, exit the process.
169         Otherwise, return None.'''
170     try:
171         return subprocess.check_output(cmd).strip()
172     except (subprocess.CalledProcessError, OSError) as exc:
173         if not exit:
174             return None
175         print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
176         print("Please make sure it's installed and in the path.")
177         sys.exit(1)
178
179
180 def format_build_time(duration):
181     """Return a nicer format for build time
182
183     >>> format_build_time('300')
184     '0:05:00'
185     """
186     return str(datetime.timedelta(seconds=int(duration)))
187
188
189 def default_build_triple(verbose):
190     """Build triple as in LLVM"""
191     # If the user already has a host build triple with an existing `rustc`
192     # install, use their preference. This fixes most issues with Windows builds
193     # being detected as GNU instead of MSVC.
194     default_encoding = sys.getdefaultencoding()
195     try:
196         version = subprocess.check_output(["rustc", "--version", "--verbose"],
197                 stderr=subprocess.DEVNULL)
198         version = version.decode(default_encoding)
199         host = next(x for x in version.split('\n') if x.startswith("host: "))
200         triple = host.split("host: ")[1]
201         if verbose:
202             print("detected default triple {}".format(triple))
203         return triple
204     except Exception as e:
205         if verbose:
206             print("rustup not detected: {}".format(e))
207             print("falling back to auto-detect")
208
209     required = sys.platform != 'win32'
210     ostype = require(["uname", "-s"], exit=required)
211     cputype = require(['uname', '-m'], exit=required)
212
213     # If we do not have `uname`, assume Windows.
214     if ostype is None or cputype is None:
215         return 'x86_64-pc-windows-msvc'
216
217     ostype = ostype.decode(default_encoding)
218     cputype = cputype.decode(default_encoding)
219
220     # The goal here is to come up with the same triple as LLVM would,
221     # at least for the subset of platforms we're willing to target.
222     ostype_mapper = {
223         'Darwin': 'apple-darwin',
224         'DragonFly': 'unknown-dragonfly',
225         'FreeBSD': 'unknown-freebsd',
226         'Haiku': 'unknown-haiku',
227         'NetBSD': 'unknown-netbsd',
228         'OpenBSD': 'unknown-openbsd'
229     }
230
231     # Consider the direct transformation first and then the special cases
232     if ostype in ostype_mapper:
233         ostype = ostype_mapper[ostype]
234     elif ostype == 'Linux':
235         os_from_sp = subprocess.check_output(
236             ['uname', '-o']).strip().decode(default_encoding)
237         if os_from_sp == 'Android':
238             ostype = 'linux-android'
239         else:
240             ostype = 'unknown-linux-gnu'
241     elif ostype == 'SunOS':
242         ostype = 'pc-solaris'
243         # On Solaris, uname -m will return a machine classification instead
244         # of a cpu type, so uname -p is recommended instead.  However, the
245         # output from that option is too generic for our purposes (it will
246         # always emit 'i386' on x86/amd64 systems).  As such, isainfo -k
247         # must be used instead.
248         cputype = require(['isainfo', '-k']).decode(default_encoding)
249         # sparc cpus have sun as a target vendor
250         if 'sparc' in cputype:
251             ostype = 'sun-solaris'
252     elif ostype.startswith('MINGW'):
253         # msys' `uname` does not print gcc configuration, but prints msys
254         # configuration. so we cannot believe `uname -m`:
255         # msys1 is always i686 and msys2 is always x86_64.
256         # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
257         # MINGW64 on x86_64.
258         ostype = 'pc-windows-gnu'
259         cputype = 'i686'
260         if os.environ.get('MSYSTEM') == 'MINGW64':
261             cputype = 'x86_64'
262     elif ostype.startswith('MSYS'):
263         ostype = 'pc-windows-gnu'
264     elif ostype.startswith('CYGWIN_NT'):
265         cputype = 'i686'
266         if ostype.endswith('WOW64'):
267             cputype = 'x86_64'
268         ostype = 'pc-windows-gnu'
269     elif sys.platform == 'win32':
270         # Some Windows platforms might have a `uname` command that returns a
271         # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
272         # these cases, fall back to using sys.platform.
273         return 'x86_64-pc-windows-msvc'
274     else:
275         err = "unknown OS type: {}".format(ostype)
276         sys.exit(err)
277
278     if cputype == 'powerpc' and ostype == 'unknown-freebsd':
279         cputype = subprocess.check_output(
280               ['uname', '-p']).strip().decode(default_encoding)
281     cputype_mapper = {
282         'BePC': 'i686',
283         'aarch64': 'aarch64',
284         'amd64': 'x86_64',
285         'arm64': 'aarch64',
286         'i386': 'i686',
287         'i486': 'i686',
288         'i686': 'i686',
289         'i786': 'i686',
290         'powerpc': 'powerpc',
291         'powerpc64': 'powerpc64',
292         'powerpc64le': 'powerpc64le',
293         'ppc': 'powerpc',
294         'ppc64': 'powerpc64',
295         'ppc64le': 'powerpc64le',
296         's390x': 's390x',
297         'x64': 'x86_64',
298         'x86': 'i686',
299         'x86-64': 'x86_64',
300         'x86_64': 'x86_64'
301     }
302
303     # Consider the direct transformation first and then the special cases
304     if cputype in cputype_mapper:
305         cputype = cputype_mapper[cputype]
306     elif cputype in {'xscale', 'arm'}:
307         cputype = 'arm'
308         if ostype == 'linux-android':
309             ostype = 'linux-androideabi'
310         elif ostype == 'unknown-freebsd':
311             cputype = subprocess.check_output(
312                 ['uname', '-p']).strip().decode(default_encoding)
313             ostype = 'unknown-freebsd'
314     elif cputype == 'armv6l':
315         cputype = 'arm'
316         if ostype == 'linux-android':
317             ostype = 'linux-androideabi'
318         else:
319             ostype += 'eabihf'
320     elif cputype in {'armv7l', 'armv8l'}:
321         cputype = 'armv7'
322         if ostype == 'linux-android':
323             ostype = 'linux-androideabi'
324         else:
325             ostype += 'eabihf'
326     elif cputype == 'mips':
327         if sys.byteorder == 'big':
328             cputype = 'mips'
329         elif sys.byteorder == 'little':
330             cputype = 'mipsel'
331         else:
332             raise ValueError("unknown byteorder: {}".format(sys.byteorder))
333     elif cputype == 'mips64':
334         if sys.byteorder == 'big':
335             cputype = 'mips64'
336         elif sys.byteorder == 'little':
337             cputype = 'mips64el'
338         else:
339             raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
340         # only the n64 ABI is supported, indicate it
341         ostype += 'abi64'
342     elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
343         pass
344     else:
345         err = "unknown cpu type: {}".format(cputype)
346         sys.exit(err)
347
348     return "{}-{}".format(cputype, ostype)
349
350
351 @contextlib.contextmanager
352 def output(filepath):
353     tmp = filepath + '.tmp'
354     with open(tmp, 'w') as f:
355         yield f
356     try:
357         if os.path.exists(filepath):
358             os.remove(filepath)  # PermissionError/OSError on Win32 if in use
359     except OSError:
360         shutil.copy2(tmp, filepath)
361         os.remove(tmp)
362         return
363     os.rename(tmp, filepath)
364
365
366 class Stage0Toolchain:
367     def __init__(self, stage0_payload):
368         self.date = stage0_payload["date"]
369         self.version = stage0_payload["version"]
370
371     def channel(self):
372         return self.version + "-" + self.date
373
374
375 class RustBuild(object):
376     """Provide all the methods required to build Rust"""
377     def __init__(self):
378         self.stage0_compiler = None
379         self.stage0_rustfmt = None
380         self._download_url = ''
381         self.build = ''
382         self.build_dir = ''
383         self.clean = False
384         self.config_toml = ''
385         self.rust_root = ''
386         self.use_locked_deps = ''
387         self.use_vendored_sources = ''
388         self.verbose = False
389         self.git_version = None
390         self.nix_deps_dir = None
391         self.rustc_commit = None
392
393     def download_toolchain(self, stage0=True, rustc_channel=None):
394         """Fetch the build system for Rust, written in Rust
395
396         This method will build a cache directory, then it will fetch the
397         tarball which has the stage0 compiler used to then bootstrap the Rust
398         compiler itself.
399
400         Each downloaded tarball is extracted, after that, the script
401         will move all the content to the right place.
402         """
403         if rustc_channel is None:
404             rustc_channel = self.stage0_compiler.version
405         bin_root = self.bin_root(stage0)
406
407         key = self.stage0_compiler.date
408         if not stage0:
409             key += str(self.rustc_commit)
410         if self.rustc(stage0).startswith(bin_root) and \
411                 (not os.path.exists(self.rustc(stage0)) or
412                  self.program_out_of_date(self.rustc_stamp(stage0), key)):
413             if os.path.exists(bin_root):
414                 shutil.rmtree(bin_root)
415             tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
416             filename = "rust-std-{}-{}{}".format(
417                 rustc_channel, self.build, tarball_suffix)
418             pattern = "rust-std-{}".format(self.build)
419             self._download_component_helper(filename, pattern, tarball_suffix, stage0)
420             filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
421                                               tarball_suffix)
422             self._download_component_helper(filename, "rustc", tarball_suffix, stage0)
423             # download-rustc doesn't need its own cargo, it can just use beta's.
424             if stage0:
425                 filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
426                                                 tarball_suffix)
427                 self._download_component_helper(filename, "cargo", tarball_suffix)
428                 self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
429             else:
430                 filename = "rustc-dev-{}-{}{}".format(rustc_channel, self.build, tarball_suffix)
431                 self._download_component_helper(
432                     filename, "rustc-dev", tarball_suffix, stage0
433                 )
434
435             self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
436             self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
437             lib_dir = "{}/lib".format(bin_root)
438             for lib in os.listdir(lib_dir):
439                 if lib.endswith(".so"):
440                     self.fix_bin_or_dylib(os.path.join(lib_dir, lib))
441             with output(self.rustc_stamp(stage0)) as rust_stamp:
442                 rust_stamp.write(key)
443
444         if self.rustfmt() and self.rustfmt().startswith(bin_root) and (
445             not os.path.exists(self.rustfmt())
446             or self.program_out_of_date(
447                 self.rustfmt_stamp(),
448                 "" if self.stage0_rustfmt is None else self.stage0_rustfmt.channel()
449             )
450         ):
451             if self.stage0_rustfmt is not None:
452                 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
453                 filename = "rustfmt-{}-{}{}".format(
454                     self.stage0_rustfmt.version, self.build, tarball_suffix,
455                 )
456                 self._download_component_helper(
457                     filename, "rustfmt-preview", tarball_suffix, key=self.stage0_rustfmt.date
458                 )
459                 self.fix_bin_or_dylib("{}/bin/rustfmt".format(bin_root))
460                 self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(bin_root))
461                 with output(self.rustfmt_stamp()) as rustfmt_stamp:
462                     rustfmt_stamp.write(self.stage0_rustfmt.channel())
463
464         # Avoid downloading LLVM twice (once for stage0 and once for the master rustc)
465         if self.downloading_llvm() and stage0:
466             # We want the most recent LLVM submodule update to avoid downloading
467             # LLVM more often than necessary.
468             #
469             # This git command finds that commit SHA, looking for bors-authored
470             # merges that modified src/llvm-project or other relevant version
471             # stamp files.
472             #
473             # This works even in a repository that has not yet initialized
474             # submodules.
475             top_level = subprocess.check_output([
476                 "git", "rev-parse", "--show-toplevel",
477             ]).decode(sys.getdefaultencoding()).strip()
478             llvm_sha = subprocess.check_output([
479                 "git", "rev-list", "--author=bors@rust-lang.org", "-n1",
480                 "--merges", "--first-parent", "HEAD",
481                 "--",
482                 "{}/src/llvm-project".format(top_level),
483                 "{}/src/bootstrap/download-ci-llvm-stamp".format(top_level),
484                 # the LLVM shared object file is named `LLVM-12-rust-{version}-nightly`
485                 "{}/src/version".format(top_level)
486             ]).decode(sys.getdefaultencoding()).strip()
487             llvm_assertions = self.get_toml('assertions', 'llvm') == 'true'
488             llvm_root = self.llvm_root()
489             llvm_lib = os.path.join(llvm_root, "lib")
490             if self.program_out_of_date(self.llvm_stamp(), llvm_sha + str(llvm_assertions)):
491                 self._download_ci_llvm(llvm_sha, llvm_assertions)
492                 for binary in ["llvm-config", "FileCheck"]:
493                     self.fix_bin_or_dylib(os.path.join(llvm_root, "bin", binary))
494                 for lib in os.listdir(llvm_lib):
495                     if lib.endswith(".so"):
496                         self.fix_bin_or_dylib(os.path.join(llvm_lib, lib))
497                 with output(self.llvm_stamp()) as llvm_stamp:
498                     llvm_stamp.write(llvm_sha + str(llvm_assertions))
499
500     def downloading_llvm(self):
501         opt = self.get_toml('download-ci-llvm', 'llvm')
502         # This is currently all tier 1 targets (since others may not have CI
503         # artifacts)
504         # https://doc.rust-lang.org/rustc/platform-support.html#tier-1
505         supported_platforms = [
506             "aarch64-unknown-linux-gnu",
507             "i686-pc-windows-gnu",
508             "i686-pc-windows-msvc",
509             "i686-unknown-linux-gnu",
510             "x86_64-unknown-linux-gnu",
511             "x86_64-apple-darwin",
512             "x86_64-pc-windows-gnu",
513             "x86_64-pc-windows-msvc",
514         ]
515         return opt == "true" \
516             or (opt == "if-available" and self.build in supported_platforms)
517
518     def _download_component_helper(
519         self, filename, pattern, tarball_suffix, stage0=True, key=None
520     ):
521         if key is None:
522             if stage0:
523                 key = self.stage0_compiler.date
524             else:
525                 key = self.rustc_commit
526         cache_dst = os.path.join(self.build_dir, "cache")
527         rustc_cache = os.path.join(cache_dst, key)
528         if not os.path.exists(rustc_cache):
529             os.makedirs(rustc_cache)
530
531         if stage0:
532             url = "{}/dist/{}".format(self._download_url, key)
533         else:
534             url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(self.rustc_commit)
535         tarball = os.path.join(rustc_cache, filename)
536         if not os.path.exists(tarball):
537             get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=stage0)
538         unpack(tarball, tarball_suffix, self.bin_root(stage0), match=pattern, verbose=self.verbose)
539
540     def _download_ci_llvm(self, llvm_sha, llvm_assertions):
541         cache_prefix = "llvm-{}-{}".format(llvm_sha, llvm_assertions)
542         cache_dst = os.path.join(self.build_dir, "cache")
543         rustc_cache = os.path.join(cache_dst, cache_prefix)
544         if not os.path.exists(rustc_cache):
545             os.makedirs(rustc_cache)
546
547         url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(llvm_sha)
548         if llvm_assertions:
549             url = url.replace('rustc-builds', 'rustc-builds-alt')
550         # ci-artifacts are only stored as .xz, not .gz
551         if not support_xz():
552             print("error: XZ support is required to download LLVM")
553             print("help: consider disabling `download-ci-llvm` or using python3")
554             exit(1)
555         tarball_suffix = '.tar.xz'
556         filename = "rust-dev-nightly-" + self.build + tarball_suffix
557         tarball = os.path.join(rustc_cache, filename)
558         if not os.path.exists(tarball):
559             get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=False)
560         unpack(tarball, tarball_suffix, self.llvm_root(),
561                 match="rust-dev",
562                 verbose=self.verbose)
563
564     def fix_bin_or_dylib(self, fname):
565         """Modifies the interpreter section of 'fname' to fix the dynamic linker,
566         or the RPATH section, to fix the dynamic library search path
567
568         This method is only required on NixOS and uses the PatchELF utility to
569         change the interpreter/RPATH of ELF executables.
570
571         Please see https://nixos.org/patchelf.html for more information
572         """
573         default_encoding = sys.getdefaultencoding()
574         try:
575             ostype = subprocess.check_output(
576                 ['uname', '-s']).strip().decode(default_encoding)
577         except subprocess.CalledProcessError:
578             return
579         except OSError as reason:
580             if getattr(reason, 'winerror', None) is not None:
581                 return
582             raise reason
583
584         if ostype != "Linux":
585             return
586
587         # Use `/etc/os-release` instead of `/etc/NIXOS`.
588         # The latter one does not exist on NixOS when using tmpfs as root.
589         try:
590             with open("/etc/os-release", "r") as f:
591                 if not any(line.strip() == "ID=nixos" for line in f):
592                     return
593         except FileNotFoundError:
594             return
595         if os.path.exists("/lib"):
596             return
597
598         # At this point we're pretty sure the user is running NixOS
599         nix_os_msg = "info: you seem to be running NixOS. Attempting to patch"
600         print(nix_os_msg, fname)
601
602         # Only build `.nix-deps` once.
603         nix_deps_dir = self.nix_deps_dir
604         if not nix_deps_dir:
605             # Run `nix-build` to "build" each dependency (which will likely reuse
606             # the existing `/nix/store` copy, or at most download a pre-built copy).
607             #
608             # Importantly, we create a gc-root called `.nix-deps` in the `build/`
609             # directory, but still reference the actual `/nix/store` path in the rpath
610             # as it makes it significantly more robust against changes to the location of
611             # the `.nix-deps` location.
612             #
613             # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
614             # zlib: Needed as a system dependency of `libLLVM-*.so`.
615             # patchelf: Needed for patching ELF binaries (see doc comment above).
616             nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
617             nix_expr = '''
618             with (import <nixpkgs> {});
619             symlinkJoin {
620               name = "rust-stage0-dependencies";
621               paths = [
622                 zlib
623                 patchelf
624                 stdenv.cc.bintools
625               ];
626             }
627             '''
628             try:
629                 subprocess.check_output([
630                     "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
631                 ])
632             except subprocess.CalledProcessError as reason:
633                 print("warning: failed to call nix-build:", reason)
634                 return
635             self.nix_deps_dir = nix_deps_dir
636
637         patchelf = "{}/bin/patchelf".format(nix_deps_dir)
638         rpath_entries = [
639             # Relative default, all binary and dynamic libraries we ship
640             # appear to have this (even when `../lib` is redundant).
641             "$ORIGIN/../lib",
642             os.path.join(os.path.realpath(nix_deps_dir), "lib")
643         ]
644         patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
645         if not fname.endswith(".so"):
646             # Finally, set the corret .interp for binaries
647             with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
648                 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
649
650         try:
651             subprocess.check_output([patchelf] + patchelf_args + [fname])
652         except subprocess.CalledProcessError as reason:
653             print("warning: failed to call patchelf:", reason)
654             return
655
656     # If `download-rustc` is set, download the most recent commit with CI artifacts
657     def maybe_download_ci_toolchain(self):
658         # If `download-rustc` is not set, default to rebuilding.
659         download_rustc = self.get_toml("download-rustc", section="rust")
660         if download_rustc is None or download_rustc == "false":
661             return None
662         assert download_rustc == "true" or download_rustc == "if-unchanged", download_rustc
663
664         # Handle running from a directory other than the top level
665         rev_parse = ["git", "rev-parse", "--show-toplevel"]
666         top_level = subprocess.check_output(rev_parse, universal_newlines=True).strip()
667         compiler = "{}/compiler/".format(top_level)
668         library = "{}/library/".format(top_level)
669
670         # Look for a version to compare to based on the current commit.
671         # Only commits merged by bors will have CI artifacts.
672         merge_base = [
673             "git", "rev-list", "--author=bors@rust-lang.org", "-n1",
674             "--merges", "--first-parent", "HEAD"
675         ]
676         commit = subprocess.check_output(merge_base, universal_newlines=True).strip()
677
678         # Warn if there were changes to the compiler or standard library since the ancestor commit.
679         status = subprocess.call(["git", "diff-index", "--quiet", commit, "--", compiler, library])
680         if status != 0:
681             if download_rustc == "if-unchanged":
682                 return None
683             print("warning: `download-rustc` is enabled, but there are changes to \
684                    compiler/ or library/")
685
686         if self.verbose:
687             print("using downloaded stage1 artifacts from CI (commit {})".format(commit))
688         self.rustc_commit = commit
689         # FIXME: support downloading artifacts from the beta channel
690         self.download_toolchain(False, "nightly")
691
692     def rustc_stamp(self, stage0):
693         """Return the path for .rustc-stamp at the given stage
694
695         >>> rb = RustBuild()
696         >>> rb.build_dir = "build"
697         >>> rb.rustc_stamp(True) == os.path.join("build", "stage0", ".rustc-stamp")
698         True
699         >>> rb.rustc_stamp(False) == os.path.join("build", "ci-rustc", ".rustc-stamp")
700         True
701         """
702         return os.path.join(self.bin_root(stage0), '.rustc-stamp')
703
704     def rustfmt_stamp(self):
705         """Return the path for .rustfmt-stamp
706
707         >>> rb = RustBuild()
708         >>> rb.build_dir = "build"
709         >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
710         True
711         """
712         return os.path.join(self.bin_root(True), '.rustfmt-stamp')
713
714     def llvm_stamp(self):
715         """Return the path for .rustfmt-stamp
716
717         >>> rb = RustBuild()
718         >>> rb.build_dir = "build"
719         >>> rb.llvm_stamp() == os.path.join("build", "ci-llvm", ".llvm-stamp")
720         True
721         """
722         return os.path.join(self.llvm_root(), '.llvm-stamp')
723
724
725     def program_out_of_date(self, stamp_path, key):
726         """Check if the given program stamp is out of date"""
727         if not os.path.exists(stamp_path) or self.clean:
728             return True
729         with open(stamp_path, 'r') as stamp:
730             return key != stamp.read()
731
732     def bin_root(self, stage0):
733         """Return the binary root directory for the given stage
734
735         >>> rb = RustBuild()
736         >>> rb.build_dir = "build"
737         >>> rb.bin_root(True) == os.path.join("build", "stage0")
738         True
739         >>> rb.bin_root(False) == os.path.join("build", "ci-rustc")
740         True
741
742         When the 'build' property is given should be a nested directory:
743
744         >>> rb.build = "devel"
745         >>> rb.bin_root(True) == os.path.join("build", "devel", "stage0")
746         True
747         """
748         if stage0:
749             subdir = "stage0"
750         else:
751             subdir = "ci-rustc"
752         return os.path.join(self.build_dir, self.build, subdir)
753
754     def llvm_root(self):
755         """Return the CI LLVM root directory
756
757         >>> rb = RustBuild()
758         >>> rb.build_dir = "build"
759         >>> rb.llvm_root() == os.path.join("build", "ci-llvm")
760         True
761
762         When the 'build' property is given should be a nested directory:
763
764         >>> rb.build = "devel"
765         >>> rb.llvm_root() == os.path.join("build", "devel", "ci-llvm")
766         True
767         """
768         return os.path.join(self.build_dir, self.build, "ci-llvm")
769
770     def get_toml(self, key, section=None):
771         """Returns the value of the given key in config.toml, otherwise returns None
772
773         >>> rb = RustBuild()
774         >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
775         >>> rb.get_toml("key2")
776         'value2'
777
778         If the key does not exists, the result is None:
779
780         >>> rb.get_toml("key3") is None
781         True
782
783         Optionally also matches the section the key appears in
784
785         >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
786         >>> rb.get_toml('key', 'a')
787         'value1'
788         >>> rb.get_toml('key', 'b')
789         'value2'
790         >>> rb.get_toml('key', 'c') is None
791         True
792
793         >>> rb.config_toml = 'key1 = true'
794         >>> rb.get_toml("key1")
795         'true'
796         """
797
798         cur_section = None
799         for line in self.config_toml.splitlines():
800             section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
801             if section_match is not None:
802                 cur_section = section_match.group(1)
803
804             match = re.match(r'^{}\s*=(.*)$'.format(key), line)
805             if match is not None:
806                 value = match.group(1)
807                 if section is None or section == cur_section:
808                     return self.get_string(value) or value.strip()
809         return None
810
811     def cargo(self):
812         """Return config path for cargo"""
813         return self.program_config('cargo')
814
815     def rustc(self, stage0):
816         """Return config path for rustc"""
817         return self.program_config('rustc', stage0)
818
819     def rustfmt(self):
820         """Return config path for rustfmt"""
821         if self.stage0_rustfmt is None:
822             return None
823         return self.program_config('rustfmt')
824
825     def program_config(self, program, stage0=True):
826         """Return config path for the given program at the given stage
827
828         >>> rb = RustBuild()
829         >>> rb.config_toml = 'rustc = "rustc"\\n'
830         >>> rb.program_config('rustc')
831         'rustc'
832         >>> rb.config_toml = ''
833         >>> cargo_path = rb.program_config('cargo', True)
834         >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(True),
835         ... "bin", "cargo")
836         True
837         >>> cargo_path = rb.program_config('cargo', False)
838         >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(False),
839         ... "bin", "cargo")
840         True
841         """
842         config = self.get_toml(program)
843         if config:
844             return os.path.expanduser(config)
845         return os.path.join(self.bin_root(stage0), "bin", "{}{}".format(
846             program, self.exe_suffix()))
847
848     @staticmethod
849     def get_string(line):
850         """Return the value between double quotes
851
852         >>> RustBuild.get_string('    "devel"   ')
853         'devel'
854         >>> RustBuild.get_string("    'devel'   ")
855         'devel'
856         >>> RustBuild.get_string('devel') is None
857         True
858         >>> RustBuild.get_string('    "devel   ')
859         ''
860         """
861         start = line.find('"')
862         if start != -1:
863             end = start + 1 + line[start + 1:].find('"')
864             return line[start + 1:end]
865         start = line.find('\'')
866         if start != -1:
867             end = start + 1 + line[start + 1:].find('\'')
868             return line[start + 1:end]
869         return None
870
871     @staticmethod
872     def exe_suffix():
873         """Return a suffix for executables"""
874         if sys.platform == 'win32':
875             return '.exe'
876         return ''
877
878     def bootstrap_binary(self):
879         """Return the path of the bootstrap binary
880
881         >>> rb = RustBuild()
882         >>> rb.build_dir = "build"
883         >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
884         ... "debug", "bootstrap")
885         True
886         """
887         return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
888
889     def build_bootstrap(self):
890         """Build bootstrap"""
891         build_dir = os.path.join(self.build_dir, "bootstrap")
892         if self.clean and os.path.exists(build_dir):
893             shutil.rmtree(build_dir)
894         env = os.environ.copy()
895         # `CARGO_BUILD_TARGET` breaks bootstrap build.
896         # See also: <https://github.com/rust-lang/rust/issues/70208>.
897         if "CARGO_BUILD_TARGET" in env:
898             del env["CARGO_BUILD_TARGET"]
899         env["CARGO_TARGET_DIR"] = build_dir
900         env["RUSTC"] = self.rustc(True)
901         env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
902             (os.pathsep + env["LD_LIBRARY_PATH"]) \
903             if "LD_LIBRARY_PATH" in env else ""
904         env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
905             (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
906             if "DYLD_LIBRARY_PATH" in env else ""
907         env["LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
908             (os.pathsep + env["LIBRARY_PATH"]) \
909             if "LIBRARY_PATH" in env else ""
910         # preserve existing RUSTFLAGS
911         env.setdefault("RUSTFLAGS", "")
912         env["RUSTFLAGS"] += " -Cdebuginfo=2"
913
914         build_section = "target.{}".format(self.build)
915         target_features = []
916         if self.get_toml("crt-static", build_section) == "true":
917             target_features += ["+crt-static"]
918         elif self.get_toml("crt-static", build_section) == "false":
919             target_features += ["-crt-static"]
920         if target_features:
921             env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
922         target_linker = self.get_toml("linker", build_section)
923         if target_linker is not None:
924             env["RUSTFLAGS"] += " -C linker=" + target_linker
925         env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
926         env["RUSTFLAGS"] += " -Wsemicolon_in_expressions_from_macros"
927         if self.get_toml("deny-warnings", "rust") != "false":
928             env["RUSTFLAGS"] += " -Dwarnings"
929
930         env["PATH"] = os.path.join(self.bin_root(True), "bin") + \
931             os.pathsep + env["PATH"]
932         if not os.path.isfile(self.cargo()):
933             raise Exception("no cargo executable found at `{}`".format(
934                 self.cargo()))
935         args = [self.cargo(), "build", "--manifest-path",
936                 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
937         for _ in range(1, self.verbose):
938             args.append("--verbose")
939         if self.use_locked_deps:
940             args.append("--locked")
941         if self.use_vendored_sources:
942             args.append("--frozen")
943         run(args, env=env, verbose=self.verbose)
944
945     def build_triple(self):
946         """Build triple as in LLVM
947
948         Note that `default_build_triple` is moderately expensive,
949         so use `self.build` where possible.
950         """
951         config = self.get_toml('build')
952         if config:
953             return config
954         return default_build_triple(self.verbose)
955
956     def check_submodule(self, module, slow_submodules):
957         if not slow_submodules:
958             checked_out = subprocess.Popen(["git", "rev-parse", "HEAD"],
959                                            cwd=os.path.join(self.rust_root, module),
960                                            stdout=subprocess.PIPE)
961             return checked_out
962         else:
963             return None
964
965     def update_submodule(self, module, checked_out, recorded_submodules):
966         module_path = os.path.join(self.rust_root, module)
967
968         if checked_out is not None:
969             default_encoding = sys.getdefaultencoding()
970             checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
971             if recorded_submodules[module] == checked_out:
972                 return
973
974         print("Updating submodule", module)
975
976         run(["git", "submodule", "-q", "sync", module],
977             cwd=self.rust_root, verbose=self.verbose)
978
979         update_args = ["git", "submodule", "update", "--init", "--recursive"]
980         if self.git_version >= distutils.version.LooseVersion("2.11.0"):
981             update_args.append("--progress")
982         update_args.append(module)
983         run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
984
985         run(["git", "reset", "-q", "--hard"],
986             cwd=module_path, verbose=self.verbose)
987         run(["git", "clean", "-qdfx"],
988             cwd=module_path, verbose=self.verbose)
989
990     def update_submodules(self):
991         """Update submodules"""
992         if (not os.path.exists(os.path.join(self.rust_root, ".git"))) or \
993                 self.get_toml('submodules') == "false":
994             return
995
996         default_encoding = sys.getdefaultencoding()
997
998         # check the existence and version of 'git' command
999         git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
1000         self.git_version = distutils.version.LooseVersion(git_version_str)
1001
1002         slow_submodules = self.get_toml('fast-submodules') == "false"
1003         start_time = time()
1004         if slow_submodules:
1005             print('Unconditionally updating submodules')
1006         else:
1007             print('Updating only changed submodules')
1008         default_encoding = sys.getdefaultencoding()
1009         # Only update submodules that are needed to build bootstrap.  These are needed because Cargo
1010         # currently requires everything in a workspace to be "locally present" when starting a
1011         # build, and will give a hard error if any Cargo.toml files are missing.
1012         # FIXME: Is there a way to avoid cloning these eagerly? Bootstrap itself doesn't need to
1013         #   share a workspace with any tools - maybe it could be excluded from the workspace?
1014         #   That will still require cloning the submodules the second you check the standard
1015         #   library, though...
1016         # FIXME: Is there a way to avoid hard-coding the submodules required?
1017         # WARNING: keep this in sync with the submodules hard-coded in bootstrap/lib.rs
1018         submodules = [
1019             "src/tools/rust-installer",
1020             "src/tools/cargo",
1021             "src/tools/rls",
1022             "src/tools/miri",
1023             "library/backtrace",
1024             "library/stdarch"
1025         ]
1026         filtered_submodules = []
1027         submodules_names = []
1028         for module in submodules:
1029             check = self.check_submodule(module, slow_submodules)
1030             filtered_submodules.append((module, check))
1031             submodules_names.append(module)
1032         recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
1033                                     cwd=self.rust_root, stdout=subprocess.PIPE)
1034         recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
1035         # { filename: hash }
1036         recorded_submodules = {}
1037         for data in recorded:
1038             # [mode, kind, hash, filename]
1039             data = data.split()
1040             recorded_submodules[data[3]] = data[2]
1041         for module in filtered_submodules:
1042             self.update_submodule(module[0], module[1], recorded_submodules)
1043         print("Submodules updated in %.2f seconds" % (time() - start_time))
1044
1045     def set_dist_environment(self, url):
1046         """Set download URL for normal environment"""
1047         if 'RUSTUP_DIST_SERVER' in os.environ:
1048             self._download_url = os.environ['RUSTUP_DIST_SERVER']
1049         else:
1050             self._download_url = url
1051
1052     def check_vendored_status(self):
1053         """Check that vendoring is configured properly"""
1054         vendor_dir = os.path.join(self.rust_root, 'vendor')
1055         if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
1056             if os.environ.get('USER') != os.environ['SUDO_USER']:
1057                 self.use_vendored_sources = True
1058                 print('info: looks like you are running this command under `sudo`')
1059                 print('      and so in order to preserve your $HOME this will now')
1060                 print('      use vendored sources by default.')
1061                 if not os.path.exists(vendor_dir):
1062                     print('error: vendoring required, but vendor directory does not exist.')
1063                     print('       Run `cargo vendor` without sudo to initialize the '
1064                           'vendor directory.')
1065                     raise Exception("{} not found".format(vendor_dir))
1066
1067         if self.use_vendored_sources:
1068             if not os.path.exists('.cargo'):
1069                 os.makedirs('.cargo')
1070             with output('.cargo/config') as cargo_config:
1071                 cargo_config.write(
1072                     "[source.crates-io]\n"
1073                     "replace-with = 'vendored-sources'\n"
1074                     "registry = 'https://example.com'\n"
1075                     "\n"
1076                     "[source.vendored-sources]\n"
1077                     "directory = '{}/vendor'\n"
1078                     .format(self.rust_root))
1079         else:
1080             if os.path.exists('.cargo'):
1081                 shutil.rmtree('.cargo')
1082
1083     def ensure_vendored(self):
1084         """Ensure that the vendored sources are available if needed"""
1085         vendor_dir = os.path.join(self.rust_root, 'vendor')
1086         # Note that this does not handle updating the vendored dependencies if
1087         # the rust git repository is updated. Normal development usually does
1088         # not use vendoring, so hopefully this isn't too much of a problem.
1089         if self.use_vendored_sources and not os.path.exists(vendor_dir):
1090             run([
1091                 self.cargo(),
1092                 "vendor",
1093                 "--sync=./src/tools/rust-analyzer/Cargo.toml",
1094                 "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
1095             ], verbose=self.verbose, cwd=self.rust_root)
1096
1097
1098 def bootstrap(help_triggered):
1099     """Configure, fetch, build and run the initial bootstrap"""
1100
1101     # If the user is asking for help, let them know that the whole download-and-build
1102     # process has to happen before anything is printed out.
1103     if help_triggered:
1104         print("info: Downloading and building bootstrap before processing --help")
1105         print("      command. See src/bootstrap/README.md for help with common")
1106         print("      commands.")
1107
1108     parser = argparse.ArgumentParser(description='Build rust')
1109     parser.add_argument('--config')
1110     parser.add_argument('--build')
1111     parser.add_argument('--clean', action='store_true')
1112     parser.add_argument('-v', '--verbose', action='count', default=0)
1113
1114     args = [a for a in sys.argv if a != '-h' and a != '--help']
1115     args, _ = parser.parse_known_args(args)
1116
1117     # Configure initial bootstrap
1118     build = RustBuild()
1119     build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1120     build.verbose = args.verbose
1121     build.clean = args.clean
1122
1123     # Read from `RUST_BOOTSTRAP_CONFIG`, then `--config`, then fallback to `config.toml` (if it
1124     # exists).
1125     toml_path = os.getenv('RUST_BOOTSTRAP_CONFIG') or args.config
1126     if not toml_path and os.path.exists('config.toml'):
1127         toml_path = 'config.toml'
1128
1129     if toml_path:
1130         if not os.path.exists(toml_path):
1131             toml_path = os.path.join(build.rust_root, toml_path)
1132
1133         with open(toml_path) as config:
1134             build.config_toml = config.read()
1135
1136     profile = build.get_toml('profile')
1137     if profile is not None:
1138         include_file = 'config.{}.toml'.format(profile)
1139         include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
1140         include_path = os.path.join(include_dir, include_file)
1141         # HACK: This works because `build.get_toml()` returns the first match it finds for a
1142         # specific key, so appending our defaults at the end allows the user to override them
1143         with open(include_path) as included_toml:
1144             build.config_toml += os.linesep + included_toml.read()
1145
1146     config_verbose = build.get_toml('verbose', 'build')
1147     if config_verbose is not None:
1148         build.verbose = max(build.verbose, int(config_verbose))
1149
1150     build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
1151
1152     build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
1153
1154     build.check_vendored_status()
1155
1156     build_dir = build.get_toml('build-dir', 'build') or 'build'
1157     build.build_dir = os.path.abspath(build_dir.replace("$ROOT", build.rust_root))
1158
1159     with open(os.path.join(build.rust_root, "src", "stage0.json")) as f:
1160         data = json.load(f)
1161     build.stage0_compiler = Stage0Toolchain(data["compiler"])
1162     if data.get("rustfmt") is not None:
1163         build.stage0_rustfmt = Stage0Toolchain(data["rustfmt"])
1164
1165     build.set_dist_environment(data["dist_server"])
1166
1167     build.build = args.build or build.build_triple()
1168     build.update_submodules()
1169
1170     # Fetch/build the bootstrap
1171     build.download_toolchain()
1172     # Download the master compiler if `download-rustc` is set
1173     build.maybe_download_ci_toolchain()
1174     sys.stdout.flush()
1175     build.ensure_vendored()
1176     build.build_bootstrap()
1177     sys.stdout.flush()
1178
1179     # Run the bootstrap
1180     args = [build.bootstrap_binary()]
1181     args.extend(sys.argv[1:])
1182     env = os.environ.copy()
1183     env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1184     env["BOOTSTRAP_PYTHON"] = sys.executable
1185     env["BUILD_DIR"] = build.build_dir
1186     env["RUSTC_BOOTSTRAP"] = '1'
1187     if toml_path:
1188         env["BOOTSTRAP_CONFIG"] = toml_path
1189     if build.rustc_commit is not None:
1190         env["BOOTSTRAP_DOWNLOAD_RUSTC"] = '1'
1191     run(args, env=env, verbose=build.verbose, is_bootstrap=True)
1192
1193
1194 def main():
1195     """Entry point for the bootstrap process"""
1196     start_time = time()
1197
1198     # x.py help <cmd> ...
1199     if len(sys.argv) > 1 and sys.argv[1] == 'help':
1200         sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
1201
1202     help_triggered = (
1203         '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
1204     try:
1205         bootstrap(help_triggered)
1206         if not help_triggered:
1207             print("Build completed successfully in {}".format(
1208                 format_build_time(time() - start_time)))
1209     except (SystemExit, KeyboardInterrupt) as error:
1210         if hasattr(error, 'code') and isinstance(error.code, int):
1211             exit_code = error.code
1212         else:
1213             exit_code = 1
1214             print(error)
1215         if not help_triggered:
1216             print("Build completed unsuccessfully in {}".format(
1217                 format_build_time(time() - start_time)))
1218         sys.exit(exit_code)
1219
1220
1221 if __name__ == '__main__':
1222     main()