]> git.proxmox.com Git - rustc.git/blobdiff - src/bootstrap/bootstrap.py
New upstream version 1.55.0+dfsg1
[rustc.git] / src / bootstrap / bootstrap.py
index d9c66ce2d77905614b5f242c68bfafc123bc7d48..d2cf929aa266f82a0b9618b68aa528a92a7de65c 100644 (file)
@@ -1,17 +1,8 @@
-# Copyright 2015-2016 The Rust Project Developers. See the COPYRIGHT
-# file at the top-level directory of this distribution and at
-# http://rust-lang.org/COPYRIGHT.
-#
-# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
-# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
-# option. This file may not be copied, modified, or distributed
-# except according to those terms.
-
 from __future__ import absolute_import, division, print_function
 import argparse
 import contextlib
 import datetime
+import distutils.version
 import hashlib
 import os
 import re
@@ -23,8 +14,17 @@ import tempfile
 
 from time import time
 
-
-def get(url, path, verbose=False):
+def support_xz():
+    try:
+        with tempfile.NamedTemporaryFile(delete=False) as temp_file:
+            temp_path = temp_file.name
+        with tarfile.open(temp_path, "w:xz"):
+            pass
+        return True
+    except tarfile.CompressionError:
+        return False
+
+def get(url, path, verbose=False, do_verify=True):
     suffix = '.sha256'
     sha_url = url + suffix
     with tempfile.NamedTemporaryFile(delete=False) as temp_file:
@@ -33,19 +33,20 @@ def get(url, path, verbose=False):
         sha_path = sha_file.name
 
     try:
-        download(sha_path, sha_url, False, verbose)
-        if os.path.exists(path):
-            if verify(path, sha_path, False):
-                if verbose:
-                    print("using already-download file", path)
-                return
-            else:
-                if verbose:
-                    print("ignoring already-download file",
-                          path, "due to failed verification")
-                os.unlink(path)
+        if do_verify:
+            download(sha_path, sha_url, False, verbose)
+            if os.path.exists(path):
+                if verify(path, sha_path, False):
+                    if verbose:
+                        print("using already-download file", path)
+                    return
+                else:
+                    if verbose:
+                        print("ignoring already-download file",
+                            path, "due to failed verification")
+                    os.unlink(path)
         download(temp_path, url, True, verbose)
-        if not verify(temp_path, sha_path, verbose):
+        if do_verify and not verify(temp_path, sha_path, verbose):
             raise RuntimeError("failed verification")
         if verbose:
             print("moving {} to {}".format(temp_path, path))
@@ -76,11 +77,11 @@ def download(path, url, probably_big, verbose):
 def _download(path, url, probably_big, verbose, exception):
     if probably_big or verbose:
         print("downloading {}".format(url))
-    # see http://serverfault.com/questions/301128/how-to-download
+    # see https://serverfault.com/questions/301128/how-to-download
     if sys.platform == 'win32':
         run(["PowerShell.exe", "/nologo", "-Command",
-             "(New-Object System.Net.WebClient)"
-             ".DownloadFile('{}', '{}')".format(url, path)],
+             "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
+             "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
             verbose=verbose,
             exception=exception)
     else:
@@ -88,9 +89,10 @@ def _download(path, url, probably_big, verbose, exception):
             option = "-#"
         else:
             option = "-s"
+        require(["curl", "--version"])
         run(["curl", option,
              "-y", "30", "-Y", "10",    # timeout if speed is < 10 bytes/sec for > 30 seconds
-             "--connect-timeout", "30", # timeout if cannot connect within 30 seconds
+             "--connect-timeout", "30",  # timeout if cannot connect within 30 seconds
              "--retry", "3", "-Sf", "-o", path, url],
             verbose=verbose,
             exception=exception)
@@ -112,10 +114,10 @@ def verify(path, sha_path, verbose):
     return verified
 
 
-def unpack(tarball, dst, verbose=False, match=None):
+def unpack(tarball, tarball_suffix, dst, verbose=False, match=None):
     """Unpack the given tarball file"""
     print("extracting", tarball)
-    fname = os.path.basename(tarball).replace(".tar.gz", "")
+    fname = os.path.basename(tarball).replace(tarball_suffix, "")
     with contextlib.closing(tarfile.open(tarball)) as tar:
         for member in tar.getnames():
             if "/" not in member:
@@ -136,7 +138,7 @@ def unpack(tarball, dst, verbose=False, match=None):
     shutil.rmtree(os.path.join(dst, fname))
 
 
-def run(args, verbose=False, exception=False, **kwargs):
+def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
     """Run a child program in a new process"""
     if verbose:
         print("running: " + ' '.join(args))
@@ -149,7 +151,29 @@ def run(args, verbose=False, exception=False, **kwargs):
         err = "failed to run: " + ' '.join(args)
         if verbose or exception:
             raise RuntimeError(err)
-        sys.exit(err)
+        # For most failures, we definitely do want to print this error, or the user will have no
+        # idea what went wrong. But when we've successfully built bootstrap and it failed, it will
+        # have already printed an error above, so there's no need to print the exact command we're
+        # running.
+        if is_bootstrap:
+            sys.exit(1)
+        else:
+            sys.exit(err)
+
+
+def require(cmd, exit=True):
+    '''Run a command, returning its output.
+    On error,
+        If `exit` is `True`, exit the process.
+        Otherwise, return None.'''
+    try:
+        return subprocess.check_output(cmd).strip()
+    except (subprocess.CalledProcessError, OSError) as exc:
+        if not exit:
+            return None
+        print("error: unable to run `{}`: {}".format(' '.join(cmd), exc))
+        print("Please make sure it's installed and in the path.")
+        sys.exit(1)
 
 
 def stage0_data(rust_root):
@@ -170,24 +194,40 @@ def format_build_time(duration):
     return str(datetime.timedelta(seconds=int(duration)))
 
 
-def default_build_triple():
+def default_build_triple(verbose):
     """Build triple as in LLVM"""
+    # If the user already has a host build triple with an existing `rustc`
+    # install, use their preference. This fixes most issues with Windows builds
+    # being detected as GNU instead of MSVC.
     default_encoding = sys.getdefaultencoding()
     try:
