build/moz.configure/rust.configure
author Narcis Beleuzu <nbeleuzu@mozilla.com>
Tue, 02 Oct 2018 05:36:29 +0300
changeset 439128 2b4331c9d829043af5513f3f1b97f53026928d8c
parent 439123 11df51f97f138ade3124881b9a72a3d5e65fabba
child 440253 6b740afea4037c8d83d8ff3329a0c72689f13867
permissions -rw-r--r--
Backed out changeset 11df51f97f13 (bug 1495293) for artifact build bustage

# -*- 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/.


@imports(_from='os.path', _import='expanduser')
def add_rustup_path(what):
    # rustup installs rustc/cargo into ~/.cargo/bin by default,
    # so look there if the binaries aren't in $PATH.
    return [what, os.path.join(expanduser('~/.cargo/bin'), what)]


# 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', add_rustup_path('rustc'),
                   input='RUSTC', allow_missing=True)
cargo = check_prog('CARGO', add_rustup_path('cargo'),
                   input='CARGO', allow_missing=True)


@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'],
    )


@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)
@imports(_from='textwrap', _import='dedent')
def rust_compiler(rustc_info, cargo_info):
    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/
        '''))
    rustc_min_version = Version('1.29.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)
        key = (t.cpu, t.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, t.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)

    @depends(rustc, host_or_target, c_compiler, rust_supported_targets,
             when=rust_compiler)
    @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):
        # 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

        rustc_target = rust_supported_targets.per_os.get(
            (host_or_target.cpu, host_or_target.endianness, host_or_target_os))

        if rustc_target is None:
            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 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)

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


@depends(rust_target_triple)
def rust_target_env_name(triple):
    return triple.upper().replace('-', '_')


# We need this to form various Cargo environment variables, as there is no
# uppercase function in make, and we don't want to shell out just for
# converting a string to uppercase.
set_config('RUST_TARGET_ENV_NAME', rust_target_env_name)

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

# Until we remove all the other Rust checks in old-configure.
add_old_configure_assignment('RUSTC', rustc)
add_old_configure_assignment('RUST_TARGET', rust_target_triple)

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

rustdoc = check_prog('RUSTDOC', add_rustup_path('rustdoc'),
                     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 of Rust tests, and build-time execution of them')


@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)

# cbindgen is needed by the style system build.
cbindgen = check_prog('CBINDGEN', add_rustup_path('cbindgen'), paths=toolchain_search_path,
                      when=depends(build_project)
                      (lambda build_project: build_project != 'js'))


@depends_if(cbindgen)
@checking('cbindgen version')
@imports(_from='textwrap', _import='dedent')
def cbindgen_version(cbindgen):
    cbindgen_min_version = Version('0.6.2')

    # cbindgen x.y.z
    version = Version(check_cmd_output(cbindgen, '--version').strip().split(" ")[1])

    if version < cbindgen_min_version:
        die(dedent('''\
        cbindgen version {} is too old. At least version {} is required.

        Please update using 'cargo install cbindgen --force' or running
        './mach bootstrap', after removing the existing executable located at
        {}.
        '''.format(version, cbindgen_min_version, cbindgen)))

    return version


# Bindgen can use rustfmt to format Rust file, but it's not required.
js_option(env='RUSTFMT', nargs=1, help='Path to the rustfmt program')

rustfmt = check_prog('RUSTFMT', add_rustup_path('rustfmt'),
                     input='RUSTFMT', allow_missing=True)

js_option(env='WIN64_LINK', nargs=1, help='Path to link.exe that targets win64')
js_option(env='WIN64_LIB', nargs=1, help='Paths to libraries for the win64 linker')

set_config('WIN64_LINK', depends('WIN64_LINK')(lambda x: x))
set_config('WIN64_LIB', depends('WIN64_LIB')(lambda x: x))


@depends(target, rustc_info, c_compiler, 'WIN64_LINK', 'WIN64_LIB')
def win64_cargo_linker(target, rustc_info, compiler_info, link, lib):
    # When we're building a 32-bit Windows build with a 64-bit rustc, we
    # need to configure the linker it will use for host binaries (build scripts)
    # specially because the compiler configuration we use for the build is for
    # MSVC targeting 32-bit binaries.
    if target.kernel == 'WINNT' and target.cpu == 'x86' and \
       compiler_info.type in ('msvc', 'clang-cl') and \
       rustc_info.host == 'x86_64-pc-windows-msvc' and link and lib:
        return True


set_config('WIN64_CARGO_LINKER', win64_cargo_linker)


@depends(win64_cargo_linker, check_build_environment)
@imports(_from='textwrap', _import='dedent')
def win64_cargo_linker_config(linker, env):
    if linker:
        return dedent('''\
        [target.x86_64-pc-windows-msvc]
        linker = "{objdir}/build/win64/cargo-linker.bat"
        '''.format(objdir=env.topobjdir))
    # We want an empty string here so we don't leave the @ variable in the config file.
    return ''


set_config('WIN64_CARGO_LINKER_CONFIG', win64_cargo_linker_config)