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