]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/bootstrap.py
Rollup merge of #97627 - lcnr:comment-tick, r=Dylan-DPC
[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):
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)
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):
103     for _ in range(0, 4):
104         try:
105             _download(path, url, probably_big, verbose, True)
106             return
107         except RuntimeError:
108             print("\nspurious failure, trying again")
109     _download(path, url, probably_big, verbose, False)
110
111
112 def _download(path, url, probably_big, verbose, exception):
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         )
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, **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 verbose or exception:
201             raise RuntimeError(err)
202         # For most failures, we definitely do want to print this error, or the user will have no
203         # idea what went wrong. But when we've successfully built bootstrap and it failed, it will
204         # have already printed an error above, so there's no need to print the exact command we're
205         # running.
206         if is_bootstrap:
207             sys.exit(1)
208         else:
209             sys.exit(err)
210
211
212 def require(cmd, exit=True, exception=False):
213     '''Run a command, returning its output.
214     On error,
215         If `exception` is `True`, raise the error
216         Otherwise If `exit` is `True`, exit the process
217         Else return None.'''
218     try:
219         return subprocess.check_output(cmd).strip()
220     except (subprocess.CalledProcessError, OSError) as exc:
221         if exception:
222             raise
223         elif exit:
224             print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
225             print("Please make sure it's installed and in the path.")
226             sys.exit(1)
227         return None
228
229
230
231 def format_build_time(duration):
232     """Return a nicer format for build time
233
234     >>> format_build_time('300')
235     '0:05:00'
236     """
237     return str(datetime.timedelta(seconds=int(duration)))
238
239
240 def default_build_triple(verbose):
241     """Build triple as in LLVM"""
242     # If the user already has a host build triple with an existing `rustc`
243     # install, use their preference. This fixes most issues with Windows builds
244     # being detected as GNU instead of MSVC.
245     default_encoding = sys.getdefaultencoding()
246     try:
247         version = subprocess.check_output(["rustc", "--version", "--verbose"],
248                 stderr=subprocess.DEVNULL)
249         version = version.decode(default_encoding)
250         host = next(x for x in version.split('\n') if x.startswith("host: "))
251         triple = host.split("host: ")[1]
252         if verbose:
253             print("detected default triple {} from pre-installed rustc".format(triple))
254         return triple
255     except Exception as e:
256         if verbose:
257             print("pre-installed rustc not detected: {}".format(e))
258             print("falling back to auto-detect")
259
260     required = sys.platform != 'win32'
261     ostype = require(["uname", "-s"], exit=required)
262     cputype = require(['uname', '-m'], exit=required)
263
264     # If we do not have `uname`, assume Windows.
265     if ostype is None or cputype is None:
266         return 'x86_64-pc-windows-msvc'
267
268     ostype = ostype.decode(default_encoding)
269     cputype = cputype.decode(default_encoding)
270
271     # The goal here is to come up with the same triple as LLVM would,
272     # at least for the subset of platforms we're willing to target.
273     ostype_mapper = {
274         'Darwin': 'apple-darwin',
275         'DragonFly': 'unknown-dragonfly',
276         'FreeBSD': 'unknown-freebsd',
277         'Haiku': 'unknown-haiku',
278         'NetBSD': 'unknown-netbsd',
279         'OpenBSD': 'unknown-openbsd'
280     }
281
282     # Consider the direct transformation first and then the special cases
283     if ostype in ostype_mapper:
284         ostype = ostype_mapper[ostype]
285     elif ostype == 'Linux':
286         os_from_sp = subprocess.check_output(
287             ['uname', '-o']).strip().decode(default_encoding)
288         if os_from_sp == 'Android':
289             ostype = 'linux-android'
290         else:
291             ostype = 'unknown-linux-gnu'
292     elif ostype == 'SunOS':
293         ostype = 'pc-solaris'
294         # On Solaris, uname -m will return a machine classification instead
295         # of a cpu type, so uname -p is recommended instead.  However, the
296         # output from that option is too generic for our purposes (it will
297         # always emit 'i386' on x86/amd64 systems).  As such, isainfo -k
298         # must be used instead.
299         cputype = require(['isainfo', '-k']).decode(default_encoding)
300         # sparc cpus have sun as a target vendor
301         if 'sparc' in cputype:
302             ostype = 'sun-solaris'
303     elif ostype.startswith('MINGW'):
304         # msys' `uname` does not print gcc configuration, but prints msys
305         # configuration. so we cannot believe `uname -m`:
306         # msys1 is always i686 and msys2 is always x86_64.
307         # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
308         # MINGW64 on x86_64.
309         ostype = 'pc-windows-gnu'
310         cputype = 'i686'
311         if os.environ.get('MSYSTEM') == 'MINGW64':
312             cputype = 'x86_64'
313     elif ostype.startswith('MSYS'):
314         ostype = 'pc-windows-gnu'
315     elif ostype.startswith('CYGWIN_NT'):
316         cputype = 'i686'
317         if ostype.endswith('WOW64'):
318             cputype = 'x86_64'
319         ostype = 'pc-windows-gnu'
320     elif sys.platform == 'win32':
321         # Some Windows platforms might have a `uname` command that returns a
322         # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
323         # these cases, fall back to using sys.platform.
324         return 'x86_64-pc-windows-msvc'
325     else:
326         err = "unknown OS type: {}".format(ostype)
327         sys.exit(err)
328
329     if cputype in ['powerpc', 'riscv'] and ostype == 'unknown-freebsd':
330         cputype = subprocess.check_output(
331               ['uname', '-p']).strip().decode(default_encoding)
332     cputype_mapper = {
333         'BePC': 'i686',
334         'aarch64': 'aarch64',
335         'amd64': 'x86_64',
336         'arm64': 'aarch64',
337         'i386': 'i686',
338         'i486': 'i686',
339         'i686': 'i686',
340         'i786': 'i686',
341         'm68k': 'm68k',
342         'powerpc': 'powerpc',
343         'powerpc64': 'powerpc64',
344         'powerpc64le': 'powerpc64le',
345         'ppc': 'powerpc',
346         'ppc64': 'powerpc64',
347         'ppc64le': 'powerpc64le',
348         'riscv64': 'riscv64gc',
349         's390x': 's390x',
350         'x64': 'x86_64',
351         'x86': 'i686',
352         'x86-64': 'x86_64',
353         'x86_64': 'x86_64'
354     }
355
356     # Consider the direct transformation first and then the special cases
357     if cputype in cputype_mapper:
358         cputype = cputype_mapper[cputype]
359     elif cputype in {'xscale', 'arm'}:
360         cputype = 'arm'
361         if ostype == 'linux-android':
362             ostype = 'linux-androideabi'
363         elif ostype == 'unknown-freebsd':
364             cputype = subprocess.check_output(
365                 ['uname', '-p']).strip().decode(default_encoding)
366             ostype = 'unknown-freebsd'
367     elif cputype == 'armv6l':
368         cputype = 'arm'
369         if ostype == 'linux-android':
370             ostype = 'linux-androideabi'
371         else:
372             ostype += 'eabihf'
373     elif cputype in {'armv7l', 'armv8l'}:
374         cputype = 'armv7'
375         if ostype == 'linux-android':
376             ostype = 'linux-androideabi'
377         else:
378             ostype += 'eabihf'
379     elif cputype == 'mips':
380         if sys.byteorder == 'big':
381             cputype = 'mips'
382         elif sys.byteorder == 'little':
383             cputype = 'mipsel'
384         else:
385             raise ValueError("unknown byteorder: {}".format(sys.byteorder))
386     elif cputype == 'mips64':
387         if sys.byteorder == 'big':
388             cputype = 'mips64'
389         elif sys.byteorder == 'little':
390             cputype = 'mips64el'
391         else:
392             raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
393         # only the n64 ABI is supported, indicate it
394         ostype += 'abi64'
395     elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
396         pass
397     else:
398         err = "unknown cpu type: {}".format(cputype)
399         sys.exit(err)
400
401     return "{}-{}".format(cputype, ostype)
402
403
404 @contextlib.contextmanager
405 def output(filepath):
406     tmp = filepath + '.tmp'
407     with open(tmp, 'w') as f:
408         yield f
409     try:
410         if os.path.exists(filepath):
411             os.remove(filepath)  # PermissionError/OSError on Win32 if in use
412     except OSError:
413         shutil.copy2(tmp, filepath)
414         os.remove(tmp)
415         return
416     os.rename(tmp, filepath)
417
418
419 class Stage0Toolchain:
420     def __init__(self, stage0_payload):
421         self.date = stage0_payload["date"]
422         self.version = stage0_payload["version"]
423
424     def channel(self):
425         return self.version + "-" + self.date
426
427
428 class RustBuild(object):
429     """Provide all the methods required to build Rust"""
430     def __init__(self):
431         self.checksums_sha256 = {}
432         self.stage0_compiler = None
433         self.stage0_rustfmt = None
434         self._download_url = ''
435         self.build = ''
436         self.build_dir = ''
437         self.clean = False
438         self.config_toml = ''
439         self.rust_root = ''
440         self.use_locked_deps = ''
441         self.use_vendored_sources = ''
442         self.verbose = False
443         self.git_version = None
444         self.nix_deps_dir = None
445
446     def download_toolchain(self):
447         """Fetch the build system for Rust, written in Rust
448
449         This method will build a cache directory, then it will fetch the
450         tarball which has the stage0 compiler used to then bootstrap the Rust
451         compiler itself.
452
453         Each downloaded tarball is extracted, after that, the script
454         will move all the content to the right place.
455         """
456         rustc_channel = self.stage0_compiler.version
457         bin_root = self.bin_root()
458
459         key = self.stage0_compiler.date
460         if self.rustc().startswith(bin_root) and \
461                 (not os.path.exists(self.rustc()) or
462                  self.program_out_of_date(self.rustc_stamp(), key)):
463             if os.path.exists(bin_root):
464                 shutil.rmtree(bin_root)
465             tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
466             filename = "rust-std-{}-{}{}".format(
467                 rustc_channel, self.build, tarball_suffix)
468             pattern = "rust-std-{}".format(self.build)
469             self._download_component_helper(filename, pattern, tarball_suffix)
470             filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
471                                               tarball_suffix)
472             self._download_component_helper(filename, "rustc", tarball_suffix)
473             filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
474                                             tarball_suffix)
475             self._download_component_helper(filename, "cargo", tarball_suffix)
476             self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
477
478             self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
479             self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
480             lib_dir = "{}/lib".format(bin_root)
481             for lib in os.listdir(lib_dir):
482                 if lib.endswith(".so"):
483                     self.fix_bin_or_dylib(os.path.join(lib_dir, lib))
484             with output(self.rustc_stamp()) as rust_stamp:
485                 rust_stamp.write(key)
486
487         if self.rustfmt() and self.rustfmt().startswith(bin_root) and (
488             not os.path.exists(self.rustfmt())
489             or self.program_out_of_date(
490                 self.rustfmt_stamp(),
491                 "" if self.stage0_rustfmt is None else self.stage0_rustfmt.channel()
492             )
493         ):
494             if self.stage0_rustfmt is not None:
495                 tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
496                 filename = "rustfmt-{}-{}{}".format(
497                     self.stage0_rustfmt.version, self.build, tarball_suffix,
498                 )
499                 self._download_component_helper(
500                     filename, "rustfmt-preview", tarball_suffix, key=self.stage0_rustfmt.date
501                 )
502                 self.fix_bin_or_dylib("{}/bin/rustfmt".format(bin_root))
503                 self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(bin_root))
504                 with output(self.rustfmt_stamp()) as rustfmt_stamp:
505                     rustfmt_stamp.write(self.stage0_rustfmt.channel())
506
507     def _download_component_helper(
508         self, filename, pattern, tarball_suffix, key=None
509     ):
510         if key is None:
511             key = self.stage0_compiler.date
512         cache_dst = os.path.join(self.build_dir, "cache")
513         rustc_cache = os.path.join(cache_dst, key)
514         if not os.path.exists(rustc_cache):
515             os.makedirs(rustc_cache)
516
517         base = self._download_url
518         url = "dist/{}".format(key)
519         tarball = os.path.join(rustc_cache, filename)
520         if not os.path.exists(tarball):
521             get(
522                 base,
523                 "{}/{}".format(url, filename),
524                 tarball,
525                 self.checksums_sha256,
526                 verbose=self.verbose,
527                 do_verify=True,
528             )
529         unpack(tarball, tarball_suffix, self.bin_root(), match=pattern, verbose=self.verbose)
530
531     def fix_bin_or_dylib(self, fname):
532         """Modifies the interpreter section of 'fname' to fix the dynamic linker,
533         or the RPATH section, to fix the dynamic library search path
534
535         This method is only required on NixOS and uses the PatchELF utility to
536         change the interpreter/RPATH of ELF executables.
537
538         Please see https://nixos.org/patchelf.html for more information
539         """
540         default_encoding = sys.getdefaultencoding()
541         try:
542             ostype = subprocess.check_output(
543                 ['uname', '-s']).strip().decode(default_encoding)
544         except subprocess.CalledProcessError:
545             return
546         except OSError as reason:
547             if getattr(reason, 'winerror', None) is not None:
548                 return
549             raise reason
550
551         if ostype != "Linux":
552             return
553
554         # If the user has asked binaries to be patched for Nix, then
555         # don't check for NixOS or `/lib`, just continue to the patching.
556         if self.get_toml('patch-binaries-for-nix', 'build') != 'true':
557             # Use `/etc/os-release` instead of `/etc/NIXOS`.
558             # The latter one does not exist on NixOS when using tmpfs as root.
559             try:
560                 with open("/etc/os-release", "r") as f:
561                     if not any(l.strip() in ["ID=nixos", "ID='nixos'", 'ID="nixos"'] for l in f):
562                         return
563             except FileNotFoundError:
564                 return
565             if os.path.exists("/lib"):
566                 return
567
568         # At this point we're pretty sure the user is running NixOS or
569         # using Nix
570         nix_os_msg = "info: you seem to be using Nix. Attempting to patch"
571         print(nix_os_msg, fname)
572
573         # Only build `.nix-deps` once.
574         nix_deps_dir = self.nix_deps_dir
575         if not nix_deps_dir:
576             # Run `nix-build` to "build" each dependency (which will likely reuse
577             # the existing `/nix/store` copy, or at most download a pre-built copy).
578             #
579             # Importantly, we create a gc-root called `.nix-deps` in the `build/`
580             # directory, but still reference the actual `/nix/store` path in the rpath
581             # as it makes it significantly more robust against changes to the location of
582             # the `.nix-deps` location.
583             #
584             # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
585             # zlib: Needed as a system dependency of `libLLVM-*.so`.
586             # patchelf: Needed for patching ELF binaries (see doc comment above).
587             nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
588             nix_expr = '''
589             with (import <nixpkgs> {});
590             symlinkJoin {
591               name = "rust-stage0-dependencies";
592               paths = [
593                 zlib
594                 patchelf
595                 stdenv.cc.bintools
596               ];
597             }
598             '''
599             try:
600                 subprocess.check_output([
601                     "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
602                 ])
603             except subprocess.CalledProcessError as reason:
604                 print("warning: failed to call nix-build:", reason)
605                 return
606             self.nix_deps_dir = nix_deps_dir
607
608         patchelf = "{}/bin/patchelf".format(nix_deps_dir)
609         rpath_entries = [
610             # Relative default, all binary and dynamic libraries we ship
611             # appear to have this (even when `../lib` is redundant).
612             "$ORIGIN/../lib",
613             os.path.join(os.path.realpath(nix_deps_dir), "lib")
614         ]
615         patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
616         if not fname.endswith(".so"):
617             # Finally, set the corret .interp for binaries
618             with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
619                 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
620
621         try:
622             subprocess.check_output([patchelf] + patchelf_args + [fname])
623         except subprocess.CalledProcessError as reason:
624             print("warning: failed to call patchelf:", reason)
625             return
626
627     def rustc_stamp(self):
628         """Return the path for .rustc-stamp at the given stage
629
630         >>> rb = RustBuild()
631         >>> rb.build_dir = "build"
632         >>> rb.rustc_stamp() == os.path.join("build", "stage0", ".rustc-stamp")
633         True
634         """
635         return os.path.join(self.bin_root(), '.rustc-stamp')
636
637     def rustfmt_stamp(self):
638         """Return the path for .rustfmt-stamp
639
640         >>> rb = RustBuild()
641         >>> rb.build_dir = "build"
642         >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
643         True
644         """
645         return os.path.join(self.bin_root(), '.rustfmt-stamp')
646
647     def program_out_of_date(self, stamp_path, key):
648         """Check if the given program stamp is out of date"""
649         if not os.path.exists(stamp_path) or self.clean:
650             return True
651         with open(stamp_path, 'r') as stamp:
652             return key != stamp.read()
653
654     def bin_root(self):
655         """Return the binary root directory for the given stage
656
657         >>> rb = RustBuild()
658         >>> rb.build_dir = "build"
659         >>> rb.bin_root() == os.path.join("build", "stage0")
660         True
661
662         When the 'build' property is given should be a nested directory:
663
664         >>> rb.build = "devel"
665         >>> rb.bin_root() == os.path.join("build", "devel", "stage0")
666         True
667         """
668         subdir = "stage0"
669         return os.path.join(self.build_dir, self.build, subdir)
670
671     def get_toml(self, key, section=None):
672         """Returns the value of the given key in config.toml, otherwise returns None
673
674         >>> rb = RustBuild()
675         >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
676         >>> rb.get_toml("key2")
677         'value2'
678
679         If the key does not exist, the result is None:
680
681         >>> rb.get_toml("key3") is None
682         True
683
684         Optionally also matches the section the key appears in
685
686         >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
687         >>> rb.get_toml('key', 'a')
688         'value1'
689         >>> rb.get_toml('key', 'b')
690         'value2'
691         >>> rb.get_toml('key', 'c') is None
692         True
693
694         >>> rb.config_toml = 'key1 = true'
695         >>> rb.get_toml("key1")
696         'true'
697         """
698
699         cur_section = None
700         for line in self.config_toml.splitlines():
701             section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
702             if section_match is not None:
703                 cur_section = section_match.group(1)
704
705             match = re.match(r'^{}\s*=(.*)$'.format(key), line)
706             if match is not None:
707                 value = match.group(1)
708                 if section is None or section == cur_section:
709                     return self.get_string(value) or value.strip()
710         return None
711
712     def cargo(self):
713         """Return config path for cargo"""
714         return self.program_config('cargo')
715
716     def rustc(self):
717         """Return config path for rustc"""
718         return self.program_config('rustc')
719
720     def rustfmt(self):
721         """Return config path for rustfmt"""
722         if self.stage0_rustfmt is None:
723             return None
724         return self.program_config('rustfmt')
725
726     def program_config(self, program):
727         """Return config path for the given program at the given stage
728
729         >>> rb = RustBuild()
730         >>> rb.config_toml = 'rustc = "rustc"\\n'
731         >>> rb.program_config('rustc')
732         'rustc'
733         >>> rb.config_toml = ''
734         >>> cargo_path = rb.program_config('cargo')
735         >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(),
736         ... "bin", "cargo")
737         True
738         """
739         config = self.get_toml(program)
740         if config:
741             return os.path.expanduser(config)
742         return os.path.join(self.bin_root(), "bin", "{}{}".format(
743             program, self.exe_suffix()))
744
745     @staticmethod
746     def get_string(line):
747         """Return the value between double quotes
748
749         >>> RustBuild.get_string('    "devel"   ')
750         'devel'
751         >>> RustBuild.get_string("    'devel'   ")
752         'devel'
753         >>> RustBuild.get_string('devel') is None
754         True
755         >>> RustBuild.get_string('    "devel   ')
756         ''
757         """
758         start = line.find('"')
759         if start != -1:
760             end = start + 1 + line[start + 1:].find('"')
761             return line[start + 1:end]
762         start = line.find('\'')
763         if start != -1:
764             end = start + 1 + line[start + 1:].find('\'')
765             return line[start + 1:end]
766         return None
767
768     @staticmethod
769     def exe_suffix():
770         """Return a suffix for executables"""
771         if sys.platform == 'win32':
772             return '.exe'
773         return ''
774
775     def bootstrap_binary(self):
776         """Return the path of the bootstrap binary
777
778         >>> rb = RustBuild()
779         >>> rb.build_dir = "build"
780         >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
781         ... "debug", "bootstrap")
782         True
783         """
784         return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
785
786     def build_bootstrap(self):
787         """Build bootstrap"""
788         print("Building rustbuild")
789         build_dir = os.path.join(self.build_dir, "bootstrap")
790         if self.clean and os.path.exists(build_dir):
791             shutil.rmtree(build_dir)
792         env = os.environ.copy()
793         # `CARGO_BUILD_TARGET` breaks bootstrap build.
794         # See also: <https://github.com/rust-lang/rust/issues/70208>.
795         if "CARGO_BUILD_TARGET" in env:
796             del env["CARGO_BUILD_TARGET"]
797         env["CARGO_TARGET_DIR"] = build_dir
798         env["RUSTC"] = self.rustc()
799         env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
800             (os.pathsep + env["LD_LIBRARY_PATH"]) \
801             if "LD_LIBRARY_PATH" in env else ""
802         env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
803             (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
804             if "DYLD_LIBRARY_PATH" in env else ""
805         env["LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
806             (os.pathsep + env["LIBRARY_PATH"]) \
807             if "LIBRARY_PATH" in env else ""
808
809         # preserve existing RUSTFLAGS
810         env.setdefault("RUSTFLAGS", "")
811         build_section = "target.{}".format(self.build)
812         target_features = []
813         if self.get_toml("crt-static", build_section) == "true":
814             target_features += ["+crt-static"]
815         elif self.get_toml("crt-static", build_section) == "false":
816             target_features += ["-crt-static"]
817         if target_features:
818             env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
819         target_linker = self.get_toml("linker", build_section)
820         if target_linker is not None:
821             env["RUSTFLAGS"] += " -C linker=" + target_linker
822         env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
823         env["RUSTFLAGS"] += " -Wsemicolon_in_expressions_from_macros"
824         if self.get_toml("deny-warnings", "rust") != "false":
825             env["RUSTFLAGS"] += " -Dwarnings"
826
827         env["PATH"] = os.path.join(self.bin_root(), "bin") + \
828             os.pathsep + env["PATH"]
829         if not os.path.isfile(self.cargo()):
830             raise Exception("no cargo executable found at `{}`".format(
831                 self.cargo()))
832         args = [self.cargo(), "build", "--manifest-path",
833                 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
834         for _ in range(0, self.verbose):
835             args.append("--verbose")
836         if self.use_locked_deps:
837             args.append("--locked")
838         if self.use_vendored_sources:
839             args.append("--frozen")
840         run(args, env=env, verbose=self.verbose)
841
842     def build_triple(self):
843         """Build triple as in LLVM
844
845         Note that `default_build_triple` is moderately expensive,
846         so use `self.build` where possible.
847         """
848         config = self.get_toml('build')
849         if config:
850             return config
851         return default_build_triple(self.verbose)
852
853     def check_submodule(self, module):
854         checked_out = subprocess.Popen(["git", "rev-parse", "HEAD"],
855                                         cwd=os.path.join(self.rust_root, module),
856                                         stdout=subprocess.PIPE)
857         return checked_out
858
859     def update_submodule(self, module, checked_out, recorded_submodules):
860         module_path = os.path.join(self.rust_root, module)
861
862         default_encoding = sys.getdefaultencoding()
863         checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
864         if recorded_submodules[module] == checked_out:
865             return
866
867         print("Updating submodule", module)
868
869         run(["git", "submodule", "-q", "sync", module],
870             cwd=self.rust_root, verbose=self.verbose)
871
872         update_args = ["git", "submodule", "update", "--init", "--recursive", "--depth=1"]
873         if self.git_version >= distutils.version.LooseVersion("2.11.0"):
874             update_args.append("--progress")
875         update_args.append(module)
876         try:
877             run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
878         except RuntimeError:
879             print("Failed updating submodule. This is probably due to uncommitted local changes.")
880             print('Either stash the changes by running "git stash" within the submodule\'s')
881             print('directory, reset them by running "git reset --hard", or commit them.')
882             print("To reset all submodules' changes run", end=" ")
883             print('"git submodule foreach --recursive git reset --hard".')
884             raise SystemExit(1)
885
886         run(["git", "reset", "-q", "--hard"],
887             cwd=module_path, verbose=self.verbose)
888         run(["git", "clean", "-qdfx"],
889             cwd=module_path, verbose=self.verbose)
890
891     def update_submodules(self):
892         """Update submodules"""
893         has_git = os.path.exists(os.path.join(self.rust_root, ".git"))
894         # This just arbitrarily checks for cargo, but any workspace member in
895         # a submodule would work.
896         has_submodules = os.path.exists(os.path.join(self.rust_root, "src/tools/cargo/Cargo.toml"))
897         if not has_git and not has_submodules:
898             print("This is not a git repository, and the requisite git submodules were not found.")
899             print("If you downloaded the source from https://github.com/rust-lang/rust/releases,")
900             print("those sources will not work. Instead, consider downloading from the source")
901             print("releases linked at")
902             print("https://forge.rust-lang.org/infra/other-installation-methods.html#source-code")
903             print("or clone the repository at https://github.com/rust-lang/rust/.")
904             raise SystemExit(1)
905         if not has_git or self.get_toml('submodules') == "false":
906             return
907
908         default_encoding = sys.getdefaultencoding()
909
910         # check the existence and version of 'git' command
911         git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
912         self.git_version = distutils.version.LooseVersion(git_version_str)
913
914         start_time = time()
915         print('Updating only changed submodules')
916         default_encoding = sys.getdefaultencoding()
917         # Only update submodules that are needed to build bootstrap.  These are needed because Cargo
918         # currently requires everything in a workspace to be "locally present" when starting a
919         # build, and will give a hard error if any Cargo.toml files are missing.
920         # FIXME: Is there a way to avoid cloning these eagerly? Bootstrap itself doesn't need to
921         #   share a workspace with any tools - maybe it could be excluded from the workspace?
922         #   That will still require cloning the submodules the second you check the standard
923         #   library, though...
924         # FIXME: Is there a way to avoid hard-coding the submodules required?
925         # WARNING: keep this in sync with the submodules hard-coded in bootstrap/lib.rs
926         submodules = [
927             "src/tools/rust-installer",
928             "src/tools/cargo",
929             "src/tools/rls",
930             "src/tools/miri",
931             "library/backtrace",
932             "library/stdarch"
933         ]
934         # If build.vendor is set in config.toml, we must update rust-analyzer also.
935         # Otherwise, the bootstrap will fail (#96456).
936         if self.use_vendored_sources:
937             submodules.append("src/tools/rust-analyzer")
938         filtered_submodules = []
939         submodules_names = []
940         for module in submodules:
941             check = self.check_submodule(module)
942             filtered_submodules.append((module, check))
943             submodules_names.append(module)
944         recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
945                                     cwd=self.rust_root, stdout=subprocess.PIPE)
946         recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
947         # { filename: hash }
948         recorded_submodules = {}
949         for data in recorded:
950             # [mode, kind, hash, filename]
951             data = data.split()
952             recorded_submodules[data[3]] = data[2]
953         for module in filtered_submodules:
954             self.update_submodule(module[0], module[1], recorded_submodules)
955         print("  Submodules updated in %.2f seconds" % (time() - start_time))
956
957     def set_dist_environment(self, url):
958         """Set download URL for normal environment"""
959         if 'RUSTUP_DIST_SERVER' in os.environ:
960             self._download_url = os.environ['RUSTUP_DIST_SERVER']
961         else:
962             self._download_url = url
963
964     def check_vendored_status(self):
965         """Check that vendoring is configured properly"""
966         vendor_dir = os.path.join(self.rust_root, 'vendor')
967         if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
968             if os.getuid() == 0:
969                 self.use_vendored_sources = True
970                 print('info: looks like you\'re trying to run this command as root')
971                 print('      and so in order to preserve your $HOME this will now')
972                 print('      use vendored sources by default.')
973                 if not os.path.exists(vendor_dir):
974                     print('error: vendoring required, but vendor directory does not exist.')
975                     print('       Run `cargo vendor` without sudo to initialize the '
976                           'vendor directory.')
977                     raise Exception("{} not found".format(vendor_dir))
978
979         if self.use_vendored_sources:
980             config = ("[source.crates-io]\n"
981                       "replace-with = 'vendored-sources'\n"
982                       "registry = 'https://example.com'\n"
983                       "\n"
984                       "[source.vendored-sources]\n"
985                       "directory = '{}/vendor'\n"
986                       .format(self.rust_root))
987             if not os.path.exists('.cargo'):
988                 os.makedirs('.cargo')
989                 with output('.cargo/config') as cargo_config:
990                     cargo_config.write(config)
991             else:
992                 print('info: using vendored source, but .cargo/config is already present.')
993                 print('      Reusing the current configuration file. But you may want to '
994                       'configure vendoring like this:')
995                 print(config)
996         else:
997             if os.path.exists('.cargo'):
998                 shutil.rmtree('.cargo')
999
1000     def ensure_vendored(self):
1001         """Ensure that the vendored sources are available if needed"""
1002         vendor_dir = os.path.join(self.rust_root, 'vendor')
1003         # Note that this does not handle updating the vendored dependencies if
1004         # the rust git repository is updated. Normal development usually does
1005         # not use vendoring, so hopefully this isn't too much of a problem.
1006         if self.use_vendored_sources and not os.path.exists(vendor_dir):
1007             run([
1008                 self.cargo(),
1009                 "vendor",
1010                 "--sync=./src/tools/rust-analyzer/Cargo.toml",
1011                 "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
1012             ], verbose=self.verbose, cwd=self.rust_root)
1013
1014
1015 def bootstrap(help_triggered):
1016     """Configure, fetch, build and run the initial bootstrap"""
1017
1018     # If the user is asking for help, let them know that the whole download-and-build
1019     # process has to happen before anything is printed out.
1020     if help_triggered:
1021         print("info: Downloading and building bootstrap before processing --help")
1022         print("      command. See src/bootstrap/README.md for help with common")
1023         print("      commands.")
1024
1025     parser = argparse.ArgumentParser(description='Build rust')
1026     parser.add_argument('--config')
1027     parser.add_argument('--build')
1028     parser.add_argument('--clean', action='store_true')
1029     parser.add_argument('-v', '--verbose', action='count', default=0)
1030
1031     args = [a for a in sys.argv if a != '-h' and a != '--help']
1032     args, _ = parser.parse_known_args(args)
1033
1034     # Configure initial bootstrap
1035     build = RustBuild()
1036     build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1037     build.verbose = args.verbose
1038     build.clean = args.clean
1039
1040     # Read from `--config`, then `RUST_BOOTSTRAP_CONFIG`, then `./config.toml`,
1041     # then `config.toml` in the root directory.
1042     toml_path = args.config or os.getenv('RUST_BOOTSTRAP_CONFIG')
1043     using_default_path = toml_path is None
1044     if using_default_path:
1045         toml_path = 'config.toml'
1046         if not os.path.exists(toml_path):
1047             toml_path = os.path.join(build.rust_root, toml_path)
1048
1049     # Give a hard error if `--config` or `RUST_BOOTSTRAP_CONFIG` are set to a missing path,
1050     # but not if `config.toml` hasn't been created.
1051     if not using_default_path or os.path.exists(toml_path):
1052         with open(toml_path) as config:
1053             build.config_toml = config.read()
1054
1055     profile = build.get_toml('profile')
1056     if profile is not None:
1057         include_file = 'config.{}.toml'.format(profile)
1058         include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
1059         include_path = os.path.join(include_dir, include_file)
1060         # HACK: This works because `build.get_toml()` returns the first match it finds for a
1061         # specific key, so appending our defaults at the end allows the user to override them
1062         with open(include_path) as included_toml:
1063             build.config_toml += os.linesep + included_toml.read()
1064
1065     config_verbose = build.get_toml('verbose', 'build')
1066     if config_verbose is not None:
1067         build.verbose = max(build.verbose, int(config_verbose))
1068
1069     build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
1070
1071     build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
1072
1073     build.check_vendored_status()
1074
1075     build_dir = build.get_toml('build-dir', 'build') or 'build'
1076     build.build_dir = os.path.abspath(build_dir)
1077
1078     with open(os.path.join(build.rust_root, "src", "stage0.json")) as f:
1079         data = json.load(f)
1080     build.checksums_sha256 = data["checksums_sha256"]
1081     build.stage0_compiler = Stage0Toolchain(data["compiler"])
1082     if data.get("rustfmt") is not None:
1083         build.stage0_rustfmt = Stage0Toolchain(data["rustfmt"])
1084
1085     build.set_dist_environment(data["dist_server"])
1086
1087     build.build = args.build or build.build_triple()
1088
1089     # Acquire the lock before doing any build actions
1090     # The lock is released when `lock` is dropped
1091     if not os.path.exists(build.build_dir):
1092         os.makedirs(build.build_dir)
1093     lock = acquire_lock(build.build_dir)
1094     build.update_submodules()
1095
1096     # Fetch/build the bootstrap
1097     build.download_toolchain()
1098     sys.stdout.flush()
1099     build.ensure_vendored()
1100     build.build_bootstrap()
1101     sys.stdout.flush()
1102
1103     # Run the bootstrap
1104     args = [build.bootstrap_binary()]
1105     args.extend(sys.argv[1:])
1106     env = os.environ.copy()
1107     env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1108     env["BOOTSTRAP_PYTHON"] = sys.executable
1109     run(args, env=env, verbose=build.verbose, is_bootstrap=True)
1110
1111
1112 def main():
1113     """Entry point for the bootstrap process"""
1114     start_time = time()
1115
1116     # x.py help <cmd> ...
1117     if len(sys.argv) > 1 and sys.argv[1] == 'help':
1118         sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
1119
1120     help_triggered = (
1121         '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
1122     try:
1123         bootstrap(help_triggered)
1124         if not help_triggered:
1125             print("Build completed successfully in {}".format(
1126                 format_build_time(time() - start_time)))
1127     except (SystemExit, KeyboardInterrupt) as error:
1128         if hasattr(error, 'code') and isinstance(error.code, int):
1129             exit_code = error.code
1130         else:
1131             exit_code = 1
1132             print(error)
1133         if not help_triggered:
1134             print("Build completed unsuccessfully in {}".format(
1135                 format_build_time(time() - start_time)))
1136         sys.exit(exit_code)
1137
1138
1139 if __name__ == '__main__':
1140     main()