bootstrap.py 42.2 KB
Newer Older
1
from __future__ import absolute_import, division, print_function
A
Alex Crichton 已提交
2 3
import argparse
import contextlib
4
import datetime
5
import hashlib
6
import json
A
Alex Crichton 已提交
7
import os
8
import re
A
Alex Crichton 已提交
9 10 11 12
import shutil
import subprocess
import sys
import tarfile
13 14
import tempfile

K
KaDiWa 已提交
15
from time import time
N
Nilstrieb 已提交
16
from multiprocessing import Pool, cpu_count
17

K
KaDiWa 已提交
18 19 20 21 22
try:
    import lzma
except ImportError:
    lzma = None

23 24 25 26
def platform_is_win32():
    return sys.platform == 'win32'

if platform_is_win32():
K
KaDiWa 已提交
27 28 29
    EXE_SUFFIX = ".exe"
else:
    EXE_SUFFIX = ""
30

31 32 33 34 35 36 37 38 39 40 41 42 43 44
def get_cpus():
    if hasattr(os, "sched_getaffinity"):
        return len(os.sched_getaffinity(0))
    if hasattr(os, "cpu_count"):
        cpus = os.cpu_count()
        if cpus is not None:
            return cpus
    try:
        return cpu_count()
    except NotImplementedError:
        return 1



45
def get(base, url, path, checksums, verbose=False):
46 47 48 49
    with tempfile.NamedTemporaryFile(delete=False) as temp_file:
        temp_path = temp_file.name

    try:
50 51 52 53 54 55 56 57 58 59
        if url not in checksums:
            raise RuntimeError(("src/stage0.json doesn't contain a checksum for {}. "
                                "Pre-built artifacts might not be available for this "
                                "target at this time, see https://doc.rust-lang.org/nightly"
                                "/rustc/platform-support.html for more information.")
                                .format(url))
        sha256 = checksums[url]
        if os.path.exists(path):
            if verify(path, sha256, False):
                if verbose:
60
                    print("using already-download file", path, file=sys.stderr)
61 62 63 64
                return
            else:
                if verbose:
                    print("ignoring already-download file",
65
                        path, "due to failed verification", file=sys.stderr)
66
                os.unlink(path)
67
        download(temp_path, "{}/{}".format(base, url), True, verbose)
68
        if not verify(temp_path, sha256, verbose):
69
            raise RuntimeError("failed verification")
70
        if verbose:
71
            print("moving {} to {}".format(temp_path, path), file=sys.stderr)
72 73
        shutil.move(temp_path, path)
    finally:
74 75
        if os.path.isfile(temp_path):
            if verbose:
76
                print("removing", temp_path, file=sys.stderr)
77
            os.unlink(temp_path)
78 79


80
def download(path, url, probably_big, verbose):
K
KaDiWa 已提交
81
    for _ in range(4):
82
        try:
83
            _download(path, url, probably_big, verbose, True)
84 85
            return
        except RuntimeError:
86
            print("\nspurious failure, trying again", file=sys.stderr)
87
    _download(path, url, probably_big, verbose, False)
88 89


90
def _download(path, url, probably_big, verbose, exception):
T
Thomas Applencourt 已提交
91 92 93 94 95
    # Try to use curl (potentially available on win32
    #    https://devblogs.microsoft.com/commandline/tar-and-curl-come-to-windows/)
    # If an error occurs:
    #  - If we are on win32 fallback to powershell
    #  - Otherwise raise the error if appropriate
96
    if probably_big or verbose:
97
        print("downloading {}".format(url), file=sys.stderr)
T
Thomas Applencourt 已提交
98 99

    try:
J
jyn 已提交
100
        if (probably_big or verbose) and "GITHUB_ACTIONS" not in os.environ:
101 102 103
            option = "-#"
        else:
            option = "-s"
D
Dezhi Wu 已提交
104
        # If curl is not present on Win32, we should not sys.exit
T
Thomas Applencourt 已提交
105
        #   but raise `CalledProcessError` or `OSError` instead
106
        require(["curl", "--version"], exception=platform_is_win32())
107 108 109 110 111
        with open(path, "wb") as outfile:
            run(["curl", option,
                "-L", # Follow redirect.
                "-y", "30", "-Y", "10",    # timeout if speed is < 10 bytes/sec for > 30 seconds
                "--connect-timeout", "30",  # timeout if cannot connect within 30 seconds
112
                "--retry", "3", "-SRf", url],
113 114 115 116
                stdout=outfile,    #Implements cli redirect operator '>'
                verbose=verbose,
                exception=True, # Will raise RuntimeError on failure
            )
T
Thomas Applencourt 已提交
117 118
    except (subprocess.CalledProcessError, OSError, RuntimeError):
        # see http://serverfault.com/questions/301128/how-to-download
119 120
        if platform_is_win32():
            run_powershell([
T
Thomas Applencourt 已提交
121 122 123 124 125 126 127
                 "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
                 "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')".format(url, path)],
                verbose=verbose,
                exception=exception)
        # Check if the RuntimeError raised by run(curl) should be silenced
        elif verbose or exception:
            raise
128 129


130
def verify(path, expected, verbose):
131
    """Check if the sha256 sum of the given path is valid"""
132
    if verbose:
133
        print("verifying", path, file=sys.stderr)
134 135
    with open(path, "rb") as source:
        found = hashlib.sha256(source.read()).hexdigest()
136
    verified = found == expected
137
    if not verified:
138
        print("invalid checksum:\n"
139
              "    found:    {}\n"
140
              "    expected: {}".format(found, expected), file=sys.stderr)
141
    return verified
A
Alex Crichton 已提交
142

143

G
Guanqun Lu 已提交
144
def unpack(tarball, tarball_suffix, dst, verbose=False, match=None):
145
    """Unpack the given tarball file"""
146
    print("extracting", tarball, file=sys.stderr)
