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