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