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