G
Guanqun Lu 已提交
147
    fname = os.path.basename(tarball).replace(tarball_suffix, "")
A
Alex Crichton 已提交
148
    with contextlib.closing(tarfile.open(tarball)) as tar:
M
Milton Mazzarri 已提交
149 150
        for member in tar.getnames():
            if "/" not in member:
A
Alex Crichton 已提交
151
                continue
M
Milton Mazzarri 已提交
152
            name = member.replace(fname + "/", "", 1)
A
Alex Crichton 已提交
153 154 155 156
            if match is not None and not name.startswith(match):
                continue
            name = name[len(match) + 1:]

M
Milton Mazzarri 已提交
157
            dst_path = os.path.join(dst, name)
A
Alex Crichton 已提交
158
            if verbose:
159
                print("  extracting", member, file=sys.stderr)
M
Milton Mazzarri 已提交
160 161 162
            tar.extract(member, dst)
            src_path = os.path.join(dst, member)
            if os.path.isdir(src_path) and os.path.exists(dst_path):
A
Alex Crichton 已提交
163
                continue
M
Milton Mazzarri 已提交
164
            shutil.move(src_path, dst_path)
A
Alex Crichton 已提交
165 166
    shutil.rmtree(os.path.join(dst, fname))

167

168
def run(args, verbose=False, exception=False, is_bootstrap=False, **kwargs):
M
Milton Mazzarri 已提交
169
    """Run a child program in a new process"""
A
Alex Crichton 已提交
170
    if verbose:
171
        print("running: " + ' '.join(args), file=sys.stderr)
A
Alex Crichton 已提交
172
    sys.stdout.flush()
173
    # Ensure that the .exe is used on Windows just in case a Linux ELF has been
174 175 176
    # compiled in the same directory.
    if os.name == 'nt' and not args[0].endswith('.exe'):
        args[0] += '.exe'
A
Alex Crichton 已提交
177 178
    # Use Popen here instead of call() as it apparently allows powershell on
    # Windows to not lock up waiting for input presumably.
179
    ret = subprocess.Popen(args, **kwargs)
A
Alex Crichton 已提交
180 181
    code = ret.wait()
    if code != 0:
182
        err = "failed to run: " + ' '.join(args)
183
        if verbose or exception:
184
            raise RuntimeError(err)
185 186 187 188 189 190 191 192
        # 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)
A
Alex Crichton 已提交
193

194 195 196 197
def run_powershell(script, *args, **kwargs):
    """Run a powershell script"""
    run(["PowerShell.exe", "/nologo", "-Command"] + script, *args, **kwargs)

198

T
Thomas Applencourt 已提交
199
def require(cmd, exit=True, exception=False):
200 201
    '''Run a command, returning its output.
    On error,
T
Thomas Applencourt 已提交
202 203 204
        If `exception` is `True`, raise the error
        Otherwise If `exit` is `True`, exit the process
        Else return None.'''
205 206 207
    try:
        return subprocess.check_output(cmd).strip()
    except (subprocess.CalledProcessError, OSError) as exc:
T
Thomas Applencourt 已提交
208 209 210
        if exception:
            raise
        elif exit:
211 212
            print("error: unable to run `{}`: {}".format(' '.join(cmd), exc), file=sys.stderr)
            print("Please make sure it's installed and in the path.", file=sys.stderr)
T
Thomas Applencourt 已提交
213 214 215
            sys.exit(1)
        return None

216 217


218
def format_build_time(duration):
M
Milton Mazzarri 已提交
219 220 221 222 223
    """Return a nicer format for build time

    >>> format_build_time('300')
    '0:05:00'
    """
224 225
    return str(datetime.timedelta(seconds=int(duration)))

E
Eitan Adler 已提交
226

227
def default_build_triple(verbose):
228
    """Build triple as in LLVM"""
229 230 231 232
    # If we're on Windows and have an existing `rustc` toolchain, use `rustc --version --verbose`
    # to find our host target triple. This fixes an issue with Windows builds being detected
    # as GNU instead of MSVC.
    # Otherwise, detect it via `uname`
233
    default_encoding = sys.getdefaultencoding()
234

235
    if platform_is_win32():
236 237 238 239 240 241 242
        try:
            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:
243 244
                print("detected default triple {} from pre-installed rustc".format(triple),
                    file=sys.stderr)
245 246 247
            return triple
        except Exception as e:
            if verbose:
248 249 250
                print("pre-installed rustc not detected: {}".format(e),
                    file=sys.stderr)
                print("falling back to auto-detect", file=sys.stderr)
251

252
    required = not platform_is_win32()
253
    uname = require(["uname", "-smp"], exit=required)
254

255
    # If we do not have `uname`, assume Windows.
256
    if uname is None:
257
        return 'x86_64-pc-windows-msvc'
258

E
Edgar Luque 已提交
259
    kernel, cputype, processor = uname.decode(default_encoding).split(maxsplit=2)
J
Joshua Cotton 已提交
260

261 262
    # 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.
263
    kerneltype_mapper = {
264 265 266 267 268 269 270 271 272
        'Darwin': 'apple-darwin',
        'DragonFly': 'unknown-dragonfly',
        'FreeBSD': 'unknown-freebsd',
        'Haiku': 'unknown-haiku',
        'NetBSD': 'unknown-netbsd',
        'OpenBSD': 'unknown-openbsd'
    }

    # Consider the direct transformation first and then the special cases
273 274 275 276 277 278 279 280
    if kernel in kerneltype_mapper:
        kernel = kerneltype_mapper[kernel]
    elif kernel == 'Linux':
        # Apple doesn't support `-o` so this can't be used in the combined
        # uname invocation above
        ostype = require(["uname", "-o"], exit=required).decode(default_encoding)
        if ostype == 'Android':
            kernel = 'linux-android'
281
        else:
282 283 284
            kernel = 'unknown-linux-gnu'
    elif kernel == 'SunOS':
        kernel = 'pc-solaris'