-        ostype = subprocess.check_output(
-            ['uname', '-s']).strip().decode(default_encoding)
-        cputype = subprocess.check_output(
-            ['uname', '-m']).strip().decode(default_encoding)
-    except (subprocess.CalledProcessError, OSError):
-        if sys.platform == 'win32':
-            return 'x86_64-pc-windows-msvc'
-        err = "uname not found"
-        sys.exit(err)
+        version = subprocess.check_output(["rustc", "--version", "--verbose"],
+                stderr=subprocess.DEVNULL)
+        version = version.decode(default_encoding)
+        host = next(x for x in version.split('\n') if x.startswith("host: "))
+        triple = host.split("host: ")[1]
+        if verbose:
+            print("detected default triple {}".format(triple))
+        return triple
+    except Exception as e:
+        if verbose:
+            print("rustup not detected: {}".format(e))
+            print("falling back to auto-detect")
+
+    required = sys.platform != 'win32'
+    ostype = require(["uname", "-s"], exit=required)
+    cputype = require(['uname', '-m'], exit=required)
+
+    # If we do not have `uname`, assume Windows.
+    if ostype is None or cputype is None:
+        return 'x86_64-pc-windows-msvc'
+
+    ostype = ostype.decode(default_encoding)
+    cputype = cputype.decode(default_encoding)
 
     # The goal here is to come up with the same triple as LLVM would,
     # at least for the subset of platforms we're willing to target.
     ostype_mapper = {
-        'Bitrig': 'unknown-bitrig',
         'Darwin': 'apple-darwin',
         'DragonFly': 'unknown-dragonfly',
         'FreeBSD': 'unknown-freebsd',
@@ -207,18 +247,16 @@ def default_build_triple():
         else:
             ostype = 'unknown-linux-gnu'
     elif ostype == 'SunOS':
-        ostype = 'sun-solaris'
+        ostype = 'pc-solaris'
         # On Solaris, uname -m will return a machine classification instead
         # of a cpu type, so uname -p is recommended instead.  However, the
         # output from that option is too generic for our purposes (it will
         # always emit 'i386' on x86/amd64 systems).  As such, isainfo -k
         # must be used instead.
-        try:
-            cputype = subprocess.check_output(
-                ['isainfo', '-k']).strip().decode(default_encoding)
-        except (subprocess.CalledProcessError, OSError):
-            err = "isainfo not found"
-            sys.exit(err)
+        cputype = require(['isainfo', '-k']).decode(default_encoding)
+        # sparc cpus have sun as a target vendor
+        if 'sparc' in cputype:
+            ostype = 'sun-solaris'
     elif ostype.startswith('MINGW'):
         # msys' `uname` does not print gcc configuration, but prints msys
         # configuration. so we cannot believe `uname -m`:
@@ -236,10 +274,18 @@ def default_build_triple():
         if ostype.endswith('WOW64'):
             cputype = 'x86_64'
         ostype = 'pc-windows-gnu'
+    elif sys.platform == 'win32':
+        # Some Windows platforms might have a `uname` command that returns a
+        # non-standard string (e.g. gnuwin32 tools returns `windows32`). In
+        # these cases, fall back to using sys.platform.
+        return 'x86_64-pc-windows-msvc'
     else:
         err = "unknown OS type: {}".format(ostype)
         sys.exit(err)
 
+    if cputype == 'powerpc' and ostype == 'unknown-freebsd':
+        cputype = subprocess.check_output(
+              ['uname', '-p']).strip().decode(default_encoding)
     cputype_mapper = {
         'BePC': 'i686',
         'aarch64': 'aarch64',
@@ -269,6 +315,10 @@ def default_build_triple():
         cputype = 'arm'
         if ostype == 'linux-android':
             ostype = 'linux-androideabi'
+        elif ostype == 'unknown-freebsd':
+            cputype = subprocess.check_output(
+                ['uname', '-p']).strip().decode(default_encoding)
+            ostype = 'unknown-freebsd'
     elif cputype == 'armv6l':
         cputype = 'arm'
         if ostype == 'linux-android':
@@ -312,30 +362,35 @@ def output(filepath):
     with open(tmp, 'w') as f:
         yield f
     try:
-        os.remove(filepath)  # PermissionError/OSError on Win32 if in use
-        os.rename(tmp, filepath)
+        if os.path.exists(filepath):
+            os.remove(filepath)  # PermissionError/OSError on Win32 if in use
     except OSError:
         shutil.copy2(tmp, filepath)
         os.remove(tmp)
+        return
+    os.rename(tmp, filepath)
 
 
 class RustBuild(object):
     """Provide all the methods required to build Rust"""
     def __init__(self):
-        self.cargo_channel = ''
         self.date = ''
-        self._download_url = 'https://static.rust-lang.org'
+        self._download_url = ''
         self.rustc_channel = ''
+        self.rustfmt_channel = ''
         self.build = ''
-        self.build_dir = os.path.join(os.getcwd(), "build")
+        self.build_dir = ''
         self.clean = False
         self.config_toml = ''
         self.rust_root = ''
         self.use_locked_deps = ''
         self.use_vendored_sources = ''
         self.verbose = False
+        self.git_version = None
+        self.nix_deps_dir = None
+        self.rustc_commit = None
 
-    def download_stage0(self):
+    def download_toolchain(self, stage0=True, rustc_channel=None):
         """Fetch the build system for Rust, written in Rust
 
         This method will build a cache directory, then it will fetch the
@@ -345,61 +400,169 @@ class RustBuild(object):
         Each downloaded tarball is extracted, after that, the script
         will move all the content to the right place.
         """
-        rustc_channel = self.rustc_channel
-        cargo_channel = self.cargo_channel
-
-        if self.rustc().startswith(self.bin_root()) and \
-                (not os.path.exists(self.rustc()) or
-                 self.program_out_of_date(self.rustc_stamp())):
-            if os.path.exists(self.bin_root()):
-                shutil.rmtree(self.bin_root())
-            filename = "rust-std-{}-{}.tar.gz".format(
-                rustc_channel, self.build)
+        if rustc_channel is None:
+            rustc_channel = self.rustc_channel
+        rustfmt_channel = self.rustfmt_channel
+        bin_root = self.bin_root(stage0)
+
+        key = self.date
+        if not stage0:
+            key += str(self.rustc_commit)
+        if self.rustc(stage0).startswith(bin_root) and \
+                (not os.path.exists(self.rustc(stage0)) or
+                 self.program_out_of_date(self.rustc_stamp(stage0), key)):
+            if os.path.exists(bin_root):
+                shutil.rmtree(bin_root)
+            tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
+            filename = "rust-std-{}-{}{}".format(
+                rustc_channel, self.build, tarball_suffix)
             pattern = "rust-std-{}".format(self.build)
-            self._download_stage0_helper(filename, pattern)
-
-            filename = "rustc-{}-{}.tar.gz".format(rustc_channel, self.build)
-            self._download_stage0_helper(filename, "rustc")
-            self.fix_executable("{}/bin/rustc".format(self.bin_root()))
-            self.fix_executable("{}/bin/rustdoc".format(self.bin_root()))
-            with output(self.rustc_stamp()) as rust_stamp:
-                rust_stamp.write(self.date)
-
-            # This is required so that we don't mix incompatible MinGW
-            # libraries/binaries that are included in rust-std with
-            # the system MinGW ones.
-            if "pc-windows-gnu" in self.build:
-                filename = "rust-mingw-{}-{}.tar.gz".format(
-                    rustc_channel, self.build)
-                self._download_stage0_helper(filename, "rust-mingw")
-
-        if self.cargo().startswith(self.bin_root()) and \
-                (not os.path.exists(self.cargo()) or
-                 self.program_out_of_date(self.cargo_stamp())):
-            filename = "cargo-{}-{}.tar.gz".format(cargo_channel, self.build)
-            self._download_stage0_helper(filename, "cargo")
-            self.fix_executable("{}/bin/cargo".format(self.bin_root()))
-            with output(self.cargo_stamp()) as cargo_stamp:
-                cargo_stamp.write(self.date)
-
-    def _download_stage0_helper(self, filename, pattern):
+            self._download_component_helper(filename, pattern, tarball_suffix, stage0)
+            filename = "rustc-{}-{}{}".format(rustc_channel, self.build,
+                                              tarball_suffix)
+            self._download_component_helper(filename, "rustc", tarball_suffix, stage0)
+            # download-rustc doesn't need its own cargo, it can just use beta's.
+            if stage0:
+                filename = "cargo-{}-{}{}".format(rustc_channel, self.build,
+                                                tarball_suffix)
+                self._download_component_helper(filename, "cargo", tarball_suffix)
+                self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))
+            else:
+                filename = "rustc-dev-{}-{}{}".format(rustc_channel, self.build, tarball_suffix)
+                self._download_component_helper(
+                    filename, "rustc-dev", tarball_suffix, stage0
+                )
+
+            self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
+            self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
+            lib_dir = "{}/lib".format(bin_root)
+            for lib in os.listdir(lib_dir):
+                if lib.endswith(".so"):
+                    self.fix_bin_or_dylib(os.path.join(lib_dir, lib))
+            with output(self.rustc_stamp(stage0)) as rust_stamp:
+                rust_stamp.write(key)
+
+        if self.rustfmt() and self.rustfmt().startswith(bin_root) and (
+            not os.path.exists(self.rustfmt())
+            or self.program_out_of_date(self.rustfmt_stamp(), self.rustfmt_channel)
+        ):
+            if rustfmt_channel:
+                tarball_suffix = '.tar.xz' if support_xz() else '.tar.gz'
+                [channel, date] = rustfmt_channel.split('-', 1)
+                filename = "rustfmt-{}-{}{}".format(channel, self.build, tarball_suffix)
+                self._download_component_helper(
+                    filename, "rustfmt-preview", tarball_suffix, key=date
+                )
+                self.fix_bin_or_dylib("{}/bin/rustfmt".format(bin_root))
+                self.fix_bin_or_dylib("{}/bin/cargo-fmt".format(bin_root))
+                with output(self.rustfmt_stamp()) as rustfmt_stamp:
+                    rustfmt_stamp.write(self.rustfmt_channel)
+
+        # Avoid downloading LLVM twice (once for stage0 and once for the master rustc)
+        if self.downloading_llvm() and stage0:
+            # We want the most recent LLVM submodule update to avoid downloading
+            # LLVM more often than necessary.
+            #
+            # This git command finds that commit SHA, looking for bors-authored
+            # merges that modified src/llvm-project.
+            #
+            # This works even in a repository that has not yet initialized
+            # submodules.
+            top_level = subprocess.check_output([
+                "git", "rev-parse", "--show-toplevel",
+            ]).decode(sys.getdefaultencoding()).strip()
+            llvm_sha = subprocess.check_output([
+                "git", "log", "--author=bors", "--format=%H", "-n1",
+                "-m", "--first-parent",
+                "--",
+                "{}/src/llvm-project".format(top_level),
+                "{}/src/bootstrap/download-ci-llvm-stamp".format(top_level),
+                # the LLVM shared object file is named `LLVM-12-rust-{version}-nightly`
+                "{}/src/version".format(top_level)
+            ]).decode(sys.getdefaultencoding()).strip()
+            llvm_assertions = self.get_toml('assertions', 'llvm') == 'true'
+            llvm_root = self.llvm_root()
+            llvm_lib = os.path.join(llvm_root, "lib")
+            if self.program_out_of_date(self.llvm_stamp(), llvm_sha + str(llvm_assertions)):
+                self._download_ci_llvm(llvm_sha, llvm_assertions)
+                for binary in ["llvm-config", "FileCheck"]:
+                    self.fix_bin_or_dylib(os.path.join(llvm_root, "bin", binary))
+                for lib in os.listdir(llvm_lib):
+                    if lib.endswith(".so"):
+                        self.fix_bin_or_dylib(os.path.join(llvm_lib, lib))
+                with output(self.llvm_stamp()) as llvm_stamp:
+                    llvm_stamp.write(llvm_sha + str(llvm_assertions))
+
+    def downloading_llvm(self):
+        opt = self.get_toml('download-ci-llvm', 'llvm')
+        # This is currently all tier 1 targets (since others may not have CI
+        # artifacts)
+        # https://doc.rust-lang.org/rustc/platform-support.html#tier-1
+        supported_platforms = [
+            "aarch64-unknown-linux-gnu",
+            "i686-pc-windows-gnu",
+            "i686-pc-windows-msvc",
+            "i686-unknown-linux-gnu",
+            "x86_64-unknown-linux-gnu",
+            "x86_64-apple-darwin",
+            "x86_64-pc-windows-gnu",
+            "x86_64-pc-windows-msvc",
+        ]
+        return opt == "true" \
+            or (opt == "if-available" and self.build in supported_platforms)
+
+    def _download_component_helper(
+        self, filename, pattern, tarball_suffix, stage0=True, key=None
+    ):
+        if key is None:
+            if stage0:
+                key = self.date
+            else:
+                key = self.rustc_commit
         cache_dst = os.path.join(self.build_dir, "cache")
