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