285 286 287 288 289
        # 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.
290
        cputype = require(['isainfo', '-k']).decode(default_encoding)
291 292
        # sparc cpus have sun as a target vendor
        if 'sparc' in cputype:
293 294
            kernel = 'sun-solaris'
    elif kernel.startswith('MINGW'):
295 296 297 298 299
        # msys' `uname` does not print gcc configuration, but prints msys
        # configuration. so we cannot believe `uname -m`:
        # msys1 is always i686 and msys2 is always x86_64.
        # instead, msys defines $MSYSTEM which is MINGW32 on i686 and
        # MINGW64 on x86_64.
300
        kernel = 'pc-windows-gnu'
301 302 303
        cputype = 'i686'
        if os.environ.get('MSYSTEM') == 'MINGW64':
            cputype = 'x86_64'
304 305 306
    elif kernel.startswith('MSYS'):
        kernel = 'pc-windows-gnu'
    elif kernel.startswith('CYGWIN_NT'):
307
        cputype = 'i686'
308
        if kernel.endswith('WOW64'):
309
            cputype = 'x86_64'
310 311
        kernel = 'pc-windows-gnu'
    elif platform_is_win32():
312 313 314 315
        # 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'
316
    else:
317
        err = "unknown OS type: {}".format(kernel)
318 319
        sys.exit(err)

320
    if cputype in ['powerpc', 'riscv'] and kernel == 'unknown-freebsd':
L
lenoil98 已提交
321 322
        cputype = subprocess.check_output(
              ['uname', '-p']).strip().decode(default_encoding)
323 324 325
    cputype_mapper = {
        'BePC': 'i686',
        'aarch64': 'aarch64',
326
        'aarch64eb': 'aarch64',
327 328 329 330 331 332
        'amd64': 'x86_64',
        'arm64': 'aarch64',
        'i386': 'i686',
        'i486': 'i686',
        'i686': 'i686',
        'i786': 'i686',
333
        'loongarch64': 'loongarch64',
334
        'm68k': 'm68k',
335 336 337 338 339 340
        'powerpc': 'powerpc',
        'powerpc64': 'powerpc64',
        'powerpc64le': 'powerpc64le',
        'ppc': 'powerpc',
        'ppc64': 'powerpc64',
        'ppc64le': 'powerpc64le',
341
        'riscv64': 'riscv64gc',
342 343 344 345 346 347 348 349 350 351 352 353
        's390x': 's390x',
        'x64': 'x86_64',
        'x86': 'i686',
        'x86-64': 'x86_64',
        'x86_64': 'x86_64'
    }

    # Consider the direct transformation first and then the special cases
    if cputype in cputype_mapper:
        cputype = cputype_mapper[cputype]
    elif cputype in {'xscale', 'arm'}:
        cputype = 'arm'
354 355 356 357 358
        if kernel == 'linux-android':
            kernel = 'linux-androideabi'
        elif kernel == 'unknown-freebsd':
            cputype = processor
            kernel = 'unknown-freebsd'
359 360
    elif cputype == 'armv6l':
        cputype = 'arm'
361 362
        if kernel == 'linux-android':
            kernel = 'linux-androideabi'
363
        else:
364
            kernel += 'eabihf'
365 366
    elif cputype in {'armv7l', 'armv8l'}:
        cputype = 'armv7'
367 368
        if kernel == 'linux-android':
            kernel = 'linux-androideabi'
369
        else:
370
            kernel += 'eabihf'
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
    elif cputype == 'mips':
        if sys.byteorder == 'big':
            cputype = 'mips'
        elif sys.byteorder == 'little':
            cputype = 'mipsel'
        else:
            raise ValueError("unknown byteorder: {}".format(sys.byteorder))
    elif cputype == 'mips64':
        if sys.byteorder == 'big':
            cputype = 'mips64'
        elif sys.byteorder == 'little':
            cputype = 'mips64el'
        else:
            raise ValueError('unknown byteorder: {}'.format(sys.byteorder))
        # only the n64 ABI is supported, indicate it
386
        kernel += 'abi64'
387
    elif cputype == 'sparc' or cputype == 'sparcv9' or cputype == 'sparc64':
388 389 390 391 392
        pass
    else:
        err = "unknown cpu type: {}".format(cputype)
        sys.exit(err)

393
    return "{}-{}".format(cputype, kernel)
394

395

396 397 398 399 400
@contextlib.contextmanager
def output(filepath):
    tmp = filepath + '.tmp'
    with open(tmp, 'w') as f:
        yield f
401
    try:
402 403
        if os.path.exists(filepath):
            os.remove(filepath)  # PermissionError/OSError on Win32 if in use
404 405 406
    except OSError:
        shutil.copy2(tmp, filepath)
        os.remove(tmp)
407 408
        return
    os.rename(tmp, filepath)
409 410


411 412 413 414 415 416 417 418 419
class Stage0Toolchain:
    def __init__(self, stage0_payload):
        self.date = stage0_payload["date"]
        self.version = stage0_payload["version"]

    def channel(self):
        return self.version + "-" + self.date


N
Nilstrieb 已提交
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
class DownloadInfo:
    """A helper class that can be pickled into a parallel subprocess"""

    def __init__(
        self,
        base_download_url,
        download_path,
        bin_root,
        tarball_path,
        tarball_suffix,
        checksums_sha256,
        pattern,
        verbose,
    ):
        self.base_download_url = base_download_url
        self.download_path = download_path
        self.bin_root = bin_root
        self.tarball_path = tarball_path
        self.tarball_suffix = tarball_suffix
        self.checksums_sha256 = checksums_sha256
        self.pattern = pattern
        self.verbose = verbose

def download_component(download_info):
    if not os.path.exists(download_info.tarball_path):
        get(
            download_info.base_download_url,
            download_info.download_path,
            download_info.tarball_path,
            download_info.checksums_sha256,
            verbose=download_info.verbose,
        )