-        rustc_cache = os.path.join(cache_dst, self.date)
+        rustc_cache = os.path.join(cache_dst, key)
         if not os.path.exists(rustc_cache):
             os.makedirs(rustc_cache)
 
-        url = "{}/dist/{}".format(self._download_url, self.date)
+        if stage0:
+            url = "{}/dist/{}".format(self._download_url, key)
+        else:
+            url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(self.rustc_commit)
         tarball = os.path.join(rustc_cache, filename)
         if not os.path.exists(tarball):
-            get("{}/{}".format(url, filename), tarball, verbose=self.verbose)
-        unpack(tarball, self.bin_root(), match=pattern, verbose=self.verbose)
+            get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=stage0)
+        unpack(tarball, tarball_suffix, self.bin_root(stage0), match=pattern, verbose=self.verbose)
 
-    @staticmethod
-    def fix_executable(fname):
-        """Modifies the interpreter section of 'fname' to fix the dynamic linker
+    def _download_ci_llvm(self, llvm_sha, llvm_assertions):
+        cache_prefix = "llvm-{}-{}".format(llvm_sha, llvm_assertions)
+        cache_dst = os.path.join(self.build_dir, "cache")
+        rustc_cache = os.path.join(cache_dst, cache_prefix)
+        if not os.path.exists(rustc_cache):
+            os.makedirs(rustc_cache)
+
+        url = "https://ci-artifacts.rust-lang.org/rustc-builds/{}".format(llvm_sha)
+        if llvm_assertions:
+            url = url.replace('rustc-builds', 'rustc-builds-alt')
+        # ci-artifacts are only stored as .xz, not .gz
+        if not support_xz():
+            print("error: XZ support is required to download LLVM")
+            print("help: consider disabling `download-ci-llvm` or using python3")
+            exit(1)
+        tarball_suffix = '.tar.xz'
+        filename = "rust-dev-nightly-" + self.build + tarball_suffix
+        tarball = os.path.join(rustc_cache, filename)
+        if not os.path.exists(tarball):
+            get("{}/{}".format(url, filename), tarball, verbose=self.verbose, do_verify=False)
+        unpack(tarball, tarball_suffix, self.llvm_root(),
+                match="rust-dev",
+                verbose=self.verbose)
+
+    def fix_bin_or_dylib(self, fname):
+        """Modifies the interpreter section of 'fname' to fix the dynamic linker,
+        or the RPATH section, to fix the dynamic library search path
 
         This method is only required on NixOS and uses the PatchELF utility to
