# 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. option(env="RUSTC", nargs=1, help="Path to the rust compiler") option(env="CARGO", nargs=1, help="Path to the Cargo package manager") rustc = check_prog( "_RUSTC", ["rustc"], what="rustc", paths=rust_search_path, input="RUSTC", allow_missing=True, ) cargo = check_prog( "_CARGO", ["cargo"], what="cargo", paths=rust_search_path, input="CARGO", allow_missing=True, ) @template def unwrap_rustup(prog, name): # rustc and cargo can be rustup wrappers, the actual, plain executables, or # a wrapper script that calls one of the above. # # 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 any 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(_from="__builtin__", _import="open") @imports("os") def unwrap(prog, name): if not prog: return def from_rustup_which(): log.debug( "'%s' looks like a %s rustup wrapper, trying to find its real path", prog, name, ) # `rustup` is a multi-call binary that proxies calls to "real" Rust # binaries based on `argv[0]`. When `PROG` is a symlink to `rustup` # (or `rustup-init`), we can call it "as rustup". log.debug("Trying to call '%s' for `rustup which %s`", prog, name) out = check_cmd_output( "rustup", "which", name, executable=prog, onerror=lambda: "", ).rstrip() if out: log.info("Actually using '%s' as %s", out, name) return out # Bug 2068826: Homebrew rustup >= 1.29.0_1 sets extra environment # variables with `env_script_all_files`, which replaces symlinks # with `write_env_script` wrappers. These *don't* pass `argv[0]`, # so we need to explicitly call `rustup`, which we'll look for in # the same directory as `PROG`. rustup = os.path.join( os.path.dirname(prog), "rustup" + os.path.splitext(prog)[1], ) if os.path.isfile(rustup): log.debug("Trying to call '%s' for `rustup which %s`", rustup, name) out = check_cmd_output( "rustup", "which", name, executable=rustup, onerror=lambda: "", ).rstrip() if out: log.info("Actually using '%s' as %s", out, name) return out # If for some reason the above failed to return something, keep the # PROG we found originally. log.info("No valid `rustup which %s` output, using '%s'", name, 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 normalize_path(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): if not rustc: return 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"], llvm_version=Version(info.get("LLVM version", "0")), ) set_config( "RUSTC_VERSION", depends(rustc_info)(lambda info: str(info.version) if info else None), ) set_config( "RUSTC_LLVM_VERSION", depends(rustc_info)(lambda info: str(info.llvm_version) if info else None), ) set_config( "MOZ_CLANG_NEWER_THAN_RUSTC_LLVM", depends(c_compiler, rustc_info)( lambda c_compiler, rustc_info: ( rustc_info and c_compiler.type == "clang" and c_compiler.version.major > rustc_info.llvm_version.major ) ), ) # Must not be empty: glean-build reads it as a virtualenv path rather than as # unset. @imports(_from="sys", _import="prefix") def glean_parser_venv(): return prefix set_config("GLEAN_PARSER_VENV", glean_parser_venv()) @depends_if(cargo) @checking("cargo version", lambda info: info.version) @imports("re") def cargo_info(cargo): if not cargo: return 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, target) @imports(_from="mozboot.util", _import="MINIMUM_RUST_VERSION") @imports(_from="textwrap", _import="dedent") def rust_compiler(rustc_info, cargo_info, target): 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(MINIMUM_RUST_VERSION) cargo_min_version = rustc_min_version version = rustc_info.version is_nightly = "nightly" in version.version is_version_number_match = ( version.major == rustc_min_version.major and version.minor == rustc_min_version.minor and version.patch == rustc_min_version.patch ) if version < rustc_min_version or (is_version_number_match and is_nightly): die( dedent( """\ Rust compiler {} is too old. To compile Rust language sources please install at least version {} of the 'rustc' toolchain (or, if using nightly, at least one version newer than {}) 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, rustc_min_version) ) ) if target.kernel == "WINNT" and (version.major, version.minor) == (1, 56): die( dedent( """\ Rust compiler 1.56.* is not supported for Windows builds. Use a newer or an older version. See https://github.com/rust-lang/rust/issues/88576. """ ) ) 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) @imports(_from="__builtin__", _import="ValueError") def rust_supported_targets(rustc): out = check_cmd_output(rustc, "--print", "target-list").splitlines() data = {} for t in out: try: info = split_triplet(t, allow_wasi=True) except ValueError: if t.startswith("thumb"): cpu, rest = t.split("-", 1) retry = "-".join(("arm", rest)) elif t.endswith("-windows-gnullvm"): retry = t.removesuffix("llvm") else: continue try: info = split_triplet(retry, allow_wasi=True) except ValueError: continue key = (info.cpu, info.endianness, info.os) data.setdefault(key, []).append(namespace(rust_target=t, target=info)) return data def detect_rustc_target(host_or_target, arm_target, 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. candidates = rust_supported_targets.get( (host_or_target.cpu, host_or_target.endianness, host_or_target.os), [] ) def find_candidate(candidates): if len(candidates) == 1: return candidates[0].rust_target elif not candidates: return None vendor = ( host_or_target.vendor if any(c.target.vendor == host_or_target.vendor for c in candidates) else "unknown" ) # We have multiple candidates. There are two cases where we can try to # narrow further down using extra information from the build system. # - For windows targets, correlate with the ABI if host_or_target.kernel == "WINNT": if host_or_target.abi == "msvc": suffix = "windows-msvc" elif host_or_target.abi == "mingw": suffix = "windows-gnullvm" narrowed = [ c for c in candidates if c.rust_target.endswith("-{}".format(suffix)) ] if len(narrowed) == 1: return narrowed[0].rust_target elif narrowed: candidates = narrowed vendor_aliases = {"pc": ("w64", "windows")} narrowed = [ c for c in candidates if host_or_target.vendor in vendor_aliases.get(c.target.vendor, ()) ] if len(narrowed) == 1: return narrowed[0].rust_target # - For arm targets, correlate with arm_target # we could be more thorough with the supported rust targets, but they # don't support OSes that are supported to build Gecko anyways. # Also, sadly, the only interface to check the rust target cpu features # is --print target-spec-json, and it's unstable, so we have to rely on # our own knowledge of what each arm target means. if host_or_target.cpu == "arm" and host_or_target.endianness == "little": prefixes = [] if arm_target.arm_arch >= 7: if arm_target.thumb2 and arm_target.fpu == "neon": prefixes.append("thumbv7neon") if arm_target.thumb2: prefixes.append("thumbv7a") prefixes.append("armv7") if arm_target.arm_arch >= 6: prefixes.append("armv6") if host_or_target.os != "Android": # arm-* rust targets are armv6... except arm-linux-androideabi prefixes.append("arm") if arm_target.arm_arch >= 5: prefixes.append("armv5te") if host_or_target.os == "Android": # arm-* rust targets are armv6... except arm-linux-androideabi prefixes.append("arm") if arm_target.arm_arch >= 4: prefixes.append("armv4t") # rust freebsd targets are the only ones that don't have a 'hf' suffix # for hard-float. Technically, that means if the float abi ever is not # hard-float, this will pick a wrong target, but since rust only # supports hard-float, let's assume that means freebsd only support # hard-float. if arm_target.float_abi == "hard" and host_or_target.os != "FreeBSD": suffix = "hf" else: suffix = "" for p in prefixes: matches = [ c for c in candidates if c.rust_target.startswith("{}-".format(p)) and c.rust_target.endswith(suffix) ] if not matches: continue # As below, correlate on the (normalized) vendor. for c in matches: if c.target.vendor == vendor: return c.rust_target return matches[0].rust_target # See if we can narrow down on the exact alias. # We use the sub_configure_alias to keep support mingw32 triplets as input. narrowed = [ c for c in candidates if c.target.sub_configure_alias == host_or_target.sub_configure_alias ] if len(narrowed) == 1: return narrowed[0].rust_target elif narrowed: candidates = narrowed # See if we can narrow down with the raw OS narrowed = [c for c in candidates if c.target.raw_os == host_or_target.raw_os] if len(narrowed) == 1: return narrowed[0].rust_target elif narrowed: candidates = narrowed # The wasm32-wasi target was renamed to wasm32-wasip1 if host_or_target.raw_os == "wasi": narrowed = [c for c in candidates if c.target.raw_os == "wasip1"] if len(narrowed) == 1: return narrowed[0].rust_target elif narrowed: candidates = narrowed # Correlate on the (normalized) vendor, so vendor-specific targets (e.g. # the *-oe-linux-gnu targets added in rust 1.98) don't shadow the # generic ones for aliases whose vendor doesn't match a rust target. narrowed = [c for c in candidates if c.target.vendor == vendor] if len(narrowed) == 1: return narrowed[0].rust_target elif narrowed: candidates = narrowed # See if we can narrow down with the raw OS and raw CPU narrowed = [ c for c in candidates if c.target.raw_os == host_or_target.raw_os and c.target.raw_cpu == host_or_target.raw_cpu ] if len(narrowed) == 1: return narrowed[0].rust_target # Finally, see if the vendor can be used to disambiguate. narrowed = [c for c in candidates if c.target.vendor == host_or_target.vendor] if len(narrowed) == 1: return narrowed[0].rust_target return None rustc_target = find_candidate(candidates) if rustc_target is None: die("Don't know how to translate {} for rustc".format(host_or_target.alias)) return rustc_target @imports("os") @imports(_from="textwrap", _import="dedent") @imports(_from="mozbuild.configure.util", _import="LineIO") @imports(_from="__builtin__", _import="open") def assert_rust_compile(host_or_target, rustc_target, rustc): # Check to see whether our rustc has a reasonably functional stdlib # for our chosen target. target_arg = "--target=" + rustc_target with create_temporary_file(suffix=".rs") as in_path, create_temporary_file( suffix=".rlib" ) as out_path: with open(in_path, "w") as fd: source = b'pub extern "C" 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) fd.write(source.decode()) 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) ) ) check_cmd_output(*cmd, onerror=failed) if not os.path.exists(out_path) or os.path.getsize(out_path) == 0: failed() @depends( rustc, host, rustc_info.host, rust_supported_targets, arm_target, when=rust_compiler, ) @checking("for rust host triplet") @imports(_from="textwrap", _import="dedent") def rust_host_triple(rustc, host, rustc_host, rust_supported_targets, arm_target): rustc_target = detect_rustc_target(host, arm_target, rust_supported_targets) if rustc_target != rustc_host: if host.alias == rustc_target: configure_host = host.alias else: configure_host = "{}/{}".format(host.alias, rustc_target) die( dedent( """\ The rust compiler host ({rustc}) is not suitable for the configure host ({configure}). To resolve this, install and select a Rust toolchain for {rustc_target}: rustup default stable-{rustc_target} Then rerun configure. """.format( rustc=rustc_host, configure=configure_host, rustc_target=rustc_target, ) ) ) assert_rust_compile(host, rustc_target, rustc) return rustc_target @depends(rustc, target, rust_supported_targets, arm_target, when=rust_compiler) @checking("for rust target triplet") def rust_target_triple(rustc, target, rust_supported_targets, arm_target): rustc_target = detect_rustc_target(target, arm_target, rust_supported_targets) assert_rust_compile(target, rustc_target, rustc) return rustc_target 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. option(env="RUSTDOC", nargs=1, help="Path to the rustdoc program") rustdoc = check_prog( "RUSTDOC", ["rustdoc"], paths=rust_search_path, input="RUSTDOC", allow_missing=True, ) option( env="RUSTDOCFLAGS", nargs=1, help="Extra options for the rustdoc program", ) set_config("RUSTDOCFLAGS", depends_if("RUSTDOCFLAGS")(lambda flags: flags[0])) # 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 = [] if os.path.isdir(etc): 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) option( "--enable-rust-debug", default=depends(when="--enable-debug")(lambda: True), help="{Build|Do not build} Rust code with debug assertions turned on", ) @depends(when="--enable-rust-debug") def debug_rust(): return True set_config("MOZ_DEBUG_RUST", debug_rust) set_define("MOZ_DEBUG_RUST", debug_rust) # ============================================================== option(env="RUSTFLAGS", nargs=1, help="Rust compiler flags") set_config("RUSTFLAGS", depends("RUSTFLAGS")(lambda flags: flags)) option(env="CARGOFLAGS", nargs=1, help="Flags to pass to cargo") set_config("CARGOFLAGS", depends("CARGOFLAGS")(lambda flags: flags)) # Rust compiler flags # ============================================================== @depends(moz_optimize) def rustc_opt_level_default(moz_optimize): return "2" if moz_optimize else "0" option( env="RUSTC_OPT_LEVEL", default=rustc_opt_level_default, nargs=1, help="Rust compiler optimization level (-C opt-level=%s)", ) @depends("RUSTC_OPT_LEVEL") def rustc_opt_level(opt_level_option): return opt_level_option[0] set_config("CARGO_PROFILE_RELEASE_OPT_LEVEL", rustc_opt_level) set_config("CARGO_PROFILE_DEV_OPT_LEVEL", rustc_opt_level) @depends( rustc_opt_level, debug_rust, target, "--enable-debug-symbols", "--enable-frame-pointers", path_remapping, path_remappings, ) def rust_compile_flags( opt_level, debug_rust, target, debug_symbols, frame_pointers, path_remapping, path_remappings, ): # Cargo currently supports only two interesting profiles for building: # development and release. Those map (roughly) to --enable-debug and # --disable-debug in Gecko, respectively. # # But we'd also like to support an additional axis of control for # optimization level. Since Cargo only supports 2 profiles, we're in # a bit of a bind. # # Code here derives various compiler options given other configure options. # The options defined here effectively override defaults specified in # Cargo.toml files. debug_assertions = None debug_info = None # opt-level=0 implies -C debug-assertions, which may not be desired # unless Rust debugging is enabled. if opt_level == "0" and not debug_rust: debug_assertions = False if debug_symbols: debug_info = "2" opts = [] if debug_assertions is not None: opts.append("debug-assertions=%s" % ("yes" if debug_assertions else "no")) if debug_info is not None: opts.append("debuginfo=%s" % debug_info) opts.append("strip=none") if target.kernel == "Darwin": opts.append("split-debuginfo=unpacked") if frame_pointers: opts.append("force-frame-pointers=yes") # CFG for arm64 is crashy, see `def security_hardening_cflags`. if target.kernel == "WINNT" and target.cpu != "aarch64": opts.append("control-flow-guard=yes") flags = [] for opt in opts: flags.extend(["-C", opt]) if "rust" in path_remapping: # rustc has supported --remap-path-prefix since version 1.26, well # before our required minimum Rust version, so there's no need to # feature-detect or gate on versions. for old, new in path_remappings: flags.append(f"--remap-path-prefix={old}={new}") return flags # Rust incremental compilation # ============================================================== option("--disable-cargo-incremental", help="Disable incremental rust compilation") @depends( developer_options, debug_rust, moz_automation, code_coverage, "--disable-cargo-incremental", using_sccache, using_buildcache, "RUSTC_WRAPPER", ) @imports("os") def cargo_incremental( developer_options, debug_rust, automation, code_coverage, enabled, using_sccache, using_buildcache, rustc_wrapper, ): """Return a value for the CARGO_INCREMENTAL environment variable.""" if not enabled: return "0" elif enabled.origin != "default": return "1" # We never want to use incremental compilation in automation. sccache # handles our automation use case much better than incremental compilation # would. if automation: return "0" # Coverage instrumentation doesn't play well with incremental compilation # https://github.com/rust-lang/rust/issues/50203. if code_coverage: return "0" # Incremental compilation doesn't work as well as it should, and if we're # using a compiler cache, it's better to use it than incremental compilation. using_compiler_cache = using_sccache or using_buildcache if not using_compiler_cache and rustc_wrapper: wrapper_name = os.path.splitext(os.path.basename(rustc_wrapper[0]))[0].lower() if wrapper_name in ("sccache", "buildcache"): using_compiler_cache = True if using_compiler_cache: return "0" # Incremental compilation is automatically turned on for debug builds, so # we don't need to do anything special here. if debug_rust: return # Don't enable on --enable-release builds, because of the runtime # performance cost. if not developer_options: return # We're clear to use incremental compilation! return "1" set_config("CARGO_INCREMENTAL", cargo_incremental) @depends(rust_compile_flags, "--enable-warnings-as-errors") def rust_flags(compile_flags, warnings_as_errors): warning_flags = [] # Note that cargo passes --cap-lints warn to rustc for third-party code, so # we don't need a very complicated setup. if warnings_as_errors: warning_flags.append("-Dwarnings") else: warning_flags.extend(("--cap-lints", "warn")) return compile_flags + warning_flags set_config("MOZ_RUST_DEFAULT_FLAGS", rust_flags) @depends( cross_compiling, tsan, asan, any_ubsan, target_variables.OS_ARCH, host_variables.HOST_OS_ARCH, code_coverage, ) def pass_only_base_cflags_to_rust( cross_compiling, tsan, asan, ubsan, os_arch, host_os_arch, code_coverage ): # Cargo does not distinguish native code built for build scripts and procedural # macros from native code built for the target. Passing all target flags to # cc-rs can therefore instrument host code. Sanitizer and coverage builds use # only the base compiler flags. if not cross_compiling: if tsan: return True if (asan or ubsan) and os_arch != "Linux": return True if host_os_arch == "WINNT" and code_coverage: return True @depends(tsan, enable_js_fuzzilli, enable_libfuzzer, enable_aflfuzzer) def rust_sancov_flags(tsan, js_fuzzilli, libfuzzer, aflfuzz): # TSan and Fuzzilli builds do not use these sanitizer coverage flags. if tsan or js_fuzzilli: return [] if libfuzzer: # Match the coverage options implied by `clang -fsanitize=fuzzer`. # https://github.com/llvm/llvm-project/blob/release/13.x/clang/lib/Driver/SanitizerArgs.cpp#L422 return [ "-Cpasses=sancov-module", "-Cllvm-args=-sanitizer-coverage-inline-8bit-counters", "-Cllvm-args=-sanitizer-coverage-level=4", "-Cllvm-args=-sanitizer-coverage-trace-compares", "-Cllvm-args=-sanitizer-coverage-pc-table", ] if aflfuzz: # Match the sanitizer coverage flags used by afl-cc. # https://github.com/AFLplusplus/AFLplusplus/blob/4eaacfb095ac164afaaf9c10b8112f98d8ad7c2a/src/afl-cc.c#L2101 return [ "-Cpasses=sancov-module", "-Cllvm-args=-sanitizer-coverage-level=3", "-Cllvm-args=-sanitizer-coverage-pc-table", "-Cllvm-args=-sanitizer-coverage-trace-pc-guard", ] return [] set_config("RUST_SANCOV_FLAGS", rust_sancov_flags) @depends( moz_pgo_rust, enable_profile_generate, enable_profile_use, pgo_flags.gen_cflags, pgo_profile_path, c_compiler, rustc_info, build_environment, ) def rust_pgo_flags( moz_pgo_rust, profile_generate, profile_use, gen_cflags, profile_path, c_compiler, rustc_info, build_environment, ): if not moz_pgo_rust: return [] if profile_generate: flags = ["-C", f"profile-generate={build_environment.topobjdir}"] # Disable value profiling when: # (RUSTC_LLVM_VERSION < 12 and CC_VERSION >= 12) or # (RUSTC_LLVM_VERSION >= 12 and CC_VERSION < 12) cc_old = c_compiler.version.major < 12 rustc_old = bool(rustc_info) and rustc_info.llvm_version.major < 12 if cc_old != rustc_old: flags += ["-C", "llvm-args=--disable-vp=true"] # Convert PGO LLVM arguments from the compiler form # ["-mllvm", "foo"] to the Rust form ["-C", "llvm-args=foo"]. gen_cflags = gen_cflags or [] i = 0 while i < len(gen_cflags): if gen_cflags[i] == "-mllvm" and i + 1 < len(gen_cflags): flags += ["-C", f"llvm-args={gen_cflags[i + 1]}"] i += 2 else: i += 1 return flags if profile_use: return ["-C", f"profile-use={profile_path}"] return [] # PGO flags for C and C++ code compiled through cc-rs. # Exclude -fprofile-generate because it can instrument host build scripts on # native builds and cause link failures. No Rust directory opts out of PGO. @depends( enable_profile_generate, enable_profile_use, pgo_flags.gen_cflags, pgo_flags.use_cflags, ) def rust_pgo_cflags(profile_generate, profile_use, gen_cflags, use_cflags): if profile_generate: flags = ["-DNS_FREE_PERMANENT_DATA=1"] + (gen_cflags or []) elif profile_use: flags = list(use_cflags or []) else: return [] return [f for f in flags if not f.startswith("-fprofile-generate")] set_config("RUST_PGO_CFLAGS", rust_pgo_cflags) @depends( enable_profile_generate, enable_profile_use, pgo_flags.gen_ldflags, pgo_flags.use_ldflags, ) def rust_pgo_ldflags(profile_generate, profile_use, gen_ldflags, use_ldflags): if profile_generate: return gen_ldflags or [] if profile_use: return use_ldflags or [] return [] set_config("RUST_PGO_LDFLAGS", rust_pgo_ldflags) # Enable link-time optimization for release builds, but not when doing # cross-language LTO. # Never enable when sancov is enabled to work around https://github.com/rust-lang/rust/issues/90300. # Never enable when coverage is enabled to work around https://github.com/rust-lang/rust/issues/90045. rust_lto_eligible = ( ~developer_options & ~debug_rust & ~lto.rust_lto & ~rust_sancov_flags & ~code_coverage ) set_config("RUST_LTO_ELIGIBLE", True, when=rust_lto_eligible) # Cargo build arguments # ============================================================== set_config( "MOZ_CARGO_TARGET_ARGS", depends(rust_target_triple)(lambda triple: [f"--target={triple}"]), ) set_config( "MOZ_CARGO_HOST_TARGET_ARGS", depends(rust_host_triple)(lambda triple: [f"--target={triple}"]), ) # Custom profiles and their artifact directories use a dev or release prefix. # Other libraries use --release (or default dev profile for debug builds). @depends(debug_rust) def rust_cargo_profile(debug_rust): if debug_rust: return namespace(prefix="dev", default_dir="debug", default_args=[]) return namespace( prefix="release", default_dir="release", default_args=["--release"] ) set_config("MOZ_CARGO_PROFILE_PREFIX", rust_cargo_profile.prefix) set_config("MOZ_CARGO_DEFAULT_PROFILE_DIR", rust_cargo_profile.default_dir) set_config("MOZ_CARGO_DEFAULT_PROFILE_ARGS", rust_cargo_profile.default_args) # The Spidermonkey library can be built from a package tarball outside the # tree, so we want to let Cargo create lock files in this case. When built # within a tree, the Rust dependencies have been vendored in so Cargo won't # touch the lock file. set_config("MOZ_CARGO_FROZEN_ARGS", ["--frozen"], when=~js_standalone) # With TSan, we also need to rebuild the rust stdlib so that it's instrumented. # Because build-std is still pretty experimental, we need to explicitly request # the panic_abort crate for `panic = "abort"` support. set_config("MOZ_CARGO_BUILD_STD_ARGS", ["-Zbuild-std=std,panic_abort"], when=tsan) # Rust compiler flags for Cargo builds # ============================================================== # By default the Rust compiler will perform a limited kind of ThinLTO on each # crate. For local builds this additional optimization is not worth the # increase in compile time so we opt out of it. set_config("MOZ_RUSTFLAGS_AFTER_EXTRA", ["-Clto=off"], when=developer_options) # Work around https://github.com/rust-lang/rust/issues/112480 @depends(debug_rust, rust_target_triple) def rust_check_alignment_workaround(debug_rust, rust_target): return debug_rust and rust_target.startswith("i686-pc-windows-") @depends( arm_target.fpu, rust_target_triple, tsan, rust_lto_eligible, rust_check_alignment_workaround, ) def rustflags_target_common( fpu, rust_target, tsan, lto_eligible, check_alignment_workaround, ): flags = [] # Enable neon and disable restriction to 16 FPU registers when neon is enabled # but we're not using a thumbv7neon target, where it's already the default. # (CPUs with neon have 32 FPU registers available) if fpu == "neon" and not rust_target.startswith("thumbv7neon-"): flags += ["-C", "target_feature=+neon,-d16"] if tsan: flags.append("-Zsanitizer=thread") # We need -Cembed-bitcode=yes for all crates when using -Clto. if lto_eligible: flags.append("-Cembed-bitcode=yes") if check_alignment_workaround: flags.append("-Zmir-enable-passes=-CheckAlignment") return flags set_config("MOZ_RUSTFLAGS_TARGET_COMMON", rustflags_target_common) @depends(rust_pgo_flags, lto.rust_lto) def rustflags_target_ltoable(pgo_flags, rust_lto): flags = list(pgo_flags) if rust_lto: flags.append("-Clinker-plugin-lto") return flags set_config("MOZ_RUSTFLAGS_TARGET_LTOABLE", rustflags_target_ltoable) @depends("RUSTFLAGS") @imports(_from="mozshellutil", _import="split") def user_rustflags(flags): return split(flags[0]) if flags else [] @depends(user_rustflags, rustflags_target_common) def rust_sanitizers(rustflags, target_common): return [ flag for flag in rustflags + target_common if flag.startswith("-Zsanitizer=") ] # Rustc passes -nodefaultlibs to the linker (clang) on mac, which prevents # clang from adding the necessary sanitizer runtimes when building with # C/C++ sanitizer but without rust sanitizer. @depends("LDFLAGS", linker_flags, rust_sanitizers) @imports(_from="mozshellutil", _import="split") def rustflags_default_linker_libraries(env_ldflags, linker_flags, sanitizers): if sanitizers: return [] ldflags = list(linker_flags.ldflags) if env_ldflags: ldflags += split(env_ldflags[0]) if any(flag.startswith("-fsanitize=") for flag in ldflags): return ["-C", "default-linker-libraries=yes"] return [] set_config( "MOZ_RUSTFLAGS_DEFAULT_LINKER_LIBRARIES", rustflags_default_linker_libraries, when=target_is_darwin, ) # Cargo needs the same linker flags as the C/C++ compiler, # but not the final libraries. Filter those out because they # cause problems on macOS 10.7; see bug 1365993 for details. # Also, we don't want to pass PGO flags until cargo supports them. # Finally, we also remove the -Wl,--build-id=uuid flag when it's in # the LDFLAGS. The flag was chosen over the default (build-id=sha1) # in developer builds, because for libxul, it's faster. But it's also # non-deterministic. So when the rust compiler produces procedural # macros as libraries, they're not reproducible. Those procedural # macros then end up as dependencies of other crates, and their # non-reproducibility leads to sccache transitively having cache # misses. # # When building with sanitizer, rustc links its own runtime, which conflicts # with the one that passing -fsanitize=* to the linker would add. # Ideally, we'd always do this filtering, but because the flags may also apply # to build scripts because cargo doesn't allow the distinction, we only filter # when building programs, except when using thread sanitizer where we filter # everywhere. # # These patterns use GNU make $(filter-out) syntax, where % is the wildcard. @depends(rust_sanitizers) def cargo_ldflags_filter_out(sanitizers): target = [ "-fsanitize=cfi%", "-framework", "Cocoa", "-lobjc", "AudioToolbox", "ExceptionHandling", "-fprofile-%", "-Wl,--build-id=uuid", ] program = [] if "-Zsanitizer=thread" in sanitizers: target.append("-fsanitize=%") elif sanitizers: program.append("-fsanitize=%") return namespace(target=target, program=program) set_config("MOZ_CARGO_LDFLAGS_FILTER_OUT", cargo_ldflags_filter_out.target) set_config("MOZ_CARGO_PROGRAM_LDFLAGS_FILTER_OUT", cargo_ldflags_filter_out.program) # If this is a release build we want rustc to generate one codegen unit per # crate. This results in better optimization and less code duplication at the # cost of longer compile times. set_config("MOZ_RUSTFLAGS_CODEGEN", ["-C", "codegen-units=1"], when=~developer_options) # The rust wasi target defaults to statically link the wasi crt, but when we # build static libraries from rust and link them with C/C++ code, we also link # a wasi crt, which may conflict with rust's. set_config( "MOZ_RUST_LIBRARY_RUSTCFLAGS", ["-C", "target-feature=-crt-static"], when=depends(target.os)(lambda os: os == "WASI"), ) set_config("MOZ_RUSTC_BOOTSTRAP_FORCE", "1", when=rust_check_alignment_workaround) # When host == target, cargo will compile build scripts with sanitizers enabled # if sanitizers are enabled, which may randomly fail when they execute # because of https://github.com/google/sanitizers/issues/1322. # Work around by disabling __tls_get_addr interception (bug 1635327). @depends(rust_target_triple, rust_host_triple, asan, tsan, any_ubsan) def rust_sanitizer_option_vars(rust_target, rust_host, asan, tsan, ubsan): if rust_target != rust_host: return [] enabled = {"ASAN": asan, "TSAN": tsan, "UBSAN": ubsan} return [f"{name}_OPTIONS" for name, on in enabled.items() if on] set_config("MOZ_RUST_SANITIZER_OPTION_VARS", rust_sanitizer_option_vars) # C and C++ compilers for the cc crate # ============================================================== # We're passing these for consumption by the `cc` crate, which doesn't use the same # convention as cargo itself: # https://github.com/alexcrichton/cc-rs/blob/baa71c0e298d9ad7ac30f0ad78f20b4b3b3a8fb2/src/lib.rs#L1715 set_config( "MOZ_CARGO_CC_ENV_SUFFIX", depends(rust_target_triple)(lambda triple: triple.replace("-", "_")), ) set_config( "MOZ_CARGO_HOST_CC_ENV_SUFFIX", depends(rust_host_triple)(lambda triple: triple.replace("-", "_")), ) # HOST_CC/HOST_CXX/CC/CXX usually contain base flags for e.g. the build target. # We want to pass those through CFLAGS_*/CXXFLAGS_* instead, so that they end up # after whatever cc-rs adds to the compiler command line, so that they win. # Ideally, we'd use CRATE_CC_NO_DEFAULTS=1, but that causes other problems at the # moment. @template def cargo_compiler(compiler): return depends_if(compiler)(lambda c: list(c.wrapper) + [c.compiler]) # Replace "clang-cl.exe" by "clang.exe --driver-mode=cl" to avoid loading the # module clang.exe again when loading the plugin dll, which links to the import # library of clang.exe. @template def cargo_target_compiler(compiler): @depends(cargo_compiler(compiler), target, "--enable-clang-plugin") @imports("re") def cargo_target_compiler(command, target, clang_plugin): if not command or not clang_plugin or target.kernel != "WINNT": return command result = [] for arg in command: arg = re.sub(r"\.EXE$", ".exe", arg) if arg.endswith("clang-cl.exe"): result += [arg.replace("clang-cl.exe", "clang.exe"), "--driver-mode=cl"] else: result.append(arg) return result return cargo_target_compiler set_config("MOZ_CARGO_CC", cargo_target_compiler(c_compiler)) set_config("MOZ_CARGO_CXX", cargo_target_compiler(cxx_compiler)) set_config("MOZ_CARGO_HOST_CC", cargo_compiler(host_c_compiler)) set_config("MOZ_CARGO_HOST_CXX", cargo_compiler(host_cxx_compiler)) @template def cargo_base_flags(compiler, host_or_target): @depends_if(compiler, host_or_target) def cargo_base_flags(compiler, host_or_target): flags = list(compiler.flags) if host_or_target.kernel == "WINNT": return flags + ["-DUNICODE"] return flags return cargo_base_flags set_config("MOZ_CARGO_CFLAGS_BASE", cargo_base_flags(c_compiler, target)) set_config("MOZ_CARGO_CXXFLAGS_BASE", cargo_base_flags(cxx_compiler, target)) set_config("MOZ_CARGO_HOST_CFLAGS_BASE", cargo_base_flags(host_c_compiler, host)) set_config("MOZ_CARGO_HOST_CXXFLAGS_BASE", cargo_base_flags(host_cxx_compiler, host)) # The cc crate will fill in for those flags anyways, but we do need the PGO and # LTO flags to fill in for what the cc crate doesn't handle # (e.g. -pgo-temporal-instrumentation) # We can't use LTO flags with GCC, though: https://github.com/rust-lang/rust/issues/138681 @depends(lto.cflags, c_compiler) def rust_lto_cflags(lto_cflags, c_compiler): if c_compiler.type.startswith("clang"): return list(lto_cflags or []) return [] set_config("RUST_LTO_CFLAGS", rust_lto_cflags) # Because cargo doesn't allow to distinguish builds happening for build # scripts/procedural macros vs. those happening for the rust target, # we can't blindly pass all our flags down for cc-rs to use them, because of the # side effects they can have on what otherwise should be host builds. # So for sanitizer and coverage builds, we only pass the base compiler flags. # This means C code built by rust is not going to be covered by sanitizers # and coverage. But at least we control what compiler is being used, # rather than relying on cc-rs guesses, which, sometimes fail us. # -fno-sized-deallocation is important, though, as -fsized-deallocation may be the # compiler default and we don't want it to be used # (see build/moz.configure/flags.configure). Likewise with -fno-aligned-new. # Each filter lists the computed flags cc-rs receives, where "%" keeps them all. @depends(pass_only_base_cflags_to_rust) def cargo_computed_flags_filter(base_only): if base_only: return namespace( cflags=[], cxxflags=["-fno-aligned-new", "-fno-sized-deallocation"], host_cflags=[], host_cxxflags=[], ) return namespace( cflags=["%"], cxxflags=["%"], host_cflags=["%"], host_cxxflags=["%"] ) set_config("MOZ_CARGO_CFLAGS_FILTER", cargo_computed_flags_filter.cflags) set_config("MOZ_CARGO_CXXFLAGS_FILTER", cargo_computed_flags_filter.cxxflags) set_config("MOZ_CARGO_HOST_CFLAGS_FILTER", cargo_computed_flags_filter.host_cflags) set_config("MOZ_CARGO_HOST_CXXFLAGS_FILTER", cargo_computed_flags_filter.host_cxxflags) set_config("MOZ_RUSTC_WRAPPER", ccache, when=using_sccache | using_buildcache) # Cargo linker wrappers and build script environment # ============================================================== # Use .bat wrapping on Windows hosts, and shell wrapping on other hosts. @template def cargo_wrapper(name): @depends(host, build_environment) @imports("os") def cargo_wrapper(host, build_environment): ext = ".bat" if host.kernel == "WINNT" else "" return normsep(os.path.join(build_environment.topsrcdir, "build", name + ext)) return cargo_wrapper set_config("MOZ_CARGO_LINKER", cargo_wrapper("cargo-linker")) set_config("MOZ_CARGO_HOST_LINKER", cargo_wrapper("cargo-host-linker")) set_config("MOZ_CARGO_CLIPPY_WRAPPER", cargo_wrapper("cargo-clippy-wrapper")) @template def cargo_linker_env_var(triple): return depends(triple)( lambda t: f"CARGO_TARGET_{t.upper().replace('-', '_')}_LINKER" ) set_config("MOZ_CARGO_LINKER_ENV_VAR", cargo_linker_env_var(rust_target_triple)) set_config("MOZ_CARGO_HOST_LINKER_ENV_VAR", cargo_linker_env_var(rust_host_triple)) @template def cargo_wrap_ld(c_compiler, cxx_compiler, linker): @depends(c_compiler, cxx_compiler, linker) def cargo_wrap_ld(c_compiler, cxx_compiler, linker): if c_compiler.type == "clang-cl": return namespace(ld=[linker], ld_cxx=[linker]) def command(compiler): return list(compiler.wrapper) + [compiler.compiler] + list(compiler.flags) return namespace(ld=command(c_compiler), ld_cxx=command(cxx_compiler)) return cargo_wrap_ld cargo_target_wrap_ld = cargo_wrap_ld(c_compiler, cxx_compiler, link) cargo_host_wrap_ld = cargo_wrap_ld(host_c_compiler, host_cxx_compiler, host_link) set_config("MOZ_CARGO_LD", cargo_target_wrap_ld.ld) set_config("MOZ_CARGO_LD_CXX", cargo_target_wrap_ld.ld_cxx) set_config("MOZ_CARGO_HOST_LD", cargo_host_wrap_ld.ld) set_config("MOZ_CARGO_HOST_LD_CXX", cargo_host_wrap_ld.ld_cxx) @depends(host_ldflags, host, host_linker_libpaths, host_linker_libpaths_bat) def cargo_wrap_host_ldflags(host_ldflags, host, libpaths, libpaths_bat): libpaths = libpaths_bat if host.kernel == "WINNT" else libpaths return list(host_ldflags) + list(libpaths or []) set_config("MOZ_CARGO_HOST_LDFLAGS", cargo_wrap_host_ldflags) # Set COREAUDIO_SDK_PATH for third_party/rust/coreaudio-sys/build.rs @depends(macos_sdk, ios_sdk, when=target_is_darwin) def coreaudio_sdk_path(macos_sdk, ios_sdk): return ios_sdk or macos_sdk set_config("MOZ_RUST_COREAUDIO_SDK_PATH", coreaudio_sdk_path)