def unpack_component(download_info):
    unpack(
        download_info.tarball_path,
        download_info.tarball_suffix,
        download_info.bin_root,
        match=download_info.pattern,
        verbose=download_info.verbose,
    )

462 463
class FakeArgs:
    """Used for unit tests to avoid updating all call sites"""
M
Milton Mazzarri 已提交
464 465
    def __init__(self):
        self.build = ''
466
        self.build_dir = ''
M
Milton Mazzarri 已提交
467
        self.clean = False
468
        self.verbose = False
469
        self.json_output = False
J
jyn 已提交
470 471
        self.color = 'auto'
        self.warnings = 'default'
472 473 474

class RustBuild(object):
    """Provide all the methods required to build Rust"""
T
Trevor Gross 已提交
475 476 477
    def __init__(self, config_toml="", args=None):
        if args is None:
            args = FakeArgs()
478
        self.git_version = None
479
        self.nix_deps_dir = None
480
        self._should_fix_bins_and_dylibs = None
481 482 483 484 485 486
        self.rust_root = os.path.abspath(os.path.join(__file__, '../../..'))

        self.config_toml = config_toml

        self.clean = args.clean
        self.json_output = args.json_output
J
jyn 已提交
487 488 489
        self.verbose = args.verbose
        self.color = args.color
        self.warnings = args.warnings
490

J
jyn 已提交
491 492 493 494
        config_verbose_count = self.get_toml('verbose', 'build')
        if config_verbose_count is not None:
            self.verbose = max(self.verbose, int(config_verbose_count))

495 496 497 498 499 500 501 502 503 504 505 506 507
        self.use_vendored_sources = self.get_toml('vendor', 'build') == 'true'
        self.use_locked_deps = self.get_toml('locked-deps', 'build') == 'true'

        build_dir = args.build_dir or self.get_toml('build-dir', 'build') or 'build'
        self.build_dir = os.path.abspath(build_dir)

        with open(os.path.join(self.rust_root, "src", "stage0.json")) as f:
            data = json.load(f)
        self.checksums_sha256 = data["checksums_sha256"]
        self.stage0_compiler = Stage0Toolchain(data["compiler"])
        self.download_url = os.getenv("RUSTUP_DIST_SERVER") or data["config"]["dist_server"]

        self.build = args.build or self.build_triple()
508

J
jyn 已提交
509

510
    def download_toolchain(self):
M
Milton Mazzarri 已提交
511 512 513 514 515
        """Fetch the build system for Rust, written in Rust

        This method will build a cache directory, then it will fetch the
        tarball which has the stage0 compiler used to then bootstrap the Rust
        compiler itself.
A
Alex Crichton 已提交
516

M
Milton Mazzarri 已提交
517 518 519
        Each downloaded tarball is extracted, after that, the script
        will move all the content to the right place.
        """
520
        rustc_channel = self.stage0_compiler.version
521
        bin_root = self.bin_root()
522

523
        key = self.stage0_compiler.date
524 525 526
        if self.rustc().startswith(bin_root) and \
                (not os.path.exists(self.rustc()) or
                 self.program_out_of_date(self.rustc_stamp(), key)):
527
            if os.path.exists(bin_root):
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544
                # HACK: On Windows, we can't delete rust-analyzer-proc-macro-server while it's
                # running. Kill it.
                if platform_is_win32():
                    print("Killing rust-analyzer-proc-macro-srv before deleting stage0 toolchain")
                    regex =  '{}\\\\(host|{})\\\\stage0\\\\libexec'.format(
                        os.path.basename(self.build_dir),
                        self.build
                    )
                    script = (
                        # NOTE: can't use `taskkill` or `Get-Process -Name` because they error if
                        # the server isn't running.
                        'Get-Process | ' +
                        'Where-Object {$_.Name -eq "rust-analyzer-proc-macro-srv"} |' +
                        'Where-Object {{$_.Path -match "{}"}} |'.format(regex) +
                        'Stop-Process'
                    )
                    run_powershell([script])
545
                shutil.rmtree(bin_root)
546 547 548 549 550 551 552

            key = self.stage0_compiler.date
            cache_dst = os.path.join(self.build_dir, "cache")
            rustc_cache = os.path.join(cache_dst, key)
            if not os.path.exists(rustc_cache):
                os.makedirs(rustc_cache)

K
KaDiWa 已提交
553
            tarball_suffix = '.tar.gz' if lzma is None else '.tar.xz'
554

N
Nilstrieb 已提交
555 556
            toolchain_suffix = "{}-{}{}".format(rustc_channel, self.build, tarball_suffix)

557
            tarballs_to_download = [
N
Nilstrieb 已提交
558 559 560
                ("rust-std-{}".format(toolchain_suffix), "rust-std-{}".format(self.build)),
                ("rustc-{}".format(toolchain_suffix), "rustc"),
                ("cargo-{}".format(toolchain_suffix), "cargo"),
561 562
            ]

N
Nilstrieb 已提交
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582
            tarballs_download_info = [
                DownloadInfo(
                    base_download_url=self.download_url,
                    download_path="dist/{}/{}".format(self.stage0_compiler.date, filename),
                    bin_root=self.bin_root(),
                    tarball_path=os.path.join(rustc_cache, filename),
                    tarball_suffix=tarball_suffix,
                    checksums_sha256=self.checksums_sha256,
                    pattern=pattern,
                    verbose=self.verbose,
                )
                for filename, pattern in tarballs_to_download
            ]

            # Download the components serially to show the progress bars properly.
            for download_info in tarballs_download_info:
                download_component(download_info)

            # Unpack the tarballs in parallle.
            # In Python 2.7, Pool cannot be used as a context manager.
583 584 585 586
            pool_size = min(len(tarballs_download_info), get_cpus())
            if self.verbose:
                print('Choosing a pool size of', pool_size, 'for the unpacking of the tarballs')
            p = Pool(pool_size)