-        change the dynamic linker of ELF executables.
+        change the interpreter/RPATH of ELF executables.
 
         Please see https://nixos.org/patchelf.html for more information
         """
@@ -417,7 +580,13 @@ class RustBuild(object):
         if ostype != "Linux":
             return
 
-        if not os.path.exists("/etc/NIXOS"):
+        # Use `/etc/os-release` instead of `/etc/NIXOS`.
+        # The latter one does not exist on NixOS when using tmpfs as root.
+        try:
+            with open("/etc/os-release", "r") as f:
+                if not any(line.strip() == "ID=nixos" for line in f):
+                    return
+        except FileNotFoundError:
             return
         if os.path.exists("/lib"):
             return
@@ -426,84 +595,170 @@ class RustBuild(object):
         nix_os_msg = "info: you seem to be running NixOS. Attempting to patch"
         print(nix_os_msg, fname)
 
-        try:
-            interpreter = subprocess.check_output(
-                ["patchelf", "--print-interpreter", fname])
-            interpreter = interpreter.strip().decode(default_encoding)
-        except subprocess.CalledProcessError as reason:
-            print("warning: failed to call patchelf:", reason)
-            return
-
-        loader = interpreter.split("/")[-1]
+        # Only build `.nix-deps` once.
+        nix_deps_dir = self.nix_deps_dir
+        if not nix_deps_dir:
+            # Run `nix-build` to "build" each dependency (which will likely reuse
+            # the existing `/nix/store` copy, or at most download a pre-built copy).
+            #
+            # Importantly, we create a gc-root called `.nix-deps` in the `build/`
+            # directory, but still reference the actual `/nix/store` path in the rpath
+            # as it makes it significantly more robust against changes to the location of
+            # the `.nix-deps` location.
+            #
+            # bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
+            # zlib: Needed as a system dependency of `libLLVM-*.so`.
+            # patchelf: Needed for patching ELF binaries (see doc comment above).
+            nix_deps_dir = "{}/{}".format(self.build_dir, ".nix-deps")
+            nix_expr = '''
+            with (import <nixpkgs> {});
+            symlinkJoin {
+              name = "rust-stage0-dependencies";
+              paths = [
+                zlib
+                patchelf
+                stdenv.cc.bintools
+              ];
+            }
+            '''
+            try:
+                subprocess.check_output([
+                    "nix-build", "-E", nix_expr, "-o", nix_deps_dir,
+                ])
+            except subprocess.CalledProcessError as reason:
+                print("warning: failed to call nix-build:", reason)
+                return
+            self.nix_deps_dir = nix_deps_dir
+
+        patchelf = "{}/bin/patchelf".format(nix_deps_dir)
+        rpath_entries = [
+            # Relative default, all binary and dynamic libraries we ship
+            # appear to have this (even when `../lib` is redundant).
+            "$ORIGIN/../lib",
+            os.path.join(os.path.realpath(nix_deps_dir), "lib")
+        ]
+        patchelf_args = ["--set-rpath", ":".join(rpath_entries)]
+        if not fname.endswith(".so"):
+            # Finally, set the corret .interp for binaries
+            with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
+                patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
 
         try:
-            ldd_output = subprocess.check_output(
-                ['ldd', '/run/current-system/sw/bin/sh'])
-            ldd_output = ldd_output.strip().decode(default_encoding)
+            subprocess.check_output([patchelf] + patchelf_args + [fname])
         except subprocess.CalledProcessError as reason:
-            print("warning: unable to call ldd:", reason)
-            return
-
-        for line in ldd_output.splitlines():
-            libname = line.split()[0]
-            if libname.endswith(loader):
-                loader_path = libname[:len(libname) - len(loader)]
-                break
-        else:
-            print("warning: unable to find the path to the dynamic linker")
+            print("warning: failed to call patchelf:", reason)
             return
 
-        correct_interpreter = loader_path + loader
+    # If `download-rustc` is set, download the most recent commit with CI artifacts
+    def maybe_download_ci_toolchain(self):
+        # If `download-rustc` is not set, default to rebuilding.
+        download_rustc = self.get_toml("download-rustc", section="rust")
+        if download_rustc is None or download_rustc == "false":
+            return None
+        assert download_rustc == "true" or download_rustc == "if-unchanged", download_rustc
+
+        # Handle running from a directory other than the top level
+        rev_parse = ["git", "rev-parse", "--show-toplevel"]
+        top_level = subprocess.check_output(rev_parse, universal_newlines=True).strip()
+        compiler = "{}/compiler/".format(top_level)
+        library = "{}/library/".format(top_level)
+
+        # Look for a version to compare to based on the current commit.
+        # Only commits merged by bors will have CI artifacts.
+        merge_base = ["git", "log", "--author=bors", "--pretty=%H", "-n1"]
+        commit = subprocess.check_output(merge_base, universal_newlines=True).strip()
+
+        # Warn if there were changes to the compiler or standard library since the ancestor commit.
+        status = subprocess.call(["git", "diff-index", "--quiet", commit, "--", compiler, library])
+        if status != 0:
+            if download_rustc == "if-unchanged":
+                return None
+            print("warning: `download-rustc` is enabled, but there are changes to \
+                   compiler/ or library/")
+
+        if self.verbose:
+            print("using downloaded stage1 artifacts from CI (commit {})".format(commit))
+        self.rustc_commit = commit
+        # FIXME: support downloading artifacts from the beta channel
+        self.download_toolchain(False, "nightly")
+
+    def rustc_stamp(self, stage0):
+        """Return the path for .rustc-stamp at the given stage
 
