]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/bootstrap.py
Rollup merge of #83865 - camelid:disamb-err-fix, 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_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.rustc_channel
398         rustfmt_channel = self.rustfmt_channel
399         bin_root = self.bin_root(stage0)
400
401         key = self.date
402         if not stage0:
403             key += str(self.rustc_commit)
404         if self.rustc(stage0).startswith(bin_root) and \
405                 (not os.path.exists(self.rustc(stage0)) or
406                  self.program_out_of_date(self.rustc_stamp(stage0), key)):
407             if os.path.exists(bin_root):
408                 shutil.rmtree(bin_root)
409             tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
410             filename = "rust-std-{}-{}{}".format(
411                 rustc_channel, self.build, tarball_suffix)
412             pattern = "rust-std-{}".format(self.build)
413             self._download_component_helper(filename, pattern, tarball_suffix, stage0)
414             filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
415                                               tarball_suffix)
416             self._download_component_helper(filename, "rustc", tarball_suffix, stage0)
417             filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
418                                               tarball_suffix)
419             self._download_component_helper(filename, "cargo", tarball_suffix)
420             if not stage0:
421                 filename = "rustc-dev-{}-{}{}".format(rustc_channel, self.build, tarball_suffix)
422                 self._download_component_helper(
423                     filename, "rustc-dev", tarball_suffix, stage0
424                 )
425
426             self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
427             self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
428             self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
429             lib_dir = "{}/lib".format(bin_root)
430             for lib in os.listdir(lib_dir):
431                 if lib.endswith(".so"):
432                     self.fix_bin_or_dylib(os.path.join(lib_dir, lib), rpath_libz=True)
433             with output(self.rustc_stamp(stage0)) as rust_stamp:
434                 rust_stamp.write(key)
435
436         if self.rustfmt() and self.rustfmt().startswith(bin_root) and (
437             not os.path.exists(self.rustfmt())
438             or self.program_out_of_date(self.rustfmt_stamp(), self.rustfmt_channel)
439         ):
440             if rustfmt_channel:
441                 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
442                 [channel, date] = rustfmt_channel.split('-', 1)
443                 filename = "rustfmt-{}-{}{}".format(channel, self.build, tarball_suffix)
444                 self._download_component_helper(
445                     filename, "rustfmt-preview", tarball_suffix, key=date
446                 )
447                 self.fix_bin_or_dylib("{}/bin/rustfmt".format(bin_root))
448                 self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(bin_root))
449                 with output(self.rustfmt_stamp()) as rustfmt_stamp:
450                     rustfmt_stamp.write(self.rustfmt_channel)
451
452         # Avoid downloading LLVM twice (once for stage0 and once for the master rustc)
453         if self.downloading_llvm() and stage0:
454             # We want the most recent LLVM submodule update to avoid downloading
455             # LLVM more often than necessary.
456             #
457             # This git command finds that commit SHA, looking for bors-authored
458             # merges that modified src/llvm-project.
459             #
460             # This works even in a repository that has not yet initialized
461             # submodules.
462             top_level = subprocess.check_output([
463                 "git", "rev-parse", "--show-toplevel",
464             ]).decode(sys.getdefaultencoding()).strip()
465             llvm_sha = subprocess.check_output([
466                 "git", "log", "--author=bors", "--format=%H", "-n1",
467                 "-m", "--first-parent",
468                 "--",
469                 "{}/src/llvm-project".format(top_level),
470                 "{}/src/bootstrap/download-ci-llvm-stamp".format(top_level),
471                 # the LLVM shared object file is named `LLVM-12-rust-{version}-nightly`
472                 "{}/src/version".format(top_level)
473             ]).decode(sys.getdefaultencoding()).strip()
474             llvm_assertions = self.get_toml('assertions', 'llvm') == 'true'
475             llvm_root = self.llvm_root()
476             llvm_lib = os.path.join(llvm_root, "lib")
477             if self.program_out_of_date(self.llvm_stamp(), llvm_sha + str(llvm_assertions)):
478                 self._download_ci_llvm(llvm_sha, llvm_assertions)
479                 for binary in ["llvm-config", "FileCheck"]:
480                     self.fix_bin_or_dylib(os.path.join(llvm_root, "bin", binary), rpath_libz=True)
481                 for lib in os.listdir(llvm_lib):
482                     if lib.endswith(".so"):
483                         self.fix_bin_or_dylib(os.path.join(llvm_lib, lib), rpath_libz=True)
484                 with output(self.llvm_stamp()) as llvm_stamp:
485                     llvm_stamp.write(llvm_sha + str(llvm_assertions))
486
487     def downloading_llvm(self):
488         opt = self.get_toml('download-ci-llvm', 'llvm')
489         # This is currently all tier 1 targets (since others may not have CI
490         # artifacts)
491         # https://doc.rust-lang.org/rustc/platform-support.html#tier-1
492         supported_platforms = [
493             "aarch64-unknown-linux-gnu",
494             "i686-pc-windows-gnu",
495             "i686-pc-windows-msvc",
496             "i686-unknown-linux-gnu",
497             "x86_64-unknown-linux-gnu",
498             "x86_64-apple-darwin",
499             "x86_64-pc-windows-gnu",
500             "x86_64-pc-windows-msvc",
501         ]
502         return opt == "true" \
503             or (opt == "if-available" and self.build in supported_platforms)
504
505     def _download_component_helper(
506         self, filename, pattern, tarball_suffix, stage0=True, key=None
507     ):
508         if key is None:
509             if stage0:
510                 key = self.date
511             else:
512                 key = self.rustc_commit
513         cache_dst = os.path.join(self.build_dir, "cache")
514         rustc_cache = os.path.join(cache_dst, key)
515         if not os.path.exists(rustc_cache):
516             os.makedirs(rustc_cache)
517
518         if stage0:
519             url = "{}/dist/{}".format(self._download_url, key)
520         else:
521             url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(self.rustc_commit)
522         tarball = os.path.join(rustc_cache, filename)
523         if not os.path.exists(tarball):
524             get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=stage0)
525         unpack(tarball, tarball_suffix, self.bin_root(stage0), match=pattern, verbose=self.verbose)
526
527     def _download_ci_llvm(self, llvm_sha, llvm_assertions):
528         cache_prefix = "llvm-{}-{}".format(llvm_sha, llvm_assertions)
529         cache_dst = os.path.join(self.build_dir, "cache")
530         rustc_cache = os.path.join(cache_dst, cache_prefix)
531         if not os.path.exists(rustc_cache):
532             os.makedirs(rustc_cache)
533
534         url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(llvm_sha)
535         if llvm_assertions:
536             url = url.replace('rustc-builds', 'rustc-builds-alt')
537         # ci-artifacts are only stored as .xz, not .gz
538         if not support_xz():
539             print("error: XZ support is required to download LLVM")
540             print("help: consider disabling `download-ci-llvm` or using python3")
541             exit(1)
542         tarball_suffix = '.tar.xz'
543         filename = "rust-dev-nightly-" + self.build + tarball_suffix
544         tarball = os.path.join(rustc_cache, filename)
545         if not os.path.exists(tarball):
546             get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=False)
547         unpack(tarball, tarball_suffix, self.llvm_root(),
548                 match="rust-dev",
549                 verbose=self.verbose)
550
551     def fix_bin_or_dylib(self, fname, rpath_libz=False):
552         """Modifies the interpreter section of 'fname' to fix the dynamic linker,
553         or the RPATH section, to fix the dynamic library search path
554
555         This method is only required on NixOS and uses the PatchELF utility to
556         change the interpreter/RPATH of ELF executables.
557
558         Please see https://nixos.org/patchelf.html for more information
559         """
560         default_encoding = sys.getdefaultencoding()
561         try:
562             ostype = subprocess.check_output(
563                 ['uname', '-s']).strip().decode(default_encoding)
564         except subprocess.CalledProcessError:
565             return
566         except OSError as reason:
567             if getattr(reason, 'winerror', None) is not None:
568                 return
569             raise reason
570
571         if ostype != "Linux":
572             return
573
574         if not os.path.exists("/etc/NIXOS"):
575             return
576         if os.path.exists("/lib"):
577             return
578
579         # At this point we're pretty sure the user is running NixOS
580         nix_os_msg = "info: you seem to be running NixOS. Attempting to patch"
581         print(nix_os_msg, fname)
582
583         # Only build `.nix-deps` once.
584         nix_deps_dir = self.nix_deps_dir
585         if not nix_deps_dir:
586             nix_deps_dir = ".nix-deps"
587             if not os.path.exists(nix_deps_dir):
588                 os.makedirs(nix_deps_dir)
589
590             nix_deps = [
591                 # Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
592                 "stdenv.cc.bintools",
593
594                 # Needed as a system dependency of `libLLVM-*.so`.
595                 "zlib",
596
597                 # Needed for patching ELF binaries (see doc comment above).
598                 "patchelf",
599             ]
600
601             # Run `nix-build` to "build" each dependency (which will likely reuse
602             # the existing `/nix/store` copy, or at most download a pre-built copy).
603             # Importantly, we don't rely on `nix-build` printing the `/nix/store`
604             # path on stdout, but use `-o` to symlink it into `stage0/.nix-deps/$dep`,
605             # ensuring garbage collection will never remove the `/nix/store` path
606             # (which would break our patched binaries that hardcode those paths).
607             for dep in nix_deps:
608                 try:
609                     subprocess.check_output([
610                         "nix-build", "<nixpkgs>",
611                         "-A", dep,
612                         "-o", "{}/{}".format(nix_deps_dir, dep),
613                     ])
614                 except subprocess.CalledProcessError as reason:
615                     print("warning: failed to call nix-build:", reason)
616                     return
617
618             self.nix_deps_dir = nix_deps_dir
619
620         patchelf = "{}/patchelf/bin/patchelf".format(nix_deps_dir)
621         patchelf_args = []
622
623         if rpath_libz:
624             # Patch RPATH to add `zlib` dependency that stems from LLVM
625             dylib_deps = ["zlib"]
626             rpath_entries = [
627                 # Relative default, all binary and dynamic libraries we ship
628                 # appear to have this (even when `../lib` is redundant).
629                 "$ORIGIN/../lib",
630             ] + ["{}/{}/lib".format(nix_deps_dir, dep) for dep in dylib_deps]
631             patchelf_args += ["--set-rpath", ":".join(rpath_entries)]
632         if not fname.endswith(".so"):
633             # Finally, set the corret .interp for binaries
634             bintools_dir = "{}/stdenv.cc.bintools".format(nix_deps_dir)
635             with open("{}/nix-support/dynamic-linker".format(bintools_dir)) as dynamic_linker:
636                 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
637
638         try:
639             subprocess.check_output([patchelf] + patchelf_args + [fname])
640         except subprocess.CalledProcessError as reason:
641             print("warning: failed to call patchelf:", reason)
642             return
643
644     # If `download-rustc` is set, download the most recent commit with CI artifacts
645     def maybe_download_ci_toolchain(self):
646         # If `download-rustc` is not set, default to rebuilding.
647         if self.get_toml("download-rustc", section="rust") != "true":
648             return None
649
650         # Handle running from a directory other than the top level
651         rev_parse = ["git", "rev-parse", "--show-toplevel"]
652         top_level = subprocess.check_output(rev_parse, universal_newlines=True).strip()
653         compiler = "{}/compiler/".format(top_level)
654
655         # Look for a version to compare to based on the current commit.
656         # Only commits merged by bors will have CI artifacts.
657         merge_base = ["git", "log", "--author=bors", "--pretty=%H", "-n1"]
658         commit = subprocess.check_output(merge_base, universal_newlines=True).strip()
659
660         # Warn if there were changes to the compiler since the ancestor commit.
661         status = subprocess.call(["git", "diff-index", "--quiet", commit, "--", compiler])
662         if status != 0:
663             print("warning: `download-rustc` is enabled, but there are changes to compiler/")
664
665         if self.verbose:
666             print("using downloaded stage1 artifacts from CI (commit {})".format(commit))
667         self.rustc_commit = commit
668         # FIXME: support downloading artifacts from the beta channel
669         self.download_toolchain(False, "nightly")
670
671     def rustc_stamp(self, stage0):
672         """Return the path for .rustc-stamp at the given stage
673
674         >>> rb = RustBuild()
675         >>> rb.build_dir = "build"
676         >>> rb.rustc_stamp(True) == os.path.join("build", "stage0", ".rustc-stamp")
677         True
678         >>> rb.rustc_stamp(False) == os.path.join("build", "ci-rustc", ".rustc-stamp")
679         True
680         """
681         return os.path.join(self.bin_root(stage0), '.rustc-stamp')
682
683     def rustfmt_stamp(self):
684         """Return the path for .rustfmt-stamp
685
686         >>> rb = RustBuild()
687         >>> rb.build_dir = "build"
688         >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
689         True
690         """
691         return os.path.join(self.bin_root(True), '.rustfmt-stamp')
692
693     def llvm_stamp(self):
694         """Return the path for .rustfmt-stamp
695
696         >>> rb = RustBuild()
697         >>> rb.build_dir = "build"
698         >>> rb.llvm_stamp() == os.path.join("build", "ci-llvm", ".llvm-stamp")
699         True
700         """
701         return os.path.join(self.llvm_root(), '.llvm-stamp')
702
703
704     def program_out_of_date(self, stamp_path, key):
705         """Check if the given program stamp is out of date"""
706         if not os.path.exists(stamp_path) or self.clean:
707             return True
708         with open(stamp_path, 'r') as stamp:
709             return key != stamp.read()
710
711     def bin_root(self, stage0):
712         """Return the binary root directory for the given stage
713
714         >>> rb = RustBuild()
715         >>> rb.build_dir = "build"
716         >>> rb.bin_root(True) == os.path.join("build", "stage0")
717         True
718         >>> rb.bin_root(False) == os.path.join("build", "ci-rustc")
719         True
720
721         When the 'build' property is given should be a nested directory:
722
723         >>> rb.build = "devel"
724         >>> rb.bin_root(True) == os.path.join("build", "devel", "stage0")
725         True
726         """
727         if stage0:
728             subdir = "stage0"
729         else:
730             subdir = "ci-rustc"
731         return os.path.join(self.build_dir, self.build, subdir)
732
733     def llvm_root(self):
734         """Return the CI LLVM root directory
735
736         >>> rb = RustBuild()
737         >>> rb.build_dir = "build"
738         >>> rb.llvm_root() == os.path.join("build", "ci-llvm")
739         True
740
741         When the 'build' property is given should be a nested directory:
742
743         >>> rb.build = "devel"
744         >>> rb.llvm_root() == os.path.join("build", "devel", "ci-llvm")
745         True
746         """
747         return os.path.join(self.build_dir, self.build, "ci-llvm")
748
749     def get_toml(self, key, section=None):
750         """Returns the value of the given key in config.toml, otherwise returns None
751
752         >>> rb = RustBuild()
753         >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
754         >>> rb.get_toml("key2")
755         'value2'
756
757         If the key does not exists, the result is None:
758
759         >>> rb.get_toml("key3") is None
760         True
761
762         Optionally also matches the section the key appears in
763
764         >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
765         >>> rb.get_toml('key', 'a')
766         'value1'
767         >>> rb.get_toml('key', 'b')
768         'value2'
769         >>> rb.get_toml('key', 'c') is None
770         True
771
772         >>> rb.config_toml = 'key1 = true'
773         >>> rb.get_toml("key1")
774         'true'
775         """
776
777         cur_section = None
778         for line in self.config_toml.splitlines():
779             section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
780             if section_match is not None:
781                 cur_section = section_match.group(1)
782
783             match = re.match(r'^{}\s*=(.*)$'.format(key), line)
784             if match is not None:
785                 value = match.group(1)
786                 if section is None or section == cur_section:
787                     return self.get_string(value) or value.strip()
788         return None
789
790     def cargo(self):
791         """Return config path for cargo"""
792         return self.program_config('cargo')
793
794     def rustc(self, stage0):
795         """Return config path for rustc"""
796         return self.program_config('rustc', stage0)
797
798     def rustfmt(self):
799         """Return config path for rustfmt"""
800         if not self.rustfmt_channel:
801             return None
802         return self.program_config('rustfmt')
803
804     def program_config(self, program, stage0=True):
805         """Return config path for the given program at the given stage
806
807         >>> rb = RustBuild()
808         >>> rb.config_toml = 'rustc = "rustc"\\n'
809         >>> rb.program_config('rustc')
810         'rustc'
811         >>> rb.config_toml = ''
812         >>> cargo_path = rb.program_config('cargo', True)
813         >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(True),
814         ... "bin", "cargo")
815         True
816         >>> cargo_path = rb.program_config('cargo', False)
817         >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(False),
818         ... "bin", "cargo")
819         True
820         """
821         config = self.get_toml(program)
822         if config:
823             return os.path.expanduser(config)
824         return os.path.join(self.bin_root(stage0), "bin", "{}{}".format(
825             program, self.exe_suffix()))
826
827     @staticmethod
828     def get_string(line):
829         """Return the value between double quotes
830
831         >>> RustBuild.get_string('    "devel"   ')
832         'devel'
833         >>> RustBuild.get_string("    'devel'   ")
834         'devel'
835         >>> RustBuild.get_string('devel') is None
836         True
837         >>> RustBuild.get_string('    "devel   ')
838         ''
839         """
840         start = line.find('"')
841         if start != -1:
842             end = start + 1 + line[start + 1:].find('"')
843             return line[start + 1:end]
844         start = line.find('\'')
845         if start != -1:
846             end = start + 1 + line[start + 1:].find('\'')
847             return line[start + 1:end]
848         return None
849
850     @staticmethod
851     def exe_suffix():
852         """Return a suffix for executables"""
853         if sys.platform == 'win32':
854             return '.exe'
855         return ''
856
857     def bootstrap_binary(self):
858         """Return the path of the bootstrap binary
859
860         >>> rb = RustBuild()
861         >>> rb.build_dir = "build"
862         >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
863         ... "debug", "bootstrap")
864         True
865         """
866         return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
867
868     def build_bootstrap(self):
869         """Build bootstrap"""
870         build_dir = os.path.join(self.build_dir, "bootstrap")
871         if self.clean and os.path.exists(build_dir):
872             shutil.rmtree(build_dir)
873         env = os.environ.copy()
874         # `CARGO_BUILD_TARGET` breaks bootstrap build.
875         # See also: <https://github.com/rust-lang/rust/issues/70208>.
876         if "CARGO_BUILD_TARGET" in env:
877             del env["CARGO_BUILD_TARGET"]
878         env["CARGO_TARGET_DIR"] = build_dir
879         env["RUSTC"] = self.rustc(True)
880         env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
881             (os.pathsep + env["LD_LIBRARY_PATH"]) \
882             if "LD_LIBRARY_PATH" in env else ""
883         env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
884             (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
885             if "DYLD_LIBRARY_PATH" in env else ""
886         env["LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
887             (os.pathsep + env["LIBRARY_PATH"]) \
888             if "LIBRARY_PATH" in env else ""
889         # preserve existing RUSTFLAGS
890         env.setdefault("RUSTFLAGS", "")
891         env["RUSTFLAGS"] += " -Cdebuginfo=2"
892
893         build_section = "target.{}".format(self.build)
894         target_features = []
895         if self.get_toml("crt-static", build_section) == "true":
896             target_features += ["+crt-static"]
897         elif self.get_toml("crt-static", build_section) == "false":
898             target_features += ["-crt-static"]
899         if target_features:
900             env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
901         target_linker = self.get_toml("linker", build_section)
902         if target_linker is not None:
903             env["RUSTFLAGS"] += " -C linker=" + target_linker
904         # cfg(bootstrap): Add `-Wsemicolon_in_expressions_from_macros` after the next beta bump
905         env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
906         if self.get_toml("deny-warnings", "rust") != "false":
907             env["RUSTFLAGS"] += " -Dwarnings"
908
909         env["PATH"] = os.path.join(self.bin_root(True), "bin") + \
910             os.pathsep + env["PATH"]
911         if not os.path.isfile(self.cargo()):
912             raise Exception("no cargo executable found at `{}`".format(
913                 self.cargo()))
914         args = [self.cargo(), "build", "--manifest-path",
915                 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
916         for _ in range(1, self.verbose):
917             args.append("--verbose")
918         if self.use_locked_deps:
919             args.append("--locked")
920         if self.use_vendored_sources:
921             args.append("--frozen")
922         run(args, env=env, verbose=self.verbose)
923
924     def build_triple(self):
925         """Build triple as in LLVM
926
927         Note that `default_build_triple` is moderately expensive,
928         so use `self.build` where possible.
929         """
930         config = self.get_toml('build')
931         if config:
932             return config
933         return default_build_triple(self.verbose)
934
935     def check_submodule(self, module, slow_submodules):
936         if not slow_submodules:
937             checked_out = subprocess.Popen(["git", "rev-parse", "HEAD"],
938                                            cwd=os.path.join(self.rust_root, module),
939                                            stdout=subprocess.PIPE)
940             return checked_out
941         else:
942             return None
943
944     def update_submodule(self, module, checked_out, recorded_submodules):
945         module_path = os.path.join(self.rust_root, module)
946
947         if checked_out is not None:
948             default_encoding = sys.getdefaultencoding()
949             checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
950             if recorded_submodules[module] == checked_out:
951                 return
952
953         print("Updating submodule", module)
954
955         run(["git", "submodule", "-q", "sync", module],
956             cwd=self.rust_root, verbose=self.verbose)
957
958         update_args = ["git", "submodule", "update", "--init", "--recursive"]
959         if self.git_version >= distutils.version.LooseVersion("2.11.0"):
960             update_args.append("--progress")
961         update_args.append(module)
962         run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
963
964         run(["git", "reset", "-q", "--hard"],
965             cwd=module_path, verbose=self.verbose)
966         run(["git", "clean", "-qdfx"],
967             cwd=module_path, verbose=self.verbose)
968
969     def update_submodules(self):
970         """Update submodules"""
971         if (not os.path.exists(os.path.join(self.rust_root, ".git"))) or \
972                 self.get_toml('submodules') == "false":
973             return
974
975         default_encoding = sys.getdefaultencoding()
976
977         # check the existence and version of 'git' command
978         git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
979         self.git_version = distutils.version.LooseVersion(git_version_str)
980
981         slow_submodules = self.get_toml('fast-submodules') == "false"
982         start_time = time()
983         if slow_submodules:
984             print('Unconditionally updating all submodules')
985         else:
986             print('Updating only changed submodules')
987         default_encoding = sys.getdefaultencoding()
988         submodules = [s.split(' ', 1)[1] for s in subprocess.check_output(
989             ["git", "config", "--file",
990              os.path.join(self.rust_root, ".gitmodules"),
991              "--get-regexp", "path"]
992         ).decode(default_encoding).splitlines()]
993         filtered_submodules = []
994         submodules_names = []
995         llvm_checked_out = os.path.exists(os.path.join(self.rust_root, "src/llvm-project/.git"))
996         external_llvm_provided = self.get_toml('llvm-config') or self.downloading_llvm()
997         llvm_needed = not self.get_toml('codegen-backends', 'rust') \
998             or "llvm" in self.get_toml('codegen-backends', 'rust')
999         for module in submodules:
1000             if module.endswith("llvm-project"):
1001                 # Don't sync the llvm-project submodule if an external LLVM was
1002                 # provided, if we are downloading LLVM or if the LLVM backend is
1003                 # not being built. Also, if the submodule has been initialized
1004                 # already, sync it anyways so that it doesn't mess up contributor
1005                 # pull requests.
1006                 if external_llvm_provided or not llvm_needed:
1007                     if self.get_toml('lld') != 'true' and not llvm_checked_out:
1008                         continue
1009             check = self.check_submodule(module, slow_submodules)
1010             filtered_submodules.append((module, check))
1011             submodules_names.append(module)
1012         recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
1013                                     cwd=self.rust_root, stdout=subprocess.PIPE)
1014         recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
1015         recorded_submodules = {}
1016         for data in recorded:
1017             data = data.split()
1018             recorded_submodules[data[3]] = data[2]
1019         for module in filtered_submodules:
1020             self.update_submodule(module[0], module[1], recorded_submodules)
1021         print("Submodules updated in %.2f seconds" % (time() - start_time))
1022
1023     def set_normal_environment(self):
1024         """Set download URL for normal environment"""
1025         if 'RUSTUP_DIST_SERVER' in os.environ:
1026             self._download_url = os.environ['RUSTUP_DIST_SERVER']
1027         else:
1028             self._download_url = 'https://static.rust-lang.org'
1029
1030     def set_dev_environment(self):
1031         """Set download URL for development environment"""
1032         if 'RUSTUP_DEV_DIST_SERVER' in os.environ:
1033             self._download_url = os.environ['RUSTUP_DEV_DIST_SERVER']
1034         else:
1035             self._download_url = 'https://dev-static.rust-lang.org'
1036
1037     def check_vendored_status(self):
1038         """Check that vendoring is configured properly"""
1039         vendor_dir = os.path.join(self.rust_root, 'vendor')
1040         if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
1041             if os.environ.get('USER') != os.environ['SUDO_USER']:
1042                 self.use_vendored_sources = True
1043                 print('info: looks like you are running this command under `sudo`')
1044                 print('      and so in order to preserve your $HOME this will now')
1045                 print('      use vendored sources by default.')
1046                 if not os.path.exists(vendor_dir):
1047                     print('error: vendoring required, but vendor directory does not exist.')
1048                     print('       Run `cargo vendor` without sudo to initialize the '
1049                           'vendor directory.')
1050                     raise Exception("{} not found".format(vendor_dir))
1051
1052         if self.use_vendored_sources:
1053             if not os.path.exists('.cargo'):
1054                 os.makedirs('.cargo')
1055             with output('.cargo/config') as cargo_config:
1056                 cargo_config.write(
1057                     "[source.crates-io]\n"
1058                     "replace-with = 'vendored-sources'\n"
1059                     "registry = 'https://example.com'\n"
1060                     "\n"
1061                     "[source.vendored-sources]\n"
1062                     "directory = '{}/vendor'\n"
1063                     .format(self.rust_root))
1064         else:
1065             if os.path.exists('.cargo'):
1066                 shutil.rmtree('.cargo')
1067
1068     def ensure_vendored(self):
1069         """Ensure that the vendored sources are available if needed"""
1070         vendor_dir = os.path.join(self.rust_root, 'vendor')
1071         # Note that this does not handle updating the vendored dependencies if
1072         # the rust git repository is updated. Normal development usually does
1073         # not use vendoring, so hopefully this isn't too much of a problem.
1074         if self.use_vendored_sources and not os.path.exists(vendor_dir):
1075             run([
1076                 self.cargo(),
1077                 "vendor",
1078                 "--sync=./src/tools/rust-analyzer/Cargo.toml",
1079                 "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
1080             ], verbose=self.verbose, cwd=self.rust_root)
1081
1082
1083 def bootstrap(help_triggered):
1084     """Configure, fetch, build and run the initial bootstrap"""
1085
1086     # If the user is asking for help, let them know that the whole download-and-build
1087     # process has to happen before anything is printed out.
1088     if help_triggered:
1089         print("info: Downloading and building bootstrap before processing --help")
1090         print("      command. See src/bootstrap/README.md for help with common")
1091         print("      commands.")
1092
1093     parser = argparse.ArgumentParser(description='Build rust')
1094     parser.add_argument('--config')
1095     parser.add_argument('--build')
1096     parser.add_argument('--clean', action='store_true')
1097     parser.add_argument('-v', '--verbose', action='count', default=0)
1098
1099     args = [a for a in sys.argv if a != '-h' and a != '--help']
1100     args, _ = parser.parse_known_args(args)
1101
1102     # Configure initial bootstrap
1103     build = RustBuild()
1104     build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1105     build.verbose = args.verbose
1106     build.clean = args.clean
1107
1108     # Read from `RUST_BOOTSTRAP_CONFIG`, then `--config`, then fallback to `config.toml` (if it
1109     # exists).
1110     toml_path = os.getenv('RUST_BOOTSTRAP_CONFIG') or args.config
1111     if not toml_path and os.path.exists('config.toml'):
1112         toml_path = 'config.toml'
1113
1114     if toml_path:
1115         if not os.path.exists(toml_path):
1116             toml_path = os.path.join(build.rust_root, toml_path)
1117
1118         with open(toml_path) as config:
1119             build.config_toml = config.read()
1120
1121     profile = build.get_toml('profile')
1122     if profile is not None:
1123         include_file = 'config.{}.toml'.format(profile)
1124         include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
1125         include_path = os.path.join(include_dir, include_file)
1126         # HACK: This works because `build.get_toml()` returns the first match it finds for a
1127         # specific key, so appending our defaults at the end allows the user to override them
1128         with open(include_path) as included_toml:
1129             build.config_toml += os.linesep + included_toml.read()
1130
1131     config_verbose = build.get_toml('verbose', 'build')
1132     if config_verbose is not None:
1133         build.verbose = max(build.verbose, int(config_verbose))
1134
1135     build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
1136
1137     build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
1138
1139     build.check_vendored_status()
1140
1141     build_dir = build.get_toml('build-dir', 'build') or 'build'
1142     build.build_dir = os.path.abspath(build_dir.replace("$ROOT", build.rust_root))
1143
1144     data = stage0_data(build.rust_root)
1145     build.date = data['date']
1146     build.rustc_channel = data['rustc']
1147
1148     if "rustfmt" in data:
1149         build.rustfmt_channel = data['rustfmt']
1150
1151     if 'dev' in data:
1152         build.set_dev_environment()
1153     else:
1154         build.set_normal_environment()
1155
1156     build.build = args.build or build.build_triple()
1157     build.update_submodules()
1158
1159     # Fetch/build the bootstrap
1160     build.download_toolchain()
1161     # Download the master compiler if `download-rustc` is set
1162     build.maybe_download_ci_toolchain()
1163     sys.stdout.flush()
1164     build.ensure_vendored()
1165     build.build_bootstrap()
1166     sys.stdout.flush()
1167
1168     # Run the bootstrap
1169     args = [build.bootstrap_binary()]
1170     args.extend(sys.argv[1:])
1171     env = os.environ.copy()
1172     env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1173     env["BOOTSTRAP_PYTHON"] = sys.executable
1174     env["BUILD_DIR"] = build.build_dir
1175     env["RUSTC_BOOTSTRAP"] = '1'
1176     if toml_path:
1177         env["BOOTSTRAP_CONFIG"] = toml_path
1178     run(args, env=env, verbose=build.verbose)
1179
1180
1181 def main():
1182     """Entry point for the bootstrap process"""
1183     start_time = time()
1184
1185     # x.py help <cmd> ...
1186     if len(sys.argv) > 1 and sys.argv[1] == 'help':
1187         sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
1188
1189     help_triggered = (
1190         '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
1191     try:
1192         bootstrap(help_triggered)
1193         if not help_triggered:
1194             print("Build completed successfully in {}".format(
1195                 format_build_time(time() - start_time)))
1196     except (SystemExit, KeyboardInterrupt) as error:
1197         if hasattr(error, 'code') and isinstance(error.code, int):
1198             exit_code = error.code
1199         else:
1200             exit_code = 1
1201             print(error)
1202         if not help_triggered:
1203             print("Build completed unsuccessfully in {}".format(
1204                 format_build_time(time() - start_time)))
1205         sys.exit(exit_code)
1206
1207
1208 if __name__ == '__main__':
1209     main()