N
Nilstrieb 已提交
587 588 589 590
            try:
                p.map(unpack_component, tarballs_download_info)
            finally:
                p.close()
591
            p.join()
592

K
KaDiWa 已提交
593 594 595 596 597 598 599 600 601 602 603
            if self.should_fix_bins_and_dylibs():
                self.fix_bin_or_dylib("{}/bin/cargo".format(bin_root))

                self.fix_bin_or_dylib("{}/bin/rustc".format(bin_root))
                self.fix_bin_or_dylib("{}/bin/rustdoc".format(bin_root))
                self.fix_bin_or_dylib("{}/libexec/rust-analyzer-proc-macro-srv".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))

604
            with output(self.rustc_stamp()) as rust_stamp:
605
                rust_stamp.write(key)
A
Alex Crichton 已提交
606

607
    def _download_component_helper(
608
        self, filename, pattern, tarball_suffix, rustc_cache,
609
    ):
610
        key = self.stage0_compiler.date
M
Milton Mazzarri 已提交
611 612 613

        tarball = os.path.join(rustc_cache, filename)
        if not os.path.exists(tarball):
614
            get(
K
KaDiWa 已提交
615 616
                self.download_url,
                "dist/{}/{}".format(key, filename),
617 618
                tarball,
                self.checksums_sha256,
619
                verbose=self.verbose,
620
            )
621
        unpack(tarball, tarball_suffix, self.bin_root(), match=pattern, verbose=self.verbose)
M
Milton Mazzarri 已提交
622

K
KaDiWa 已提交
623 624 625
    def should_fix_bins_and_dylibs(self):
        """Whether or not `fix_bin_or_dylib` needs to be run; can only be True
        on NixOS.
M
Milton Mazzarri 已提交
626
        """
627 628 629 630 631 632 633 634 635
        if self._should_fix_bins_and_dylibs is not None:
            return self._should_fix_bins_and_dylibs

        def get_answer():
            default_encoding = sys.getdefaultencoding()
            try:
                ostype = subprocess.check_output(
                    ['uname', '-s']).strip().decode(default_encoding)
            except subprocess.CalledProcessError:
K
KaDiWa 已提交
636
                return False
637 638 639 640
            except OSError as reason:
                if getattr(reason, 'winerror', None) is not None:
                    return False
                raise reason
A
Andrew Cann 已提交
641

642 643
            if ostype != "Linux":
                return False
A
Andrew Cann 已提交
644

645 646 647 648
            # If the user has asked binaries to be patched for Nix, then
            # don't check for NixOS or `/lib`.
            if self.get_toml("patch-binaries-for-nix", "build") == "true":
                return True
A
Andrew Cann 已提交
649

650 651 652 653
            # 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:
654
                    if not any(ln.strip() in ("ID=nixos", "ID='nixos'", 'ID="nixos"') for ln in f):
655 656 657 658 659 660 661
                        return False
            except FileNotFoundError:
                return False
            if os.path.exists("/lib"):
                return False

            return True
K
KaDiWa 已提交
662

663 664
        answer = self._should_fix_bins_and_dylibs = get_answer()
        if answer:
665
            print("info: You seem to be using Nix.", file=sys.stderr)
666
        return answer
K
KaDiWa 已提交
667 668 669 670 671 672 673 674 675 676

    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 interpreter/RPATH of ELF executables.

        Please see https://nixos.org/patchelf.html for more information
        """
677
        assert self._should_fix_bins_and_dylibs is True
678
        print("attempting to patch", fname, file=sys.stderr)
A
Andrew Cann 已提交
679

680
        # Only build `.nix-deps` once.
681 682 683 684
        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).
S
Simonas Kazlauskas 已提交
685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710
            #
            # 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:
711
                print("warning: failed to call nix-build:", reason, file=sys.stderr)
S
Simonas Kazlauskas 已提交
712
                return
713 714
            self.nix_deps_dir = nix_deps_dir

S
Simonas Kazlauskas 已提交
715 716 717 718 719 720 721 722
        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)]
723
        if not fname.endswith(".so"):
J
Josh Soref 已提交
724
            # Finally, set the correct .interp for binaries
S
Simonas Kazlauskas 已提交
725
            with open("{}/nix-support/dynamic-linker".format(nix_deps_dir)) as dynamic_linker:
726
                patchelf_args += ["--set-interpreter", dynamic_linker.read().rstrip()]
A
Andrew Cann 已提交
727 728

        try:
729
            subprocess.check_output([patchelf] + patchelf_args + [fname])
M
Milton Mazzarri 已提交
730
        except subprocess.CalledProcessError as reason:
731
            print("warning: failed to call patchelf:", reason, file=sys.stderr)
A
Andrew Cann 已提交
732 733
            return

734
    def rustc_stamp(self):
735
        """Return the path for .rustc-stamp at the given stage
M
Milton Mazzarri 已提交
736 737

        >>> rb = RustBuild()
738
        >>> rb.build = "host"
M
Milton Mazzarri 已提交
739
        >>> rb.build_dir = "build"
740 741
        >>> expected = os.path.join("build", "host", "stage0", ".rustc-stamp")
        >>> assert rb.rustc_stamp() == expected, rb.rustc_stamp()
M
Milton Mazzarri 已提交
742
        """
743
        return os.path.join(self.bin_root(), '.rustc-stamp')
A
Alex Crichton 已提交
744

745
    def program_out_of_date(self, stamp_path, key):
M
Milton Mazzarri 已提交
746 747
        """Check if the given program stamp is out of date"""
        if not os.path.exists(stamp_path) or self.clean:
A
Alex Crichton 已提交
748
            return True
M
Milton Mazzarri 已提交
749
        with open(stamp_path, 'r') as stamp:
750
            return key != stamp.read()
A
Alex Crichton 已提交
751

752
    def bin_root(self):
753
        """Return the binary root directory for the given stage
M
Milton Mazzarri 已提交
754 755 756

        >>> rb = RustBuild()
        >>> rb.build = "devel"
757 758
        >>> expected = os.path.abspath(os.path.join("build", "devel", "stage0"))
        >>> assert rb.bin_root() == expected, rb.bin_root()
M
Milton Mazzarri 已提交
759
        """
760
        subdir = "stage0"
761
        return os.path.join(self.build_dir, self.build, subdir)
A
Alex Crichton 已提交
762

763
    def get_toml(self, key, section=None):
M
Milton Mazzarri 已提交
764 765 766 767 768 769 770
        """Returns the value of the given key in config.toml, otherwise returns None

        >>> rb = RustBuild()
        >>> rb.config_toml = 'key1 = "value1"\\nkey2 = "value2"'
        >>> rb.get_toml("key2")
        'value2'

M
Maxwase 已提交
771
        If the key does not exist, the result is None:
M
Milton Mazzarri 已提交
772

773
        >>> rb.get_toml("key3") is None
M
Milton Mazzarri 已提交
774
        True
775 776 777 778 779 780 781 782 783 784

        Optionally also matches the section the key appears in

        >>> rb.config_toml = '[a]\\nkey = "value1"\\n[b]\\nkey = "value2"'
        >>> rb.get_toml('key', 'a')
        'value1'
        >>> rb.get_toml('key', 'b')
        'value2'
        >>> rb.get_toml('key', 'c') is None
        True
785 786 787 788

        >>> rb.config_toml = 'key1 = true'
        >>> rb.get_toml("key1")
        'true'
M
Milton Mazzarri 已提交
789
        """
J
jyn 已提交
790
        return RustBuild.get_toml_static(self.config_toml, key, section)
791

J
jyn 已提交
792 793
    @staticmethod
    def get_toml_static(config_toml, key, section=None):
794
        cur_section = None
J
jyn 已提交
795
        for line in config_toml.splitlines():
796 797 798 799
            section_match = re.match(r'^\s*\[(.*)\]\s*$', line)
            if section_match is not None:
                cur_section = section_match.group(1)

800 801 802
            match = re.match(r'^{}\s*=(.*)$'.format(key), line)
            if match is not None:
                value = match.group(1)
803
                if section is None or section == cur_section:
J
jyn 已提交
804
                    return RustBuild.get_string(value) or value.strip()
A
Alex Crichton 已提交
805 806 807
        return None

    def cargo(self):
M
Milton Mazzarri 已提交
808 809
        """Return config path for cargo"""
        return self.program_config('cargo')
A
Alex Crichton 已提交
810

811
    def rustc(self):
M
Milton Mazzarri 已提交
812
        """Return config path for rustc"""
813
        return self.program_config('rustc')
M
Milton Mazzarri 已提交
814

815
    def program_config(self, program):
816
        """Return config path for the given program at the given stage
M
Milton Mazzarri 已提交
817 818 819 820 821 822

        >>> rb = RustBuild()
        >>> rb.config_toml = 'rustc = "rustc"\\n'
        >>> rb.program_config('rustc')
        'rustc'
        >>> rb.config_toml = ''
823 824
        >>> cargo_path = rb.program_config('cargo')
        >>> cargo_path.rstrip(".exe") == os.path.join(rb.bin_root(),
M
Milton Mazzarri 已提交
825 826 827 828
        ... "bin", "cargo")
        True
        """
        config = self.get_toml(program)
A
Alex Crichton 已提交
829
        if config:
T
topecongiro 已提交
830
            return os.path.expanduser(config)
K
KaDiWa 已提交
831
        return os.path.join(self.bin_root(), "bin", "{}{}".format(program, EXE_SUFFIX))
M
Milton Mazzarri 已提交
832 833 834 835 836 837 838

    @staticmethod
    def get_string(line):
        """Return the value between double quotes

        >>> RustBuild.get_string('    "devel"   ')
        'devel'
839 840 841 842 843 844
        >>> RustBuild.get_string("    'devel'   ")
        'devel'
        >>> RustBuild.get_string('devel') is None
        True
        >>> RustBuild.get_string('    "devel   ')
        ''
M
Milton Mazzarri 已提交
845
        """
A
Alex Crichton 已提交
846
        start = line.find('"')
847 848 849 850 851 852 853 854
        if start != -1:
            end = start + 1 + line[start + 1:].find('"')
            return line[start + 1:end]
        start = line.find('\'')
        if start != -1:
            end = start + 1 + line[start + 1:].find('\'')
            return line[start + 1:end]
        return None
A
Alex Crichton 已提交
855

856
    def bootstrap_binary(self):
M
Matthias Krüger 已提交
857
        """Return the path of the bootstrap binary