-        try:
-            subprocess.check_output(
-                ["patchelf", "--set-interpreter", correct_interpreter, fname])
-        except subprocess.CalledProcessError as reason:
-            print("warning: failed to call patchelf:", reason)
-            return
+        >>> rb = RustBuild()
+        >>> rb.build_dir = "build"
+        >>> rb.rustc_stamp(True) == os.path.join("build", "stage0", ".rustc-stamp")
+        True
+        >>> rb.rustc_stamp(False) == os.path.join("build", "ci-rustc", ".rustc-stamp")
+        True
+        """
+        return os.path.join(self.bin_root(stage0), '.rustc-stamp')
 
-    def rustc_stamp(self):
-        """Return the path for .rustc-stamp
+    def rustfmt_stamp(self):
+        """Return the path for .rustfmt-stamp
 
         >>> rb = RustBuild()
         >>> rb.build_dir = "build"
-        >>> rb.rustc_stamp() == os.path.join("build", "stage0", ".rustc-stamp")
+        >>> rb.rustfmt_stamp() == os.path.join("build", "stage0", ".rustfmt-stamp")
         True
         """
-        return os.path.join(self.bin_root(), '.rustc-stamp')
+        return os.path.join(self.bin_root(True), '.rustfmt-stamp')
 
-    def cargo_stamp(self):
-        """Return the path for .cargo-stamp
+    def llvm_stamp(self):
+        """Return the path for .rustfmt-stamp
 
         >>> rb = RustBuild()
         >>> rb.build_dir = "build"
-        >>> rb.cargo_stamp() == os.path.join("build", "stage0", ".cargo-stamp")
+        >>> rb.llvm_stamp() == os.path.join("build", "ci-llvm", ".llvm-stamp")
         True
         """
-        return os.path.join(self.bin_root(), '.cargo-stamp')
+        return os.path.join(self.llvm_root(), '.llvm-stamp')
 
-    def program_out_of_date(self, stamp_path):
+
+    def program_out_of_date(self, stamp_path, key):
         """Check if the given program stamp is out of date"""
         if not os.path.exists(stamp_path) or self.clean:
             return True
         with open(stamp_path, 'r') as stamp:
-            return self.date != stamp.read()
+            return key != stamp.read()
+
+    def bin_root(self, stage0):
+        """Return the binary root directory for the given stage
+
+        >>> rb = RustBuild()
+        >>> rb.build_dir = "build"
+        >>> rb.bin_root(True) == os.path.join("build", "stage0")
+        True
+        >>> rb.bin_root(False) == os.path.join("build", "ci-rustc")
+        True
+
+        When the 'build' property is given should be a nested directory:
+
+        >>> rb.build = "devel"
+        >>> rb.bin_root(True) == os.path.join("build", "devel", "stage0")
+        True
+        """
+        if stage0:
+            subdir = "stage0"
+        else:
+            subdir = "ci-rustc"
+        return os.path.join(self.build_dir, self.build, subdir)
 
-    def bin_root(self):
-        """Return the binary root directory
+    def llvm_root(self):
+        """Return the CI LLVM root directory
 
         >>> rb = RustBuild()
         >>> rb.build_dir = "build"
-        >>> rb.bin_root() == os.path.join("build", "stage0")
+        >>> rb.llvm_root() == os.path.join("build", "ci-llvm")
         True
 
         When the 'build' property is given should be a nested directory:
 
         >>> rb.build = "devel"
-        >>> rb.bin_root() == os.path.join("build", "devel", "stage0")
+        >>> rb.llvm_root() == os.path.join("build", "devel", "ci-llvm")
         True
         """
