]> git.lizzy.rs Git - rust.git/blob - src/bootstrap/bootstrap.py
Respect --set=target.platform during build
[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 def support_xz():
19     try:
20         with tempfile.NamedTemporaryFile(delete=False) as temp_file:
21             temp_path = temp_file.name
22         with tarfile.open(temp_path, "w:xz"):
23             pass
24         return True
25     except tarfile.CompressionError:
26         return False
27
28 def get(base, url, path, checksums, verbose=False):
29     with tempfile.NamedTemporaryFile(delete=False) as temp_file:
30         temp_path = temp_file.name
31
32     try:
33         if url not in checksums:
34             raise RuntimeError(("src/stage0.json doesn't contain a checksum for {}. "
35                                 "Pre-built artifacts might not be available for this "
36                                 "target at this time, see https://doc.rust-lang.org/nightly"
37                                 "/rustc/platform-support.html for more information.")
38                                 .format(url))
39         sha256 = checksums[url]
40         if os.path.exists(path):
41             if verify(path, sha256, False):
42                 if verbose:
43                     print("using already-download file", path)
44                 return
45             else:
46                 if verbose:
47                     print("ignoring already-download file",
48                         path, "due to failed verification")
49                 os.unlink(path)
50         download(temp_path, "{}/{}".format(base, url), True, verbose)
51         if not verify(temp_path, sha256, verbose):
52             raise RuntimeError("failed verification")
53         if verbose:
54             print("moving {} to {}".format(temp_path, path))
55         shutil.move(temp_path, path)
56     finally:
57         if os.path.isfile(temp_path):
58             if verbose:
59                 print("removing", temp_path)
60             os.unlink(temp_path)
61
62
63 def download(path, url, probably_big, verbose):
64     for _ in range(0, 4):
65         try:
66             _download(path, url, probably_big, verbose, True)
67             return
68         except RuntimeError:
69             print("\nspurious failure, trying again")
70     _download(path, url, probably_big, verbose, False)
71
72
73 def _download(path, url, probably_big, verbose, exception):
74     # Try to use curl (potentially available on win32
75     #    https://devblogs.microsoft.com/commandline/tar-and-curl-come-to-windows/)
76     # If an error occurs:
77     #  - If we are on win32 fallback to powershell
78     #  - Otherwise raise the error if appropriate
79     if probably_big or verbose:
80         print("downloading {}".format(url))
81
82     platform_is_win32 = sys.platform == 'win32'
83     try:
84         if probably_big or verbose:
85             option = "-#"
86         else:
87             option = "-s"
88         # If curl is not present on Win32, we should not sys.exit
89         #   but raise `CalledProcessError` or `OSError` instead
90         require(["curl", "--version"], exception=platform_is_win32)
91         run(["curl", option,
92              "-L", # Follow redirect.
93              "-y", "30", "-Y", "10",    # timeout if speed is < 10 bytes/sec for > 30 seconds
94              "--connect-timeout", "30",  # timeout if cannot connect within 30 seconds
95              "--retry", "3", "-Sf", "-o", path, url],
96             verbose=verbose,
97             exception=True, # Will raise RuntimeError on failure
98         )
99     except (subprocess.CalledProcessError, OSError, RuntimeError):
100         # see http://serverfault.com/questions/301128/how-to-download
101         if platform_is_win32:
102             run(["PowerShell.exe", "/nologo", "-Command",
103                  "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
104                  "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
105                 verbose=verbose,
106                 exception=exception)
107         # Check if the RuntimeError raised by run(curl) should be silenced
108         elif verbose or exception:
109             raise
110
111
112 def verify(path, expected, verbose):
113     """Check if the sha256 sum of the given path is valid"""
114     if verbose:
115         print("verifying", path)
116     with open(path, "rb") as source:
117         found = hashlib.sha256(source.read()).hexdigest()
118     verified = found == expected
119     if not verified:
120         print("invalid checksum:\n"
121               "    found:    {}\n"
122               "    expected: {}".format(found, expected))
123     return verified
124
125
126 def unpack(tarball, tarball_suffix, dst, verbose=False, match=None):
127     """Unpack the given tarball file"""
128     print("extracting", tarball)
129     fname = os.path.basename(tarball).replace(tarball_suffix, "")
130     with contextlib.closing(tarfile.open(tarball)) as tar:
131         for member in tar.getnames():
132             if "/" not in member:
133                 continue
134             name = member.replace(fname + "/", "", 1)
135             if match is not None and not name.startswith(match):
136                 continue
137             name = name[len(match) + 1:]
138
139             dst_path = os.path.join(dst, name)
140             if verbose:
141                 print("  extracting", member)
142             tar.extract(member, dst)
143             src_path = os.path.join(dst, member)
144             if os.path.isdir(src_path) and os.path.exists(dst_path):
145                 continue
146             shutil.move(src_path, dst_path)
147     shutil.rmtree(os.path.join(dst, fname))
148
149
150 def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
151     """Run a child program in a new process"""
152     if verbose:
153         print("running: " + ' '.join(args))
154     sys.stdout.flush()
155     # Ensure that the .exe is used on Windows just in case a Linux ELF has been
156     # compiled in the same directory.
157     if os.name == 'nt' and not args[0].endswith('.exe'):
158         args[0] += '.exe'
159     # Use Popen here instead of call() as it apparently allows powershell on
160     # Windows to not lock up waiting for input presumably.
161     ret = subprocess.Popen(args, **kwargs)
162     code = ret.wait()
163     if code != 0:
164         err = "failed to run: " + ' '.join(args)
165         if verbose or exception:
166             raise RuntimeError(err)
167         # For most failures, we definitely do want to print this error, or the user will have no
168         # idea what went wrong. But when we've successfully built bootstrap and it failed, it will
169         # have already printed an error above, so there's no need to print the exact command we're
170         # running.
171         if is_bootstrap:
172             sys.exit(1)
173         else:
174             sys.exit(err)
175
176
177 def require(cmd, exit=True, exception=False):
178     '''Run a command, returning its output.
179     On error,
180         If `exception` is `True`, raise the error
181         Otherwise If `exit` is `True`, exit the process
182         Else return None.'''
183     try:
184         return subprocess.check_output(cmd).strip()
185     except (subprocess.CalledProcessError, OSError) as exc:
186         if exception:
187             raise
188         elif exit:
189             print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
190             print("Please make sure it's installed and in the path.")
191             sys.exit(1)
192         return None
193
194
195
196 def format_build_time(duration):
197     """Return a nicer format for build time
198
199     >>> format_build_time('300')
200     '0:05:00'
201     """
202     return str(datetime.timedelta(seconds=int(duration)))
203
204
205 def default_build_triple(verbose):
206     """Build triple as in LLVM"""
207     # If the user already has a host build triple with an existing `rustc`
208     # install, use their preference. This fixes most issues with Windows builds
209     # being detected as GNU instead of MSVC.
210     default_encoding = sys.getdefaultencoding()
211     try:
212         version = subprocess.check_output(["rustc", "--version", "--verbose"],
213                 stderr=subprocess.DEVNULL)
214         version = version.decode(default_encoding)
215         host = next(x for x in version.split('\n') if x.startswith("host: "))
216         triple = host.split("host: ")[1]
217         if verbose:
218             print("detected default triple {} from pre-installed rustc".format(triple))
219         return triple
220     except Exception as e:
221         if verbose:
222             print("pre-installed rustc not detected: {}".format(e))
223             print("falling back to auto-detect")
224
225     required = sys.platform != 'win32'
226     ostype = require(["uname", "-s"], exit=required)
227     cputype = require(['uname', '-m'], exit=required)
228
229     # If we do not have `uname`, assume Windows.
230     if ostype is None or cputype is None:
231         return 'x86_64-pc-windows-msvc'
232
233     ostype = ostype.decode(default_encoding)
234     cputype = cputype.decode(default_encoding)
235
236     # The goal here is to come up with the same triple as LLVM would,
237     # at least for the subset of platforms we're willing to target.
238     ostype_mapper = {
239         'Darwin': 'apple-darwin',
240         'DragonFly': 'unknown-dragonfly',
241         'FreeBSD': 'unknown-freebsd',
242         'Haiku': 'unknown-haiku',
243         'NetBSD': 'unknown-netbsd',
244         'OpenBSD': 'unknown-openbsd'
245     }
246
247     # Consider the direct transformation first and then the special cases
248     if ostype in ostype_mapper:
249         ostype = ostype_mapper[ostype]
250     elif ostype == 'Linux':
251         os_from_sp = subprocess.check_output(
252             ['uname', '-o']).strip().decode(default_encoding)
253         if os_from_sp == 'Android':
254             ostype = 'linux-android'
255         else:
256             ostype = 'unknown-linux-gnu'
257     elif ostype == 'SunOS':
258         ostype = 'pc-solaris'
259         # On Solaris, uname -m will return a machine classification instead
260         # of a cpu type, so uname -p is recommended instead.  However, the
261         # output from that option is too generic for our purposes (it will
262         # always emit 'i386' on x86/amd64 systems).  As such, isainfo -k
263         # must be used instead.
264         cputype = require(['isainfo', '-k']).decode(default_encoding)
265         # sparc cpus have sun as a target vendor
266         if 'sparc' in cputype:
267             ostype = 'sun-solaris'
268     elif ostype.startswith('MINGW'):
269         # msys' `uname` does not print gcc configuration, but prints msys
270         # configuration. so we cannot believe `uname -m`:
271         # msys1 is always i686 and msys2 is always x86_64.
272         # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
273         # MINGW64 on x86_64.
274         ostype = 'pc-windows-gnu'
275         cputype = 'i686'
276         if os.environ.get('MSYSTEM') == 'MINGW64':
277             cputype = 'x86_64'
278     elif ostype.startswith('MSYS'):
279         ostype = 'pc-windows-gnu'
280     elif ostype.startswith('CYGWIN_NT'):
281         cputype = 'i686'
282         if ostype.endswith('WOW64'):
283             cputype = 'x86_64'
284         ostype = 'pc-windows-gnu'
285     elif sys.platform == 'win32':
286         # Some Windows platforms might have a `uname` command that returns a
287         # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
288         # these cases, fall back to using sys.platform.
289         return 'x86_64-pc-windows-msvc'
290     else:
291         err = "unknown OS type: {}".format(ostype)
292         sys.exit(err)
293
294     if cputype in ['powerpc', 'riscv'] and ostype == 'unknown-freebsd':
295         cputype = subprocess.check_output(
296               ['uname', '-p']).strip().decode(default_encoding)
297     cputype_mapper = {
298         'BePC': 'i686',
299         'aarch64': 'aarch64',
300         'amd64': 'x86_64',
301         'arm64': 'aarch64',
302         'i386': 'i686',
303         'i486': 'i686',
304         'i686': 'i686',
305         'i786': 'i686',
306         'm68k': 'm68k',
307         'powerpc': 'powerpc',
308         'powerpc64': 'powerpc64',
309         'powerpc64le': 'powerpc64le',
310         'ppc': 'powerpc',
311         'ppc64': 'powerpc64',
312         'ppc64le': 'powerpc64le',
313         'riscv64': 'riscv64gc',
314         's390x': 's390x',
315         'x64': 'x86_64',
316         'x86': 'i686',
317         'x86-64': 'x86_64',
318         'x86_64': 'x86_64'
319     }
320
321     # Consider the direct transformation first and then the special cases
322     if cputype in cputype_mapper:
323         cputype = cputype_mapper[cputype]
324     elif cputype in {'xscale', 'arm'}:
325         cputype = 'arm'
326         if ostype == 'linux-android':
327             ostype = 'linux-androideabi'
328         elif ostype == 'unknown-freebsd':
329             cputype = subprocess.check_output(
330                 ['uname', '-p']).strip().decode(default_encoding)
331             ostype = 'unknown-freebsd'
332     elif cputype == 'armv6l':
333         cputype = 'arm'
334         if ostype == 'linux-android':
335             ostype = 'linux-androideabi'
336         else:
337             ostype += 'eabihf'
338     elif cputype in {'armv7l', 'armv8l'}:
339         cputype = 'armv7'
340         if ostype == 'linux-android':
341             ostype = 'linux-androideabi'
342         else:
343             ostype += 'eabihf'
344     elif cputype == 'mips':
345         if sys.byteorder == 'big':
346             cputype = 'mips'
347         elif sys.byteorder == 'little':
348             cputype = 'mipsel'
349         else:
350             raise ValueError("unknown byteorder: {}".format(sys.byteorder))
351     elif cputype == 'mips64':
352         if sys.byteorder == 'big':
353             cputype = 'mips64'
354         elif sys.byteorder == 'little':
355             cputype = 'mips64el'
356         else:
357             raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
358         # only the n64 ABI is supported, indicate it
359         ostype += 'abi64'
360     elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
361         pass
362     else:
363         err = "unknown cpu type: {}".format(cputype)
364         sys.exit(err)
365
366     return "{}-{}".format(cputype, ostype)
367
368
369 @contextlib.contextmanager
370 def output(filepath):
371     tmp = filepath + '.tmp'
372     with open(tmp, 'w') as f:
373         yield f
374     try:
375         if os.path.exists(filepath):
376             os.remove(filepath)  # PermissionError/OSError on Win32 if in use
377     except OSError:
378         shutil.copy2(tmp, filepath)
379         os.remove(tmp)
380         return
381     os.rename(tmp, filepath)
382
383
384 class Stage0Toolchain:
385     def __init__(self, stage0_payload):
386         self.date = stage0_payload["date"]
387         self.version = stage0_payload["version"]
388
389     def channel(self):
390         return self.version + "-" + self.date
391
392
393 class RustBuild(object):
394     """Provide all the methods required to build Rust"""
395     def __init__(self):
396         self.checksums_sha256 = {}
397         self.stage0_compiler = None
398         self._download_url = ''
399         self.build = ''
400         self.build_dir = ''
401         self.clean = False
402         self.config_toml = ''
403         self.rust_root = ''
404         self.use_locked_deps = ''
405         self.use_vendored_sources = ''
406         self.verbose = False
407         self.git_version = None
408         self.nix_deps_dir = None
409
410     def download_toolchain(self):
411         """Fetch the build system for Rust, written in Rust
412
413         This method will build a cache directory, then it will fetch the
414         tarball which has the stage0 compiler used to then bootstrap the Rust
415         compiler itself.
416
417         Each downloaded tarball is extracted, after that, the script
418         will move all the content to the right place.
419         """
420         rustc_channel = self.stage0_compiler.version
421         bin_root = self.bin_root()
422
423         key = self.stage0_compiler.date
424         if self.rustc().startswith(bin_root) and \
425                 (not os.path.exists(self.rustc()) or
426                  self.program_out_of_date(self.rustc_stamp(), key)):
427             if os.path.exists(bin_root):
428                 shutil.rmtree(bin_root)
429             tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
430             filename = "rust-std-{}-{}{}".format(
431                 rustc_channel, self.build, tarball_suffix)
432             pattern = "rust-std-{}".format(self.build)
433             self._download_component_helper(filename, pattern, tarball_suffix)
434             filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
435                                               tarball_suffix)
436             self._download_component_helper(filename, "rustc", tarball_suffix)
437             filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
438                                             tarball_suffix)
439             self._download_component_helper(filename, "cargo", tarball_suffix)
440             self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
441
442             self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
443             self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
444             self.fix_bin_or_dylib("{}/libexec/rust-analyzer-proc-macro-srv".format(bin_root))
445             lib_dir = "{}/lib".format(bin_root)
446             for lib in os.listdir(lib_dir):
447                 if lib.endswith(".so"):
448                     self.fix_bin_or_dylib(os.path.join(lib_dir, lib))
449             with output(self.rustc_stamp()) as rust_stamp:
450                 rust_stamp.write(key)
451
452     def _download_component_helper(
453         self, filename, pattern, tarball_suffix,
454     ):
455         key = self.stage0_compiler.date
456         cache_dst = os.path.join(self.build_dir, "cache")
457         rustc_cache = os.path.join(cache_dst, key)
458         if not os.path.exists(rustc_cache):
459             os.makedirs(rustc_cache)
460
461         base = self._download_url
462         url = "dist/{}".format(key)
463         tarball = os.path.join(rustc_cache, filename)
464         if not os.path.exists(tarball):
465             get(
466                 base,
467                 "{}/{}".format(url, filename),
468                 tarball,
469                 self.checksums_sha256,
470                 verbose=self.verbose,
471             )
472         unpack(tarball, tarball_suffix, self.bin_root(), match=pattern, verbose=self.verbose)
473
474     def fix_bin_or_dylib(self, fname):
475         """Modifies the interpreter section of 'fname' to fix the dynamic linker,
476         or the RPATH section, to fix the dynamic library search path
477
478         This method is only required on NixOS and uses the PatchELF utility to
479         change the interpreter/RPATH of ELF executables.
480
481         Please see https://nixos.org/patchelf.html for more information
482         """
483         default_encoding = sys.getdefaultencoding()
484         try:
485             ostype = subprocess.check_output(
486                 ['uname', '-s']).strip().decode(default_encoding)
487         except subprocess.CalledProcessError:
488             return
489         except OSError as reason:
490             if getattr(reason, 'winerror', None) is not None:
491                 return
492             raise reason
493
494         if ostype != "Linux":
495             return
496
497         # If the user has asked binaries to be patched for Nix, then
498         # don't check for NixOS or `/lib`, just continue to the patching.
499         if self.get_toml('patch-binaries-for-nix', 'build') != 'true':
500             # Use `/etc/os-release` instead of `/etc/NIXOS`.
501             # The latter one does not exist on NixOS when using tmpfs as root.
502             try:
503                 with open("/etc/os-release", "r") as f:
504                     if not any(l.strip() in ["ID=nixos", "ID='nixos'", 'ID="nixos"'] for l in f):
505                         return
506             except FileNotFoundError:
507                 return
508             if os.path.exists("/lib"):
509                 return
510
511         # At this point we're pretty sure the user is running NixOS or
512         # using Nix
513         nix_os_msg = "info: you seem to be using Nix. Attempting to patch"
514         print(nix_os_msg, fname)
515
516         # Only build `.nix-deps` once.
517         nix_deps_dir = self.nix_deps_dir
518         if not nix_deps_dir:
519             # Run `nix-build` to "build" each dependency (which will likely reuse
520             # the existing `/nix/store` copy, or at most download a pre-built copy).
521             #
522             # Importantly, we create a gc-root called `.nix-deps` in the `build/`
523             # directory, but still reference the actual `/nix/store` path in the rpath
524             # as it makes it significantly more robust against changes to the location of
525             # the `.nix-deps` location.
526             #
527             # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
528             # zlib: Needed as a system dependency of `libLLVM-*.so`.
529             # patchelf: Needed for patching ELF binaries (see doc comment above).
530             nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
531             nix_expr = '''
532             with (import <nixpkgs> {});
533             symlinkJoin {
534               name = "rust-stage0-dependencies";
535               paths = [
536                 zlib
537                 patchelf
538                 stdenv.cc.bintools
539               ];
540             }
541             '''
542             try:
543                 subprocess.check_output([
544                     "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
545                 ])
546             except subprocess.CalledProcessError as reason:
547                 print("warning: failed to call nix-build:", reason)
548                 return
549             self.nix_deps_dir = nix_deps_dir
550
551         patchelf = "{}/bin/patchelf".format(nix_deps_dir)
552         rpath_entries = [
553             # Relative default, all binary and dynamic libraries we ship
554             # appear to have this (even when `../lib` is redundant).
555             "$ORIGIN/../lib",
556             os.path.join(os.path.realpath(nix_deps_dir), "lib")
557         ]
558         patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
559         if not fname.endswith(".so"):
560             # Finally, set the corret .interp for binaries
561             with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
562                 patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
563
564         try:
565             subprocess.check_output([patchelf] + patchelf_args + [fname])
566         except subprocess.CalledProcessError as reason:
567             print("warning: failed to call patchelf:", reason)
568             return
569
570     def rustc_stamp(self):
571         """Return the path for .rustc-stamp at the given stage
572
573         >>> rb = RustBuild()
574         >>> rb.build_dir = "build"
575         >>> rb.rustc_stamp() == os.path.join("build", "stage0", ".rustc-stamp")
576         True
577         """
578         return os.path.join(self.bin_root(), '.rustc-stamp')
579
580     def program_out_of_date(self, stamp_path, key):
581         """Check if the given program stamp is out of date"""
582         if not os.path.exists(stamp_path) or self.clean:
583             return True
584         with open(stamp_path, 'r') as stamp:
585             return key != stamp.read()
586
587     def bin_root(self):
588         """Return the binary root directory for the given stage
589
590         >>> rb = RustBuild()
591         >>> rb.build_dir = "build"
592         >>> rb.bin_root() == os.path.join("build", "stage0")
593         True
594
595         When the 'build' property is given should be a nested directory:
596
597         >>> rb.build = "devel"
598         >>> rb.bin_root() == os.path.join("build", "devel", "stage0")
599         True
600         """
601         subdir = "stage0"
602         return os.path.join(self.build_dir, self.build, subdir)
603
604     def get_toml(self, key, section=None):
605         """Returns the value of the given key in config.toml, otherwise returns None
606
607         >>> rb = RustBuild()
608         >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
609         >>> rb.get_toml("key2")
610         'value2'
611
612         If the key does not exist, the result is None:
613
614         >>> rb.get_toml("key3") is None
615         True
616
617         Optionally also matches the section the key appears in
618
619         >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
620         >>> rb.get_toml('key', 'a')
621         'value1'
622         >>> rb.get_toml('key', 'b')
623         'value2'
624         >>> rb.get_toml('key', 'c') is None
625         True
626
627         >>> rb.config_toml = 'key1 = true'
628         >>> rb.get_toml("key1")
629         'true'
630         """
631
632         cur_section = None
633         for line in self.config_toml.splitlines():
634             section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
635             if section_match is not None:
636                 cur_section = section_match.group(1)
637
638             match = re.match(r'^{}\s*=(.*)$'.format(key), line)
639             if match is not None:
640                 value = match.group(1)
641                 if section is None or section == cur_section:
642                     return self.get_string(value) or value.strip()
643         return None
644
645     def cargo(self):
646         """Return config path for cargo"""
647         return self.program_config('cargo')
648
649     def rustc(self):
650         """Return config path for rustc"""
651         return self.program_config('rustc')
652
653     def program_config(self, program):
654         """Return config path for the given program at the given stage
655
656         >>> rb = RustBuild()
657         >>> rb.config_toml = 'rustc = "rustc"\\n'
658         >>> rb.program_config('rustc')
659         'rustc'
660         >>> rb.config_toml = ''
661         >>> cargo_path = rb.program_config('cargo')
662         >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(),
663         ... "bin", "cargo")
664         True
665         """
666         config = self.get_toml(program)
667         if config:
668             return os.path.expanduser(config)
669         return os.path.join(self.bin_root(), "bin", "{}{}".format(
670             program, self.exe_suffix()))
671
672     @staticmethod
673     def get_string(line):
674         """Return the value between double quotes
675
676         >>> RustBuild.get_string('    "devel"   ')
677         'devel'
678         >>> RustBuild.get_string("    'devel'   ")
679         'devel'
680         >>> RustBuild.get_string('devel') is None
681         True
682         >>> RustBuild.get_string('    "devel   ')
683         ''
684         """
685         start = line.find('"')
686         if start != -1:
687             end = start + 1 + line[start + 1:].find('"')
688             return line[start + 1:end]
689         start = line.find('\'')
690         if start != -1:
691             end = start + 1 + line[start + 1:].find('\'')
692             return line[start + 1:end]
693         return None
694
695     @staticmethod
696     def exe_suffix():
697         """Return a suffix for executables"""
698         if sys.platform == 'win32':
699             return '.exe'
700         return ''
701
702     def bootstrap_binary(self):
703         """Return the path of the bootstrap binary
704
705         >>> rb = RustBuild()
706         >>> rb.build_dir = "build"
707         >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
708         ... "debug", "bootstrap")
709         True
710         """
711         return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
712
713     def build_bootstrap(self, color):
714         """Build bootstrap"""
715         print("Building rustbuild")
716         build_dir = os.path.join(self.build_dir, "bootstrap")
717         if self.clean and os.path.exists(build_dir):
718             shutil.rmtree(build_dir)
719         env = os.environ.copy()
720         # `CARGO_BUILD_TARGET` breaks bootstrap build.
721         # See also: <https://github.com/rust-lang/rust/issues/70208>.
722         if "CARGO_BUILD_TARGET" in env:
723             del env["CARGO_BUILD_TARGET"]
724         env["CARGO_TARGET_DIR"] = build_dir
725         env["RUSTC"] = self.rustc()
726         env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
727             (os.pathsep + env["LD_LIBRARY_PATH"]) \
728             if "LD_LIBRARY_PATH" in env else ""
729         env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
730             (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
731             if "DYLD_LIBRARY_PATH" in env else ""
732         env["LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
733             (os.pathsep + env["LIBRARY_PATH"]) \
734             if "LIBRARY_PATH" in env else ""
735
736         # Export Stage0 snapshot compiler related env variables
737         build_section = "target.{}".format(self.build)
738         host_triple_sanitized = self.build.replace("-", "_")
739         var_data = {
740             "CC": "cc", "CXX": "cxx", "LD": "linker", "AR": "ar", "RANLIB": "ranlib"
741         }
742         for var_name, toml_key in var_data.items():
743             toml_val = self.get_toml(toml_key, build_section)
744             if toml_val != None:
745                 env["{}_{}".format(var_name, host_triple_sanitized)] = toml_val
746
747         # preserve existing RUSTFLAGS
748         env.setdefault("RUSTFLAGS", "")
749         target_features = []
750         if self.get_toml("crt-static", build_section) == "true":
751             target_features += ["+crt-static"]
752         elif self.get_toml("crt-static", build_section) == "false":
753             target_features += ["-crt-static"]
754         if target_features:
755             env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
756         target_linker = self.get_toml("linker", build_section)
757         if target_linker is not None:
758             env["RUSTFLAGS"] += " -C linker=" + target_linker
759         env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
760         env["RUSTFLAGS"] += " -Wsemicolon_in_expressions_from_macros"
761         if self.get_toml("deny-warnings", "rust") != "false":
762             env["RUSTFLAGS"] += " -Dwarnings"
763
764         env["PATH"] = os.path.join(self.bin_root(), "bin") + \
765             os.pathsep + env["PATH"]
766         if not os.path.isfile(self.cargo()):
767             raise Exception("no cargo executable found at `{}`".format(
768                 self.cargo()))
769         args = [self.cargo(), "build", "--manifest-path",
770                 os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
771         for _ in range(0, self.verbose):
772             args.append("--verbose")
773         if self.use_locked_deps:
774             args.append("--locked")
775         if self.use_vendored_sources:
776             args.append("--frozen")
777         if self.get_toml("metrics", "build"):
778             args.append("--features")
779             args.append("build-metrics")
780         if color == "always":
781             args.append("--color=always")
782         elif color == "never":
783             args.append("--color=never")
784
785         # Run this from the source directory so cargo finds .cargo/config
786         run(args, env=env, verbose=self.verbose, cwd=self.rust_root)
787
788     def build_triple(self):
789         """Build triple as in LLVM
790
791         Note that `default_build_triple` is moderately expensive,
792         so use `self.build` where possible.
793         """
794         config = self.get_toml('build')
795         if config:
796             return config
797         return default_build_triple(self.verbose)
798
799     def set_dist_environment(self, url):
800         """Set download URL for normal environment"""
801         if 'RUSTUP_DIST_SERVER' in os.environ:
802             self._download_url = os.environ['RUSTUP_DIST_SERVER']
803         else:
804             self._download_url = url
805
806     def check_vendored_status(self):
807         """Check that vendoring is configured properly"""
808         # keep this consistent with the equivalent check in rustbuild:
809         # https://github.com/rust-lang/rust/blob/a8a33cf27166d3eabaffc58ed3799e054af3b0c6/src/bootstrap/lib.rs#L399-L405
810         if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
811             if os.getuid() == 0:
812                 self.use_vendored_sources = True
813                 print('info: looks like you\'re trying to run this command as root')
814                 print('      and so in order to preserve your $HOME this will now')
815                 print('      use vendored sources by default.')
816
817         cargo_dir = os.path.join(self.rust_root, '.cargo')
818         if self.use_vendored_sources:
819             vendor_dir = os.path.join(self.rust_root, 'vendor')
820             if not os.path.exists(vendor_dir):
821                 sync_dirs = "--sync ./src/tools/rust-analyzer/Cargo.toml " \
822                             "--sync ./compiler/rustc_codegen_cranelift/Cargo.toml " \
823                             "--sync ./src/bootstrap/Cargo.toml "
824                 print('error: vendoring required, but vendor directory does not exist.')
825                 print('       Run `cargo vendor {}` to initialize the '
826                       'vendor directory.'.format(sync_dirs))
827                 print('Alternatively, use the pre-vendored `rustc-src` dist component.')
828                 raise Exception("{} not found".format(vendor_dir))
829
830             if not os.path.exists(cargo_dir):
831                 print('error: vendoring required, but .cargo/config does not exist.')
832                 raise Exception("{} not found".format(cargo_dir))
833         else:
834             if os.path.exists(cargo_dir):
835                 shutil.rmtree(cargo_dir)
836
837 def bootstrap(help_triggered):
838     """Configure, fetch, build and run the initial bootstrap"""
839
840     # If the user is asking for help, let them know that the whole download-and-build
841     # process has to happen before anything is printed out.
842     if help_triggered:
843         print("info: Downloading and building bootstrap before processing --help")
844         print("      command. See src/bootstrap/README.md for help with common")
845         print("      commands.")
846
847     parser = argparse.ArgumentParser(description='Build rust')
848     parser.add_argument('--config')
849     parser.add_argument('--build-dir')
850     parser.add_argument('--build')
851     parser.add_argument('--color', choices=['always', 'never', 'auto'])
852     parser.add_argument('--clean', action='store_true')
853     parser.add_argument('-v', '--verbose', action='count', default=0)
854
855     args = [a for a in sys.argv if a != '-h' and a != '--help']
856     args, _ = parser.parse_known_args(args)
857
858     # Configure initial bootstrap
859     build = RustBuild()
860     build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
861     build.verbose = args.verbose
862     build.clean = args.clean
863
864     # Read from `--config`, then `RUST_BOOTSTRAP_CONFIG`, then `./config.toml`,
865     # then `config.toml` in the root directory.
866     toml_path = args.config or os.getenv('RUST_BOOTSTRAP_CONFIG')
867     using_default_path = toml_path is None
868     if using_default_path:
869         toml_path = 'config.toml'
870         if not os.path.exists(toml_path):
871             toml_path = os.path.join(build.rust_root, toml_path)
872
873     # Give a hard error if `--config` or `RUST_BOOTSTRAP_CONFIG` are set to a missing path,
874     # but not if `config.toml` hasn't been created.
875     if not using_default_path or os.path.exists(toml_path):
876         with open(toml_path) as config:
877             build.config_toml = config.read()
878
879     profile = build.get_toml('profile')
880     if profile is not None:
881         include_file = 'config.{}.toml'.format(profile)
882         include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
883         include_path = os.path.join(include_dir, include_file)
884         # HACK: This works because `build.get_toml()` returns the first match it finds for a
885         # specific key, so appending our defaults at the end allows the user to override them
886         with open(include_path) as included_toml:
887             build.config_toml += os.linesep + included_toml.read()
888
889     config_verbose = build.get_toml('verbose', 'build')
890     if config_verbose is not None:
891         build.verbose = max(build.verbose, int(config_verbose))
892
893     build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
894
895     build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
896
897     build.check_vendored_status()
898
899     build_dir = args.build_dir or build.get_toml('build-dir', 'build') or 'build'
900     build.build_dir = os.path.abspath(build_dir)
901
902     with open(os.path.join(build.rust_root, "src", "stage0.json")) as f:
903         data = json.load(f)
904     build.checksums_sha256 = data["checksums_sha256"]
905     build.stage0_compiler = Stage0Toolchain(data["compiler"])
906
907     build.set_dist_environment(data["config"]["dist_server"])
908
909     build.build = args.build or build.build_triple()
910
911     if not os.path.exists(build.build_dir):
912         os.makedirs(build.build_dir)
913
914     # Fetch/build the bootstrap
915     build.download_toolchain()
916     sys.stdout.flush()
917     build.build_bootstrap(args.color)
918     sys.stdout.flush()
919
920     # Run the bootstrap
921     args = [build.bootstrap_binary()]
922     args.extend(sys.argv[1:])
923     env = os.environ.copy()
924     env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
925     env["BOOTSTRAP_PYTHON"] = sys.executable
926     run(args, env=env, verbose=build.verbose, is_bootstrap=True)
927
928
929 def main():
930     """Entry point for the bootstrap process"""
931     start_time = time()
932
933     # x.py help <cmd> ...
934     if len(sys.argv) > 1 and sys.argv[1] == 'help':
935         sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
936
937     help_triggered = (
938         '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
939     try:
940         bootstrap(help_triggered)
941         if not help_triggered:
942             print("Build completed successfully in {}".format(
943                 format_build_time(time() - start_time)))
944     except (SystemExit, KeyboardInterrupt) as error:
945         if hasattr(error, 'code') and isinstance(error.code, int):
946             exit_code = error.code
947         else:
948             exit_code = 1
949             print(error)
950         if not help_triggered:
951             print("Build completed unsuccessfully in {}".format(
952                 format_build_time(time() - start_time)))
953         sys.exit(exit_code)
954
955
956 if __name__ == '__main__':
957     main()