M
Milton Mazzarri 已提交
858 859 860 861 862 863 864 865

        >>> rb = RustBuild()
        >>> rb.build_dir = "build"
        >>> rb.bootstrap_binary() == os.path.join("build", "bootstrap",
        ... "debug", "bootstrap")
        True
        """
        return os.path.join(self.build_dir, "bootstrap", "debug", "bootstrap")
866

J
jyn 已提交
867
    def build_bootstrap(self):
M
Milton Mazzarri 已提交
868
        """Build bootstrap"""
869 870 871 872
        env = os.environ.copy()
        if "GITHUB_ACTIONS" in env:
            print("::group::Building bootstrap")
        else:
873
            print("Building bootstrap", file=sys.stderr)
874

J
jyn 已提交
875
        args = self.build_bootstrap_cmd(env)
876 877 878 879 880 881
        # Run this from the source directory so cargo finds .cargo/config
        run(args, env=env, verbose=self.verbose, cwd=self.rust_root)

        if "GITHUB_ACTIONS" in env:
            print("::endgroup::")

J
jyn 已提交
882
    def build_bootstrap_cmd(self, env):
883
        """For tests."""
884 885 886
        build_dir = os.path.join(self.build_dir, "bootstrap")
        if self.clean and os.path.exists(build_dir):
            shutil.rmtree(build_dir)
887 888 889 890
        # `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"]
891
        env["CARGO_TARGET_DIR"] = build_dir
892 893
        env["RUSTC"] = self.rustc()
        env["LD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
894 895
            (os.pathsep + env["LD_LIBRARY_PATH"]) \
            if "LD_LIBRARY_PATH" in env else ""
896
        env["DYLD_LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
897 898
            (os.pathsep + env["DYLD_LIBRARY_PATH"]) \
            if "DYLD_LIBRARY_PATH" in env else ""
899
        env["LIBRARY_PATH"] = os.path.join(self.bin_root(), "lib") + \
900 901
            (os.pathsep + env["LIBRARY_PATH"]) \
            if "LIBRARY_PATH" in env else ""
K
Kai Luo 已提交
902 903 904
        env["LIBPATH"] = os.path.join(self.bin_root(), "lib") + \
            (os.pathsep + env["LIBPATH"]) \
            if "LIBPATH" in env else ""
905

906 907 908 909 910 911 912 913
        # Export Stage0 snapshot compiler related env variables
        build_section = "target.{}".format(self.build)
        host_triple_sanitized = self.build.replace("-", "_")
        var_data = {
            "CC": "cc", "CXX": "cxx", "LD": "linker", "AR": "ar", "RANLIB": "ranlib"
        }
        for var_name, toml_key in var_data.items():
            toml_val = self.get_toml(toml_key, build_section)
914
            if toml_val is not None:
915 916
                env["{}_{}".format(var_name, host_triple_sanitized)] = toml_val

917 918
        # preserve existing RUSTFLAGS
        env.setdefault("RUSTFLAGS", "")
O
ozkanonur 已提交
919

920 921 922 923 924 925
        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:
926
            env["RUSTFLAGS"] += " -C target-feature=" + (",".join(target_features))
927 928 929
        target_linker = self.get_toml("linker", build_section)
        if target_linker is not None:
            env["RUSTFLAGS"] += " -C linker=" + target_linker
930
        env["RUSTFLAGS"] += " -Wrust_2018_idioms -Wunused_lifetimes"
J
jyn 已提交
931
        if self.warnings == "default":
932 933
            deny_warnings = self.get_toml("deny-warnings", "rust") != "false"
        else:
J
jyn 已提交
934
            deny_warnings = self.warnings == "deny"
935
        if deny_warnings:
936
            env["RUSTFLAGS"] += " -Dwarnings"
937

938
        env["PATH"] = os.path.join(self.bin_root(), "bin") + \
939
            os.pathsep + env["PATH"]
940
        if not os.path.isfile(self.cargo()):
M
Milton Mazzarri 已提交
941 942
            raise Exception("no cargo executable found at `{}`".format(
                self.cargo()))
943 944
        args = [self.cargo(), "build", "--manifest-path",
                os.path.join(self.rust_root, "src/bootstrap/Cargo.toml")]
J
jyn 已提交
945
        args.extend("--verbose" for _ in range(self.verbose))
946 947
        if self.use_locked_deps:
            args.append("--locked")
948 949
        if self.use_vendored_sources:
            args.append("--frozen")
950 951 952
        if self.get_toml("metrics", "build"):
            args.append("--features")
            args.append("build-metrics")
953 954
        if self.json_output:
            args.append("--message-format=json")
J
jyn 已提交
955
        if self.color == "always":
956
            args.append("--color=always")
J
jyn 已提交
957
        elif self.color == "never":
958
            args.append("--color=never")
J
jyn 已提交
959 960 961 962
        try:
            args += env["CARGOFLAGS"].split()
        except KeyError:
            pass
963

964
        return args
965

A
Alex Crichton 已提交
966
    def build_triple(self):
967 968 969 970 971
        """Build triple as in LLVM

        Note that `default_build_triple` is moderately expensive,
        so use `self.build` where possible.
        """
A
Alex Crichton 已提交
972
        config = self.get_toml('build')
973
        return config or default_build_triple(self.verbose)
M
Milton Mazzarri 已提交
974

E
Eric Huss 已提交
975 976
    def check_vendored_status(self):
        """Check that vendoring is configured properly"""
977 978
        # keep this consistent with the equivalent check in rustbuild:
        # https://github.com/rust-lang/rust/blob/a8a33cf27166d3eabaffc58ed3799e054af3b0c6/src/bootstrap/lib.rs#L399-L405
E
Eric Huss 已提交
979
        if 'SUDO_USER' in os.environ and not self.use_vendored_sources:
980
            if os.getuid() == 0:
E
Eric Huss 已提交
981
                self.use_vendored_sources = True
982 983 984 985 986 987
                print('info: looks like you\'re trying to run this command as root',
                    file=sys.stderr)
                print('      and so in order to preserve your $HOME this will now',
                    file=sys.stderr)
                print('      use vendored sources by default.',
                    file=sys.stderr)
E
Eric Huss 已提交
988

989
        cargo_dir = os.path.join(self.rust_root, '.cargo')
E
Eric Huss 已提交
990
        if self.use_vendored_sources:
991 992
            vendor_dir = os.path.join(self.rust_root, 'vendor')
            if not os.path.exists(vendor_dir):
993 994
                sync_dirs = "--sync ./src/tools/cargo/Cargo.toml " \
                            "--sync ./src/tools/rust-analyzer/Cargo.toml " \
995 996
                            "--sync ./compiler/rustc_codegen_cranelift/Cargo.toml " \
                            "--sync ./src/bootstrap/Cargo.toml "
997 998
                print('error: vendoring required, but vendor directory does not exist.',
                    file=sys.stderr)
999
                print('       Run `cargo vendor {}` to initialize the '
1000 1001 1002 1003
                      'vendor directory.'.format(sync_dirs),
                      file=sys.stderr)
                print('Alternatively, use the pre-vendored `rustc-src` dist component.',
                    file=sys.stderr)
1004 1005 1006
                raise Exception("{} not found".format(vendor_dir))

            if not os.path.exists(cargo_dir):
1007 1008
                print('error: vendoring required, but .cargo/config does not exist.',
                    file=sys.stderr)
1009
                raise Exception("{} not found".format(cargo_dir))
E
Eric Huss 已提交
1010
        else:
1011 1012
            if os.path.exists(cargo_dir):
                shutil.rmtree(cargo_dir)
1013

1014
def parse_args(args):
K
KaDiWa 已提交
1015 1016 1017
    """Parse the command line arguments that the python script needs."""
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('-h', '--help', action='store_true')
1018
    parser.add_argument('--config')
1019
    parser.add_argument('--build-dir')
1020
    parser.add_argument('--build')
1021
    parser.add_argument('--color', choices=['always', 'never', 'auto'])
1022
    parser.add_argument('--clean', action='store_true')
1023
    parser.add_argument('--json-output', action='store_true')
1024
    parser.add_argument('--warnings', choices=['deny', 'warn', 'default'], default='default')
C
comex 已提交
1025
    parser.add_argument('-v', '--verbose', action='count', default=0)
1026

1027
    return parser.parse_known_args(args)[0]
1028

K
KaDiWa 已提交
1029 1030
def bootstrap(args):
    """Configure, fetch, build and run the initial bootstrap"""
1031
    rust_root = os.path.abspath(os.path.join(__file__, '../../..'))
1032

1033 1034
    # Read from `--config`, then `RUST_BOOTSTRAP_CONFIG`, then `./config.toml`,
    # then `config.toml` in the root directory.
1035
    toml_path = args.config or os.getenv('RUST_BOOTSTRAP_CONFIG')
1036 1037
    using_default_path = toml_path is None
    if using_default_path:
1038
        toml_path = 'config.toml'
1039
        if not os.path.exists(toml_path):
1040
            toml_path = os.path.join(rust_root, toml_path)
1041

1042 1043 1044
    # Give a hard error if `--config` or `RUST_BOOTSTRAP_CONFIG` are set to a missing path,
    # but not if `config.toml` hasn't been created.
    if not using_default_path or os.path.exists(toml_path):
1045
        with open(toml_path) as config:
1046
            config_toml = config.read()
J
jyn 已提交
1047 1048
    else:
        config_toml = ''
1049

J
jyn 已提交
1050 1051
    profile = RustBuild.get_toml_static(config_toml, 'profile')
    if profile is not None:
1052 1053 1054 1055 1056 1057 1058
        # Allows creating alias for profile names, allowing
        # profiles to be renamed while maintaining back compatibility
        # Keep in sync with `profile_aliases` in config.rs
        profile_aliases = {
            "user": "dist"
        }
        include_file = 'config.{}.toml'.format(profile_aliases.get(profile) or profile)
J
jyn 已提交
1059 1060 1061 1062 1063 1064 1065
        include_dir = os.path.join(rust_root, 'src', 'bootstrap', 'defaults')
        include_path = os.path.join(include_dir, include_file)
        # HACK: This works because `self.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:
            config_toml += os.linesep + included_toml.read()

1066 1067 1068
    # Configure initial bootstrap
    build = RustBuild(config_toml, args)
    build.check_vendored_status()
1069

W
worldeva 已提交
1070 1071
    if not os.path.exists(build.build_dir):
        os.makedirs(build.build_dir)
1072

1073
    # Fetch/build the bootstrap
1074
    build.download_toolchain()
1075
    sys.stdout.flush()
J
jyn 已提交
1076
    build.build_bootstrap()
1077 1078 1079
    sys.stdout.flush()

    # Run the bootstrap
M
Milton Mazzarri 已提交
1080
    args = [build.bootstrap_binary()]
1081 1082 1083
    args.extend(sys.argv[1:])
    env = os.environ.copy()
    env["BOOTSTRAP_PARENT_ID"] = str(os.getpid())
1084
    env["BOOTSTRAP_PYTHON"] = sys.executable
1085
    run(args, env=env, verbose=build.verbose, is_bootstrap=True)
1086

1087

1088
def main():
M
Milton Mazzarri 已提交
1089
    """Entry point for the bootstrap process"""
1090
    start_time = time()
1091 1092 1093

    # x.py help <cmd> ...
    if len(sys.argv) > 1 and sys.argv[1] == 'help':
K
KaDiWa 已提交
1094
        sys.argv[1] = '-h'
1095

1096
    args = parse_args(sys.argv)
K
KaDiWa 已提交
1097 1098 1099 1100 1101 1102 1103 1104
    help_triggered = args.help or len(sys.argv) == 1

    # If the user is asking for help, let them know that the whole download-and-build
    # process has to happen before anything is printed out.
    if help_triggered:
        print(
            "info: Downloading and building bootstrap before processing --help command.\n"
            "      See src/bootstrap/README.md for help with common commands."
1105
        , file=sys.stderr)
K
KaDiWa 已提交
1106 1107

    exit_code = 0
K
KaDiWa 已提交
1108
    success_word = "successfully"
1109
    try:
K
KaDiWa 已提交
1110
        bootstrap(args)
M
Milton Mazzarri 已提交
1111 1112 1113
    except (SystemExit, KeyboardInterrupt) as error:
        if hasattr(error, 'code') and isinstance(error.code, int):
            exit_code = error.code
1114 1115
        else:
            exit_code = 1
1116
            print(error, file=sys.stderr)
K
KaDiWa 已提交
1117
        success_word = "unsuccessfully"
K
KaDiWa 已提交
1118 1119

    if not help_triggered:
1120 1121
        print("Build completed", success_word, "in", format_build_time(time() - start_time),
            file=sys.stderr)
K
KaDiWa 已提交
1122
    sys.exit(exit_code)
1123

1124

1125 1126
if __name__ == '__main__':
    main()