-        return os.path.join(self.build_dir, self.build, "stage0")
+        return os.path.join(self.build_dir, self.build, "ci-llvm")
 
     def get_toml(self, key, section=None):
         """Returns the value of the given key in config.toml, otherwise returns None
@@ -527,6 +782,10 @@ class RustBuild(object):
         'value2'
         >>> rb.get_toml('key', 'c') is None
         True
+
+        >>> rb.config_toml = 'key1 = true'
+        >>> rb.get_toml("key1")
+        'true'
         """
 
         cur_section = None
@@ -546,27 +805,37 @@ class RustBuild(object):
         """Return config path for cargo"""
         return self.program_config('cargo')
 
-    def rustc(self):
+    def rustc(self, stage0):
         """Return config path for rustc"""
-        return self.program_config('rustc')
+        return self.program_config('rustc', stage0)
 
-    def program_config(self, program):
-        """Return config path for the given program
+    def rustfmt(self):
+        """Return config path for rustfmt"""
+        if not self.rustfmt_channel:
+            return None
+        return self.program_config('rustfmt')
+
+    def program_config(self, program, stage0=True):
+        """Return config path for the given program at the given stage
 
         >>> rb = RustBuild()
         >>> rb.config_toml = 'rustc = "rustc"\\n'
         >>> rb.program_config('rustc')
         'rustc'
         >>> rb.config_toml = ''
-        >>> cargo_path = rb.program_config('cargo')
-        >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(),
+        >>> cargo_path = rb.program_config('cargo', True)
+        >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(True),
+        ... "bin", "cargo")
+        True
+        >>> cargo_path = rb.program_config('cargo', False)
+        >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(False),
         ... "bin", "cargo")
         True
         """
         config = self.get_toml(program)
         if config:
             return os.path.expanduser(config)
-        return os.path.join(self.bin_root(), "bin", "{}{}".format(
+        return os.path.join(self.bin_root(stage0), "bin", "{}{}".format(
             program, self.exe_suffix()))
 
     @staticmethod
@@ -575,6 +844,12 @@ class RustBuild(object):
 
         >>> RustBuild.get_string('    "devel"   ')
         'devel'
+        >>> RustBuild.get_string("    'devel'   ")
+        'devel'
+        >>> RustBuild.get_string('devel') is None
+        True
+        >>> RustBuild.get_string('    "devel   ')
+        ''
         """
         start = line.find('"')
         if start != -1:
@@ -594,7 +869,7 @@ class RustBuild(object):
         return ''
 
     def bootstrap_binary(self):
-        """Return the path of the boostrap binary
+        """Return the path of the bootstrap binary
 
         >>> rb = RustBuild()
         >>> rb.build_dir = "build"
@@ -610,30 +885,42 @@ class RustBuild(object):
         if self.clean and os.path.exists(build_dir):
             shutil.rmtree(build_dir)
         env = os.environ.copy()
-        env["RUSTC_BOOTSTRAP"] = '1'
+        # `CARGO_BUILD_TARGET` breaks bootstrap build.
+        # See also: <https://github.com/rust-lang/rust/issues/70208>.
+        if "CARGO_BUILD_TARGET" in env:
+            del env["CARGO_BUILD_TARGET"]
         env["CARGO_TARGET_DIR"] = build_dir
-        env["RUSTC"] = self.rustc()
-        env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
+        env["RUSTC"] = self.rustc(True)
+        env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
             (os.pathsep + env["LD_LIBRARY_PATH"]) \
             if "LD_LIBRARY_PATH" in env else ""
