build/moz.configure/rust.configure
author Ed Lee <edilee@mozilla.com>
Sun, 19 May 2019 11:04:16 +0000
changeset 474449 4a74609752d2e77e4be401e92978c9c32a842b40
parent 473796 c9f1a1db8fa2190a88c09eb624b321a07da6c3e1
permissions -rw-r--r--
Bug 1552538 - Check for gBrowser before checking for pinned tabs r=r1cky Differential Revision: https://phabricator.services.mozilla.com/D31735

# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.


# Rust is required by `rust_compiler` below. We allow_missing here
# to propagate failures to the better error message there.
js_option(env='RUSTC', nargs=1, help='Path to the rust compiler')
js_option(env='CARGO', nargs=1, help='Path to the Cargo package manager')

rustc = check_prog('_RUSTC', ['rustc'], what='rustc',
                   paths=toolchain_search_path, input='RUSTC',
                   allow_missing=True)
cargo = check_prog('_CARGO', ['cargo'], what='cargo',
                   paths=toolchain_search_path, input='CARGO',
                   allow_missing=True)


@template
def unwrap_rustup(prog, name):
    # rustc and cargo can either be rustup wrappers, or they can be the actual,
    # plain executables. For cargo, on OSX, rustup sets DYLD_LIBRARY_PATH (at
    # least until https://github.com/rust-lang/rustup.rs/pull/1752 is merged
    # and shipped) and that can wreak havoc (see bug 1536486). Similarly, for
    # rustc, rustup silently honors toolchain overrides set by vendored crates
    # (see bug 1547196).
    #
    # In either case, we need to find the plain executables.
    #
    # To achieve that, try to run `PROG +stable`. When the rustup wrapper is in
    # use, it either prints PROG's help and exits with status 0, or prints
    # an error message (error: toolchain 'stable' is not installed) and exits
    # with status 1. In the cargo case, when plain cargo is in use, it exits
    # with a different error message (e.g. "error: no such subcommand:
    # `+stable`"), and exits with status 101.
    #
    # Unfortunately, in the rustc case, when plain rustc is in use,
    # `rustc +stable` will exit with status 1, complaining about a missing
    # "+stable" file. We'll examine the error output to try and distinguish
    # between failing rustup and failing rustc.
    @depends(prog, dependable(name))
    @imports('subprocess')
    @imports(_from='__builtin__', _import='open')
    @imports('os')
    def unwrap(prog, name):
        def from_rustup_which():
            out = check_cmd_output('rustup', 'which', name,
                                   executable=prog).rstrip()
            # If for some reason the above failed to return something, keep the
            # PROG we found originally.
            if out:
                log.info('Actually using \'%s\'', out)
                return out

            log.info('No `rustup which` output, using \'%s\'', prog)
            return prog

        (retcode, stdout, stderr) = get_cmd_output(prog, '+stable')

        if name == 'cargo' and retcode != 101:
            prog = from_rustup_which()
        elif name == 'rustc':
            if retcode == 0:
                prog = from_rustup_which()
            elif "+stable" in stderr:
                # PROG looks like plain `rustc`.
                pass
            else:
                # Assume PROG looks like `rustup`. This case is a little weird,
                # insofar as the user doesn't have the "stable" toolchain
                # installed, but go ahead and unwrap anyway: the user might
                # have only certain versions, beta, or nightly installed, and
                # we'll catch invalid versions later.
                prog = from_rustup_which()

        return prog

    return unwrap

rustc = unwrap_rustup(rustc, 'rustc')
cargo = unwrap_rustup(cargo, 'cargo')


set_config('CARGO', cargo)
set_config('RUSTC', rustc)


@depends_if(rustc)
@checking('rustc version', lambda info: info.version)
def rustc_info(rustc):
    out = check_cmd_output(rustc, '--version', '--verbose').splitlines()
    info = dict((s.strip() for s in line.split(':', 1)) for line in out[1:])
    return namespace(
        version=Version(info.get('release', '0')),
        commit=info.get('commit-hash', 'unknown'),
        host=info['host'],
    )

set_config('RUSTC_VERSION', depends(rustc_info)(lambda info: str(info.version)))

@depends_if(cargo)
@checking('cargo version', lambda info: info.version)
@imports('re')
def cargo_info(cargo):
    out = check_cmd_output(cargo, '--version', '--verbose').splitlines()
    info = dict((s.strip() for s in line.split(':', 1)) for line in out[1:])
    version = info.get('release')
    # Older versions of cargo didn't support --verbose, in which case, they
    # only output a not-really-pleasant-to-parse output. Fortunately, they
    # don't error out, so we can just try some regexp matching on the output
    # we already got.
    if version is None:
        VERSION_FORMAT = r'^cargo (\d\.\d+\.\d+).*'

        m = re.search(VERSION_FORMAT, out[0])
        # Fail fast if cargo changes its output on us.
        if not m:
            die('Could not determine cargo version from output: %s', out)
        version = m.group(1)

    return namespace(
        version=Version(version),
    )


@depends(rustc_info, cargo_info, build_project)
@imports(_from='textwrap', _import='dedent')
def rust_compiler(rustc_info, cargo_info, build_project):
    if not rustc_info:
        die(dedent('''\
        Rust compiler not found.
        To compile rust language sources, you must have 'rustc' in your path.
        See https://www.rust-lang.org/ for more information.

        You can install rust by running './mach bootstrap'
        or by directly running the installer from https://rustup.rs/
        '''))
    if build_project == 'tools/crashreporter':
        rustc_min_version = Version('1.22.0')
        cargo_min_version = Version('0.23.0')
    else:
        rustc_min_version = Version('1.34.0')
        cargo_min_version = rustc_min_version

    version = rustc_info.version
    if version < rustc_min_version:
        die(dedent('''\
        Rust compiler {} is too old.

        To compile Rust language sources please install at least
        version {} of the 'rustc' toolchain and make sure it is
        first in your path.

        You can verify this by typing 'rustc --version'.

        If you have the 'rustup' tool installed you can upgrade
        to the latest release by typing 'rustup update'. The
        installer is available from https://rustup.rs/
        '''.format(version, rustc_min_version)))

    if not cargo_info:
        die(dedent('''\
        Cargo package manager not found.
        To compile Rust language sources, you must have 'cargo' in your path.
        See https://www.rust-lang.org/ for more information.

        You can install cargo by running './mach bootstrap'
        or by directly running the installer from https://rustup.rs/
        '''))

    version = cargo_info.version
    if version < cargo_min_version:
        die(dedent('''\
        Cargo package manager {} is too old.

        To compile Rust language sources please install at least
        version {} of 'cargo' and make sure it is first in your path.

        You can verify this by typing 'cargo --version'.
        ''').format(version, cargo_min_version))

    return True


@depends(rustc, when=rust_compiler)
def rust_supported_targets(rustc):
    out = check_cmd_output(rustc, '--print', 'target-list').splitlines()
    # The os in the triplets used by rust may match the same OSes, in which
    # case we need to check the raw_os instead.
    per_os = {}
    ambiguous = set()
    per_raw_os = {}
    for t in out:
        t = split_triplet(t, allow_unknown=True)
        endianness = t.endianness
        if t.cpu.startswith('thumb') and endianness not in ('big', 'little'):
            endianness = 'little'
        key = (t.cpu, endianness, t.os)
        if key in per_os:
            previous = per_os[key]
            per_raw_os[(previous.cpu, previous.endianness,
                        previous.raw_os)] = previous
            del per_os[key]
            ambiguous.add(key)
        if key in ambiguous:
            raw_os = t.raw_os
            # split_triplet will return a raw_os of 'androideabi' for
            # rust targets in the form cpu-linux-androideabi, but what
            # we get from the build system is linux-androideabi, so
            # normalize.
            if raw_os == 'androideabi':
                raw_os = 'linux-androideabi'
            per_raw_os[(t.cpu, endianness, raw_os)] = t
        else:
            per_os[key] = t
    return namespace(per_os=per_os, per_raw_os=per_raw_os)


@template
def rust_triple_alias(host_or_target):
    """Template defining the alias used for rustc's --target flag.
    `host_or_target` is either `host` or `target` (the @depends functions
    from init.configure).
    """
    assert host_or_target in {host, target}

    host_or_target_str = {host: 'host', target: 'target'}[host_or_target]

    @depends(rustc, host_or_target, c_compiler, rust_supported_targets,
             arm_target, when=rust_compiler)
    @checking('for rust %s triplet' % host_or_target_str)
    @imports('os')
    @imports('subprocess')
    @imports(_from='mozbuild.configure.util', _import='LineIO')
    @imports(_from='mozbuild.shellutil', _import='quote')
    @imports(_from='tempfile', _import='mkstemp')
    @imports(_from='textwrap', _import='dedent')
    def rust_target(rustc, host_or_target, compiler_info,
                    rust_supported_targets, arm_target):
        # Rust's --target options are similar to, but not exactly the same
        # as, the autoconf-derived targets we use.  An example would be that
        # Rust uses distinct target triples for targetting the GNU C++ ABI
        # and the MSVC C++ ABI on Win32, whereas autoconf has a single
        # triple and relies on the user to ensure that everything is
        # compiled for the appropriate ABI.  We need to perform appropriate
        # munging to get the correct option to rustc.
        # We correlate the autoconf-derived targets with the list of targets
        # rustc gives us with --print target-list.
        if host_or_target.kernel == 'WINNT':
            if compiler_info.type in ('gcc', 'clang'):
                host_or_target_os = 'windows-gnu'
            else:
                host_or_target_os = 'windows-msvc'
            host_or_target_raw_os = host_or_target_os
        else:
            host_or_target_os = host_or_target.os
            host_or_target_raw_os = host_or_target.raw_os

        if host_or_target.cpu == 'arm' and arm_target.arm_arch == 7 and \
                arm_target.fpu == 'neon' and arm_target.thumb2:
            host_or_target_cpus = ('thumbv7neon', host_or_target.cpu)
        else:
            host_or_target_cpus = (host_or_target.cpu,)

        for host_or_target_cpu in host_or_target_cpus:
            rustc_target = rust_supported_targets.per_os.get(
                (host_or_target_cpu, host_or_target.endianness, host_or_target_os))
            if rustc_target:
                break

            rustc_target = rust_supported_targets.per_raw_os.get(
                (host_or_target_cpu, host_or_target.endianness,
                 host_or_target_raw_os))
            if rustc_target:
                break

        if rustc_target is None:
            die("Don't know how to translate {} for rustc".format(
                host_or_target.alias))

        # Check to see whether our rustc has a reasonably functional stdlib
        # for our chosen target.
        target_arg = '--target=' + rustc_target.alias
        in_fd, in_path = mkstemp(prefix='conftest', suffix='.rs')
        out_fd, out_path = mkstemp(prefix='conftest', suffix='.rlib')
        os.close(out_fd)
        try:
            source = 'pub extern fn hello() { println!("Hello world"); }'
            log.debug('Creating `%s` with content:', in_path)
            with LineIO(lambda l: log.debug('| %s', l)) as out:
                out.write(source)

            os.write(in_fd, source)
            os.close(in_fd)

            cmd = [
                rustc,
                '--crate-type', 'staticlib',
                target_arg,
                '-o', out_path,
                in_path,
            ]

            def failed():
                die(dedent('''\
                Cannot compile for {} with {}
                The target may be unsupported, or you may not have
                a rust std library for that target installed. Try:

                  rustup target add {}
                '''.format(host_or_target.alias, rustc, rustc_target.alias)))
            check_cmd_output(*cmd, onerror=failed)
            if not os.path.exists(out_path) or os.path.getsize(out_path) == 0:
                failed()
        finally:
            os.remove(in_path)
            os.remove(out_path)

        # This target is usable.
        return rustc_target.alias

    return rust_target


rust_target_triple = rust_triple_alias(target)
rust_host_triple = rust_triple_alias(host)


@depends(host, rust_host_triple, rustc_info.host)
def validate_rust_host_triple(host, rust_host, rustc_host):
    if rust_host != rustc_host:
        if host.alias == rust_host:
            configure_host = host.alias
        else:
            configure_host = '{}/{}'.format(host.alias, rust_host)
        die("The rust compiler host ({}) is not suitable for the configure host ({})."
            .format(rustc_host, configure_host))


set_config('RUST_TARGET', rust_target_triple)
set_config('RUST_HOST_TARGET', rust_host_triple)


# This is used for putting source info into symbol files.
set_config('RUSTC_COMMIT', depends(rustc_info)(lambda i: i.commit))

# Rustdoc is required by Rust tests below.
js_option(env='RUSTDOC', nargs=1, help='Path to the rustdoc program')

rustdoc = check_prog('RUSTDOC', ['rustdoc'], paths=toolchain_search_path,
                     input='RUSTDOC', allow_missing=True)

# This option is separate from --enable-tests because Rust tests are particularly
# expensive in terms of compile time (especially for code in libxul).
option('--enable-rust-tests',
       help='Enable building and running of Rust tests during `make check`')


@depends('--enable-rust-tests', rustdoc)
def rust_tests(enable_rust_tests, rustdoc):
    if enable_rust_tests and not rustdoc:
        die('--enable-rust-tests requires rustdoc')
    return bool(enable_rust_tests)


set_config('MOZ_RUST_TESTS', rust_tests)


@depends(target, c_compiler, rustc)
@imports('os')
def rustc_natvis_ldflags(target, compiler_info, rustc):
    if target.kernel == 'WINNT' and compiler_info.type == 'clang-cl':
        sysroot = check_cmd_output(rustc, '--print', 'sysroot').strip()
        etc = os.path.join(sysroot, 'lib/rustlib/etc')
        ldflags = []
        for f in os.listdir(etc):
            if f.endswith('.natvis'):
                ldflags.append('-NATVIS:' + normsep(os.path.join(etc, f)))
        return ldflags


set_config('RUSTC_NATVIS_LDFLAGS', rustc_natvis_ldflags)