-        env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
+        env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
             (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
             if "DYLD_LIBRARY_PATH" in env else ""
-        env["LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
+        env["LIBRARY_PATH"] = os.path.join(self.bin_root(True), "lib") + \
             (os.pathsep + env["LIBRARY_PATH"]) \
             if "LIBRARY_PATH" in env else ""
-        env["RUSTFLAGS"] = "-Cdebuginfo=2 "
+        # preserve existing RUSTFLAGS
+        env.setdefault("RUSTFLAGS", "")
+        env["RUSTFLAGS"] += " -Cdebuginfo=2"
 
-        build_section = "target.{}".format(self.build_triple())
+        build_section = "target.{}".format(self.build)
         target_features = []
         if self.get_toml("crt-static", build_section) == "true":
             target_features += ["+crt-static"]
         elif self.get_toml("crt-static", build_section) == "false":
             target_features += ["-crt-static"]
         if target_features:
-            env["RUSTFLAGS"] += "-C target-feature=" + (",".join(target_features)) + " "
-
-        env["PATH"] = os.path.join(self.bin_root(), "bin") + \
+            env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
+        target_linker = self.get_toml("linker", build_section)
+        if target_linker is not None:
+            env["RUSTFLAGS"] += " -C linker=" + target_linker
+        env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
+        env["RUSTFLAGS"] += " -Wsemicolon_in_expressions_from_macros"
+        if self.get_toml("deny-warnings", "rust") != "false":
+            env["RUSTFLAGS"] += " -Dwarnings"
+
+        env["PATH"] = os.path.join(self.bin_root(True), "bin") + \
             os.pathsep + env["PATH"]
         if not os.path.isfile(self.cargo()):
             raise Exception("no cargo executable found at `{}`".format(
@@ -649,11 +936,15 @@ class RustBuild(object):
         run(args, env=env, verbose=self.verbose)
 
     def build_triple(self):
-        """Build triple as in LLVM"""
+        """Build triple as in LLVM
+
+        Note that `default_build_triple` is moderately expensive,
+        so use `self.build` where possible.
+        """
         config = self.get_toml('build')
         if config:
             return config
-        return default_build_triple()
+        return default_build_triple(self.verbose)
 
     def check_submodule(self, module, slow_submodules):
         if not slow_submodules:
@@ -667,7 +958,7 @@ class RustBuild(object):
     def update_submodule(self, module, checked_out, recorded_submodules):
         module_path = os.path.join(self.rust_root, module)
 
-        if checked_out != None:
+        if checked_out is not None:
             default_encoding = sys.getdefaultencoding()
             checked_out = checked_out.communicate()[0].decode(default_encoding).strip()
             if recorded_submodules[module] == checked_out:
@@ -677,9 +968,13 @@ class RustBuild(object):
 
         run(["git", "submodule", "-q", "sync", module],
             cwd=self.rust_root, verbose=self.verbose)
-        run(["git", "submodule", "update",
-            "--init", "--recursive", module],
-            cwd=self.rust_root, verbose=self.verbose)
+
+        update_args = ["git", "submodule", "update", "--init", "--recursive"]
+        if self.git_version >= distutils.version.LooseVersion("2.11.0"):
+            update_args.append("--progress")
+        update_args.append(module)
+        run(update_args, cwd=self.rust_root, verbose=self.verbose, exception=True)
+
         run(["git", "reset", "-q", "--hard"],
             cwd=module_path, verbose=self.verbose)
         run(["git", "clean", "-qdfx"],
@@ -690,58 +985,114 @@ class RustBuild(object):
         if (not os.path.exists(os.path.join(self.rust_root, ".git"))) or \
                 self.get_toml('submodules') == "false":
             return
+
+        default_encoding = sys.getdefaultencoding()
+
+        # check the existence and version of 'git' command
+        git_version_str = require(['git', '--version']).split()[2].decode(default_encoding)
+        self.git_version = distutils.version.LooseVersion(git_version_str)
+
         slow_submodules = self.get_toml('fast-submodules') == "false"
         start_time = time()
         if slow_submodules:
-            print('Unconditionally updating all submodules')
+            print('Unconditionally updating submodules')
         else:
             print('Updating only changed submodules')
         default_encoding = sys.getdefaultencoding()
-        submodules = [s.split(' ', 1)[1] for s in subprocess.check_output(
-            ["git", "config", "--file",
-             os.path.join(self.rust_root, ".gitmodules"),
-             "--get-regexp", "path"]
-        ).decode(default_encoding).splitlines()]
+        # Only update submodules that are needed to build bootstrap.  These are needed because Cargo
+        # currently requires everything in a workspace to be "locally present" when starting a
+        # build, and will give a hard error if any Cargo.toml files are missing.
+        # FIXME: Is there a way to avoid cloning these eagerly? Bootstrap itself doesn't need to
+        #   share a workspace with any tools - maybe it could be excluded from the workspace?
+        #   That will still require cloning the submodules the second you check the standard
+        #   library, though...
+        # FIXME: Is there a way to avoid hard-coding the submodules required?
+        # WARNING: keep this in sync with the submodules hard-coded in bootstrap/lib.rs
+        submodules = [
+            "src/tools/rust-installer",
+            "src/tools/cargo",
+            "src/tools/rls",
+            "src/tools/miri",
+            "library/backtrace",
+            "library/stdarch"
+        ]
         filtered_submodules = []
         submodules_names = []
         for module in submodules:
-            if module.endswith("llvm"):
-                if self.get_toml('llvm-config'):
-                    continue
-            if module.endswith("llvm-emscripten"):
-                backends = self.get_toml('codegen-backends')
-                if backends is None or not 'emscripten' in backends:
-                    continue
-            if module.endswith("jemalloc"):
-                if self.get_toml('use-jemalloc') == 'false':
-                    continue
-                if self.get_toml('jemalloc'):
-                    continue
-            if module.endswith("lld"):
-                config = self.get_toml('lld')
-                if config is None or config == 'false':
-                    continue
-            if module.endswith("lldb") or module.endswith("clang"):
-                config = self.get_toml('lldb')
-                if config is None or config == 'false':
-                    continue
             check = self.check_submodule(module, slow_submodules)
             filtered_submodules.append((module, check))
             submodules_names.append(module)
         recorded = subprocess.Popen(["git", "ls-tree", "HEAD"] + submodules_names,
                                     cwd=self.rust_root, stdout=subprocess.PIPE)
         recorded = recorded.communicate()[0].decode(default_encoding).strip().splitlines()
+        # { filename: hash }
         recorded_submodules = {}
         for data in recorded:
+            # [mode, kind, hash, filename]
             data = data.split()
             recorded_submodules[data[3]] = data[2]
         for module in filtered_submodules:
             self.update_submodule(module[0], module[1], recorded_submodules)
         print("Submodules updated in %.2f seconds" % (time() - start_time))
 
+    def set_normal_environment(self):
+        """Set download URL for normal environment"""
+        if 'RUSTUP_DIST_SERVER' in os.environ:
+            self._download_url = os.environ['RUSTUP_DIST_SERVER']
+        else:
+            self._download_url = 'https://static.rust-lang.org'
+
     def set_dev_environment(self):
         """Set download URL for development environment"""
-        self._download_url = 'https://dev-static.rust-lang.org'
+        if 'RUSTUP_DEV_DIST_SERVER' in os.environ:
+            self._download_url = os.environ['RUSTUP_DEV_DIST_SERVER']
+        else:
+            self._download_url = 'https://dev-static.rust-lang.org'
+
+    def check_vendored_status(self):
+        """Check that vendoring is configured properly"""
+        vendor_dir = os.path.join(self.rust_root, 'vendor')
+        if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
+            if os.environ.get('USER') != os.environ['SUDO_USER']:
+                self.use_vendored_sources = True
+                print('info: looks like you are running this command under `sudo`')
+                print('      and so in order to preserve your $HOME this will now')
+                print('      use vendored sources by default.')
+                if not os.path.exists(vendor_dir):
+                    print('error: vendoring required, but vendor directory does not exist.')
+                    print('       Run `cargo vendor` without sudo to initialize the '
+                          'vendor directory.')
+                    raise Exception("{} not found".format(vendor_dir))
+
+        if self.use_vendored_sources:
+            if not os.path.exists('.cargo'):
+                os.makedirs('.cargo')
+            with output('.cargo/config') as cargo_config:
+                cargo_config.write(
+                    "[source.crates-io]\n"
+                    "replace-with = 'vendored-sources'\n"
+                    "registry = 'https://example.com'\n"
+                    "\n"
+                    "[source.vendored-sources]\n"
+                    "directory = '{}/vendor'\n"
+                    .format(self.rust_root))
+        else:
+            if os.path.exists('.cargo'):
+                shutil.rmtree('.cargo')
+
+    def ensure_vendored(self):
+        """Ensure that the vendored sources are available if needed"""
+        vendor_dir = os.path.join(self.rust_root, 'vendor')
+        # Note that this does not handle updating the vendored dependencies if
+        # the rust git repository is updated. Normal development usually does
+        # not use vendoring, so hopefully this isn't too much of a problem.
+        if self.use_vendored_sources and not os.path.exists(vendor_dir):
+            run([
+                self.cargo(),
+                "vendor",
+                "--sync=./src/tools/rust-analyzer/Cargo.toml",
+                "--sync=./compiler/rustc_codegen_cranelift/Cargo.toml",
+            ], verbose=self.verbose, cwd=self.rust_root)
 
 
 def bootstrap(help_triggered):
@@ -757,7 +1108,6 @@ def bootstrap(help_triggered):
     parser = argparse.ArgumentParser(description='Build rust')
     parser.add_argument('--config')
     parser.add_argument('--build')
-    parser.add_argument('--src')
     parser.add_argument('--clean', action='store_true')
     parser.add_argument('-v', '--verbose', action='count', default=0)
 
@@ -766,63 +1116,67 @@ def bootstrap(help_triggered):
 
     # Configure initial bootstrap
     build = RustBuild()
-    build.rust_root = args.src or os.path.abspath(os.path.join(__file__, '../../..'))
+    build.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
     build.verbose = args.verbose
     build.clean = args.clean
 
-    try:
-        with open(args.config or 'config.toml') as config:
+    # Read from `RUST_BOOTSTRAP_CONFIG`, then `--config`, then fallback to `config.toml` (if it
+    # exists).
+    toml_path = os.getenv('RUST_BOOTSTRAP_CONFIG') or args.config
+    if not toml_path and os.path.exists('config.toml'):
+        toml_path = 'config.toml'
+
+    if toml_path:
+        if not os.path.exists(toml_path):
+            toml_path = os.path.join(build.rust_root, toml_path)
+
+        with open(toml_path) as config:
             build.config_toml = config.read()
-    except (OSError, IOError):
-        pass
 
-    match = re.search(r'\nverbose = (\d+)', build.config_toml)
-    if match is not None:
-        build.verbose = max(build.verbose, int(match.group(1)))
-
-    build.use_vendored_sources = '\nvendor = true' in build.config_toml
-
-    build.use_locked_deps = '\nlocked-deps = true' in build.config_toml
-
-    if 'SUDO_USER' in os.environ and not build.use_vendored_sources:
-        if os.environ.get('USER') != os.environ['SUDO_USER']:
-            build.use_vendored_sources = True
-            print('info: looks like you are running this command under `sudo`')
-            print('      and so in order to preserve your $HOME this will now')
-            print('      use vendored sources by default. Note that if this')
-            print('      does not work you should run a normal build first')
-            print('      before running a command like `sudo ./x.py install`')
-
-    if build.use_vendored_sources:
-        if not os.path.exists('.cargo'):
-            os.makedirs('.cargo')
-        with output('.cargo/config') as cargo_config:
-            cargo_config.write("""
-                [source.crates-io]
-                replace-with = 'vendored-sources'
-                registry = 'https://example.com'
-
-                [source.vendored-sources]
-                directory = '{}/src/vendor'
-            """.format(build.rust_root))
-    else:
-        if os.path.exists('.cargo'):
-            shutil.rmtree('.cargo')
+    profile = build.get_toml('profile')
+    if profile is not None:
+        include_file = 'config.{}.toml'.format(profile)
+        include_dir = os.path.join(build.rust_root, 'src', 'bootstrap', 'defaults')
+        include_path = os.path.join(include_dir, include_file)
+        # HACK: This works because `build.get_toml()` returns the first match it finds for a
+        # specific key, so appending our defaults at the end allows the user to override them
+        with open(include_path) as included_toml:
+            build.config_toml += os.linesep + included_toml.read()
+
+    config_verbose = build.get_toml('verbose', 'build')
+    if config_verbose is not None:
+        build.verbose = max(build.verbose, int(config_verbose))
+
+    build.use_vendored_sources = build.get_toml('vendor', 'build') == 'true'
+
+    build.use_locked_deps = build.get_toml('locked-deps', 'build') == 'true'
+
+    build.check_vendored_status()
+
+    build_dir = build.get_toml('build-dir', 'build') or 'build'
+    build.build_dir = os.path.abspath(build_dir.replace("$ROOT", build.rust_root))
 
     data = stage0_data(build.rust_root)
     build.date = data['date']
     build.rustc_channel = data['rustc']
-    build.cargo_channel = data['cargo']
+
+    if "rustfmt" in data:
+        build.rustfmt_channel = data['rustfmt']
 
     if 'dev' in data:
         build.set_dev_environment()
+    else:
+        build.set_normal_environment()
 
+    build.build = args.build or build.build_triple()
     build.update_submodules()
 
     # Fetch/build the bootstrap
-    build.build = args.build or build.build_triple()
-    build.download_stage0()
+    build.download_toolchain()
+    # Download the master compiler if `download-rustc` is set
+    build.maybe_download_ci_toolchain()
     sys.stdout.flush()
+    build.ensure_vendored()
     build.build_bootstrap()
     sys.stdout.flush()
 
@@ -830,20 +1184,25 @@ def bootstrap(help_triggered):
     args = [build.bootstrap_binary()]
     args.extend(sys.argv[1:])
     env = os.environ.copy()
-    env["BUILD"] = build.build
-    env["SRC"] = build.rust_root
     env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
     env["BOOTSTRAP_PYTHON"] = sys.executable
     env["BUILD_DIR"] = build.build_dir
     env["RUSTC_BOOTSTRAP"] = '1'
-    env["CARGO"] = build.cargo()
-    env["RUSTC"] = build.rustc()
-    run(args, env=env, verbose=build.verbose)
+    if toml_path:
+        env["BOOTSTRAP_CONFIG"] = toml_path
+    if build.rustc_commit is not None:
+        env["BOOTSTRAP_DOWNLOAD_RUSTC"] = '1'
+    run(args, env=env, verbose=build.verbose, is_bootstrap=True)
 
 
 def main():
     """Entry point for the bootstrap process"""
     start_time = time()
+
+    # x.py help <cmd> ...
+    if len(sys.argv) > 1 and sys.argv[1] == 'help':
+        sys.argv = [sys.argv[0], '-h'] + sys.argv[2:]
+
     help_triggered = (
         '-h' in sys.argv) or ('--help' in sys.argv) or (len(sys.argv) == 1)
     try: