diff --git a/rust/BUILD.bazel b/rust/BUILD.bazel index f4edbd8b61..c59afe36a2 100644 --- a/rust/BUILD.bazel +++ b/rust/BUILD.bazel @@ -13,6 +13,12 @@ toolchain_type( name = "toolchain_type", ) +# Miri needs a separate toolchain contract because it brings a different +# driver binary, sysroot, and runtime closure than normal rustc compilation. +toolchain_type( + name = "miri_toolchain_type", +) + alias( name = "toolchain", actual = "toolchain_type", diff --git a/rust/defs.bzl b/rust/defs.bzl index 4f2ef72582..6368135e0c 100644 --- a/rust/defs.bzl +++ b/rust/defs.bzl @@ -33,6 +33,11 @@ load( "//rust/private:lints.bzl", _rust_lint_config = "rust_lint_config", ) +load( + "//rust/private:miri.bzl", + _miri_binary = "miri_binary", + _miri_test = "miri_test", +) load( "//rust/private:rust.bzl", _rust_binary = "rust_binary", @@ -102,6 +107,14 @@ rust_test = _rust_test rust_test_suite = _rust_test_suite # See @rules_rust//rust/private:rust.bzl for a complete description. +# Miri rules wrap already-declared Rust targets so they can reuse the existing +# crate graph instead of re-encoding the full rust_* rule surface. +miri_test = _miri_test +# See @rules_rust//rust/private:miri.bzl for a complete description. + +miri_binary = _miri_binary +# See @rules_rust//rust/private:miri.bzl for a complete description. + rust_doc = _rust_doc # See @rules_rust//rust/private:rustdoc.bzl for a complete description. diff --git a/rust/private/BUILD.bazel b/rust/private/BUILD.bazel index 89444bb9bc..254e2c550e 100644 --- a/rust/private/BUILD.bazel +++ b/rust/private/BUILD.bazel @@ -1,4 +1,5 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") load("//rust/private:rust_analyzer.bzl", "rust_analyzer_detect_sysroot") load("//rust/private:rustc.bzl", "is_proc_macro_dep", "is_proc_macro_dep_enabled") @@ -50,6 +51,15 @@ is_proc_macro_dep_enabled( visibility = ["//visibility:public"], ) +# This build setting is flipped by the Miri transition so the normal Rust +# compilation pipeline can rebuild target-side crates against the Miri sysroot +# without affecting unrelated Rust builds. +bool_flag( + name = "miri_enabled", + build_setting_default = False, + visibility = ["//visibility:public"], +) + rust_analyzer_detect_sysroot( name = "rust_analyzer_detect_sysroot", visibility = ["//visibility:public"], diff --git a/rust/private/miri.bzl b/rust/private/miri.bzl new file mode 100644 index 0000000000..48b05ced29 --- /dev/null +++ b/rust/private/miri.bzl @@ -0,0 +1,386 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Miri execution rules backed by the direct `miri` driver.""" + +load("//rust/private:common.bzl", "rust_common") +load("//rust/private:miri_config.bzl", "miri_transition", "rlocationpath") +load( + "//rust/private:rustc.bzl", + "miri_collect_libs_from_linker_inputs", + "miri_should_use_pic", +) +load("//rust/private:utils.bzl", "dedent", "find_cc_toolchain", "find_toolchain") + +_RUNFILES_BASH_INIT = """# --- begin runfiles.bash initialization v3 --- +set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 \"^$f \" "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 \"^$f \" "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 \"^$f \" "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v3 --- +""" + +def _shell_quote(value): + return "'{}'".format(value.replace("'", "'\"'\"'")) + +def _crate_from_target(target): + # Wrapper rules accept either a normal crate target or a rust_test target; + # tests store the executable harness in test_crate_info.crate. + if rust_common.crate_info in target: + return target[rust_common.crate_info] + return target[rust_common.test_crate_info].crate + +def _crate_info_for_extern(dep): + return dep.dep if hasattr(dep, "dep") else dep + +def _crate_name_for_extern(dep): + return dep.name if hasattr(dep, "dep") else dep.name + +def _emit_shell_array(name, values): + lines = ["{}=(".format(name)] + for value in values: + lines.append(" {}".format(_shell_quote(value))) + lines.append(")") + return lines + +def _target_flag_lines(ctx, toolchain): + if toolchain.target_json: + return [ + "TARGET_FLAG=$(rlocation {})".format(_shell_quote(rlocationpath(toolchain.target_json, ctx.workspace_name))), + ] + + return ["TARGET_FLAG={}".format(_shell_quote(toolchain.target_flag_value))] + +def _native_runfiles(ctx, crate, dep_info): + linker_inputs = dep_info.transitive_noncrates.to_list() + if not linker_inputs: + return depset() + + cc_toolchain, feature_configuration = find_cc_toolchain(ctx) + if cc_toolchain: + use_pic = miri_should_use_pic(cc_toolchain, feature_configuration, crate.type, ctx.var["COMPILATION_MODE"]) + runtime_libs = cc_toolchain.dynamic_runtime_lib(feature_configuration = feature_configuration) if crate.type in ["dylib", "cdylib"] else cc_toolchain.static_runtime_lib(feature_configuration = feature_configuration) + else: + use_pic = False + runtime_libs = depset() + + # Even when Miri interprets the Rust crate directly, mixed Rust/native + # targets still need their native inputs present in runfiles so any + # rustc-level link metadata and runtime discovery has a chance to resolve. + return depset( + direct = miri_collect_libs_from_linker_inputs(linker_inputs, use_pic) + [ + additional_input + for linker_input in linker_inputs + for additional_input in linker_input.additional_inputs + ], + transitive = [runtime_libs], + ) + +def _script_content(ctx, *, crate, dep_info, miri_toolchain, is_test, miri_flags): + # The generated launcher reconstructs a rustc-shaped direct Miri invocation + # from the analyzed Bazel crate graph; this keeps Cargo out of the runtime + # path and lets Bazel stay the source of truth for dependencies. + toolchain = find_toolchain(ctx) + crate_type = "bin" if is_test else crate.type + + if not is_test and crate.type != "bin": + fail("miri_binary requires a wrapped `rust_binary`-like target. {} has crate type {}".format(ctx.attr.crate.label, crate.type)) + + # Native linker inputs are staged separately in runfiles, but the launcher + # still reconstructs only the Rust-facing driver arguments here because the + # direct Miri path does not go through Cargo's native-link orchestration. + + # Pass direct Rust dependencies as explicit --extern flags and transitive + # crate outputs as -Ldependency search paths, mirroring the rustc command + # line shape that Miri expects. + extern_specs = [] + for dep in dep_info.direct_crates.to_list(): + dep_crate = _crate_info_for_extern(dep) + extern_specs.append("{}|{}".format( + _crate_name_for_extern(dep), + rlocationpath(dep_crate.output, ctx.workspace_name), + )) + + dependency_outputs = [] + seen_outputs = {} + for dep in dep_info.transitive_crates.to_list(): + dep_output = rlocationpath(dep.output, ctx.workspace_name) + if dep_output not in seen_outputs: + seen_outputs[dep_output] = None + dependency_outputs.append(dep_output) + + rustc_env_exports = [] + for key in sorted(crate.rustc_env.keys()): + rustc_env_exports.append("export {}={}".format(key, _shell_quote(crate.rustc_env[key]))) + + rustc_env_files = [rlocationpath(file, ctx.workspace_name) for file in crate.rustc_env_files] + + lines = [ + "#!/usr/bin/env bash", + _RUNFILES_BASH_INIT.rstrip(), + "", + "set -euo pipefail", + "", + # Resolve runtime inputs out of Bazel runfiles so the launcher works + # the same under `bazel run`, `bazel test`, and direct execution. + "MIRI=$(rlocation {})".format(_shell_quote(rlocationpath(miri_toolchain.miri, ctx.workspace_name))), + "CRATE_ROOT=$(rlocation {})".format(_shell_quote(rlocationpath(crate.root, ctx.workspace_name))), + "SYSROOT=$(dirname \"$(rlocation {})\")".format(_shell_quote(rlocationpath(miri_toolchain.sysroot_anchor, ctx.workspace_name))), + ] + lines.extend(_target_flag_lines(ctx, toolchain)) + lines.extend([ + "", + "export CARGO_MANIFEST_DIR=$(dirname \"${CRATE_ROOT}\")", + # Expose a stable runtime marker so interpreted code can detect the + # direct Bazel-backed Miri path even when dependencies were compiled by + # rustc rather than the `miri` driver itself. + "export RULES_RUST_MIRI=1", + "export REPOSITORY_NAME={}".format(_shell_quote(ctx.label.workspace_name)), + ]) + lines.extend(rustc_env_exports) + + if rustc_env_files: + lines.append("") + lines.extend(_emit_shell_array("rustc_env_files", rustc_env_files)) + lines.extend([ + 'for env_file in "${rustc_env_files[@]}"; do', + " set -a", + " # shellcheck disable=SC1090", + ' source "$(rlocation "$env_file")"', + " set +a", + "done", + ]) + + lines.append("") + lines.extend(_emit_shell_array("extern_specs", extern_specs)) + lines.extend(_emit_shell_array("dependency_outputs", dependency_outputs)) + lines.extend(_emit_shell_array("cfg_values", crate.cfgs)) + lines.extend(_emit_shell_array("miri_flags", miri_flags)) + lines.extend(_emit_shell_array("launcher_args", ctx.attr.miri_args)) + lines.extend([ + "", + "cmd=(", + ' "${MIRI}"', + ' "--sysroot=${SYSROOT}"', + ' "--crate-name={}"'.format(crate.name), + ' "--crate-type={}"'.format(crate_type), + ' "--edition={}"'.format(crate.edition), + ' "--target=${TARGET_FLAG}"', + ' "--error-format=human"', + ' "--color=always"', + ]) + if is_test: + lines.append(' "--test"') + lines.extend([ + ' "${CRATE_ROOT}"', + ")", + 'for cfg in "${cfg_values[@]}"; do', + ' cmd+=("--cfg" "$cfg")', + "done", + 'for flag in "${miri_flags[@]}"; do', + ' cmd+=("$flag")', + "done", + 'for spec in "${extern_specs[@]}"; do', + ' name="${spec%%|*}"', + ' path="${spec#*|}"', + ' cmd+=("--extern=${name}=$(rlocation "$path")")', + "done", + 'for dep_output in "${dependency_outputs[@]}"; do', + ' cmd+=("-Ldependency=$(dirname "$(rlocation "$dep_output")")")', + "done", + ]) + if is_test: + lines.extend([ + # Tests are executed through the standard libtest harness, so the + # launcher forwards Bazel test filtering after the `--` separator. + 'cmd+=("--" "--test-threads=1")', + 'if [[ -n "${TESTBRIDGE_TEST_ONLY:-}" ]]; then', + ' cmd+=("${TESTBRIDGE_TEST_ONLY}")', + "fi", + 'for arg in "${launcher_args[@]}"; do', + ' cmd+=("$arg")', + "done", + 'if [[ "$#" -gt 0 ]]; then', + ' cmd+=("$@")', + "fi", + ]) + else: + lines.extend([ + 'if [[ ${#launcher_args[@]} -gt 0 || "$#" -gt 0 ]]; then', + ' cmd+=("--")', + ' for arg in "${launcher_args[@]}"; do', + ' cmd+=("$arg")', + " done", + ' if [[ "$#" -gt 0 ]]; then', + ' cmd+=("$@")', + " fi", + "fi", + ]) + lines.extend([ + 'exec "${cmd[@]}"', + "", + ]) + return "\n".join(lines) + +def _miri_impl(ctx, *, is_test): + # The wrapped Rust target is already re-analyzed in Miri mode by the rule + # transition; this implementation only has to assemble the final launcher + # over that rebuilt crate graph. + toolchain = find_toolchain(ctx) + miri_toolchain = ctx.toolchains[str(Label("//rust:miri_toolchain_type"))] + if not miri_toolchain: + fail("No `@rules_rust//rust:miri_toolchain_type` is registered. Register a Miri toolchain before using {}.".format(ctx.label)) + + crate = _crate_from_target(ctx.attr.crate) + dep_info = ctx.attr.crate[rust_common.dep_info] + native_runfiles = _native_runfiles(ctx, crate, dep_info) + + script = ctx.actions.declare_file(ctx.label.name + (".miri_test.sh" if is_test else ".miri_binary.sh")) + ctx.actions.write( + output = script, + content = _script_content( + ctx, + crate = crate, + dep_info = dep_info, + miri_toolchain = miri_toolchain, + is_test = is_test, + miri_flags = ctx.attr.miri_flags, + ), + is_executable = True, + ) + + # Include both compile-time Rust artifacts and the Bazel test harness + # scripts so the generated launcher behaves like a normal Bazel executable. + runfiles = ctx.runfiles( + transitive_files = depset( + transitive = [ + crate.srcs, + crate.compile_data, + dep_info.transitive_crate_outputs, + dep_info.transitive_proc_macro_data, + dep_info.transitive_data, + native_runfiles, + toolchain.all_files, + miri_toolchain.all_files, + ctx.attr._bash_runfiles[DefaultInfo].files, + ctx.attr._test_setup[DefaultInfo].files, + ctx.attr._bazel_test_setup_script[DefaultInfo].files, + ], + ), + ).merge(ctx.attr._bash_runfiles[DefaultInfo].default_runfiles).merge(ctx.attr._test_setup[DefaultInfo].default_runfiles) + + return [ + DefaultInfo(executable = script, runfiles = runfiles), + RunEnvironmentInfo( + environment = dict(ctx.attr.env), + inherited_environment = ctx.attr.env_inherit, + ), + ] + +def _miri_test_impl(ctx): + return _miri_impl(ctx, is_test = True) + +def _miri_binary_impl(ctx): + return _miri_impl(ctx, is_test = False) + +_MIRI_COMMON_ATTRS = { + "crate": attr.label( + mandatory = True, + providers = [ + [rust_common.dep_info, rust_common.crate_info], + [rust_common.dep_info, rust_common.test_crate_info], + ], + doc = dedent("""\ + Existing Rust target to execute under Miri. + + For `miri_test`, prefer wrapping an existing `rust_test` target so the + wrapped target already carries any test-only dependencies. + """), + ), + "env": attr.string_dict( + doc = "Additional runtime environment variables for the generated launcher.", + ), + "env_inherit": attr.string_list( + doc = "Runtime environment variables to inherit from the outer test/run environment.", + ), + # `miri_flags` affect the Miri driver itself, while `miri_args` are passed + # through to the interpreted test harness or binary after the `--` split. + "miri_args": attr.string_list( + doc = "Arguments baked into the generated launcher and forwarded after the libtest/program separator.", + ), + "miri_flags": attr.string_list( + default = ["-Zmiri-disable-isolation"], + doc = "Extra flags forwarded directly to the `miri` driver.", + ), + "platform": attr.label( + doc = "Optional platform to transition the wrapped target to before rebuilding its Rust dependency closure for Miri.", + default = None, + ), + "_allowlist_function_transition": attr.label( + default = Label("//tools/allowlists/function_transition_allowlist"), + ), + "_bash_runfiles": attr.label( + default = Label("@bazel_tools//tools/bash/runfiles"), + ), + "_bazel_test_setup_script": attr.label( + default = Label("@bazel_tools//tools/test:test-setup.sh"), + allow_single_file = True, + ), + "_test_setup": attr.label( + default = Label("@bazel_tools//tools/test:test_setup"), + ), +} + +# V1 keeps the public surface small: users wrap an existing rust_test target +# instead of re-declaring srcs/deps on a parallel Miri-only rule. +miri_test = rule( + implementation = _miri_test_impl, + executable = True, + fragments = ["cpp"], + test = True, + attrs = _MIRI_COMMON_ATTRS, + cfg = miri_transition, + toolchains = [ + str(Label("//rust:toolchain_type")), + str(Label("//rust:miri_toolchain_type")), + config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), + ], + doc = dedent("""\ + Executes an existing Rust target under the direct `miri` driver. + + This first version wraps an already-declared Rust target rather than mirroring the + full `rust_test` attribute surface. Wrap an existing `rust_test` target when you + need test-only dependencies to be part of the interpreted harness. + """), +) + +# `miri_binary` mirrors the same wrapper approach for runnable binary crates. +miri_binary = rule( + implementation = _miri_binary_impl, + executable = True, + fragments = ["cpp"], + attrs = _MIRI_COMMON_ATTRS, + cfg = miri_transition, + toolchains = [ + str(Label("//rust:toolchain_type")), + str(Label("//rust:miri_toolchain_type")), + config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), + ], + doc = dedent("""\ + Executes an existing `rust_binary`-like target under the direct `miri` driver. + """), +) diff --git a/rust/private/miri_config.bzl b/rust/private/miri_config.bzl new file mode 100644 index 0000000000..694c168628 --- /dev/null +++ b/rust/private/miri_config.bzl @@ -0,0 +1,42 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared Miri configuration helpers.""" + +def _miri_transition_impl(settings, attr): + # Re-analyze the wrapped Rust target in a Miri-specific configuration while + # preserving the existing target platform unless the caller overrides it. + return { + "//command_line_option:platforms": str(attr.platform) if attr.platform else settings["//command_line_option:platforms"], + "//rust/private:miri_enabled": True, + } + +miri_transition = transition( + implementation = _miri_transition_impl, + inputs = [ + "//command_line_option:platforms", + ], + outputs = [ + "//command_line_option:platforms", + "//rust/private:miri_enabled", + ], +) + +def rlocationpath(file, workspace_name): + # Generated launchers run from Bazel runfiles, so they need a stable + # rlocation path even when the file comes from an external repository. + if file.short_path.startswith("../"): + return file.short_path[len("../"):] + + return "{}/{}".format(workspace_name, file.short_path) diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl index 55d1fbb746..4f440d9ec9 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -56,6 +56,15 @@ load( # TODO(marco): Separate each rule into its own file. +# All Rust rules can optionally see the Miri toolchain so a wrapper rule can +# transition an existing crate graph into Miri mode without duplicating rule +# implementations just for that case. +_RUST_TOOLCHAINS = [ + str(Label("//rust:toolchain_type")), + config_common.toolchain_type("//rust:miri_toolchain_type", mandatory = False), + config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), +] + def _assert_no_deprecated_attributes(_ctx): """Forces a failure if any deprecated attributes were specified @@ -642,6 +651,11 @@ RUSTC_ATTRS = { "_is_proc_macro_dep_enabled": attr.label( default = Label("//rust/private:is_proc_macro_dep_enabled"), ), + # Thread the Miri build setting into the common Rust attrs so the compile + # layer can detect when a target-side crate is being rebuilt for Miri. + "_miri_enabled": attr.label( + default = Label("//rust/private:miri_enabled"), + ), "_per_crate_rustc_flag": attr.label( default = Label("//rust/settings:experimental_per_crate_rustc_flag"), ), @@ -930,10 +944,7 @@ rust_library = rule( ), }, fragments = ["cpp"], - toolchains = [ - str(Label("//rust:toolchain_type")), - config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + toolchains = _RUST_TOOLCHAINS, doc = dedent("""\ Builds a Rust library crate. @@ -1028,10 +1039,7 @@ rust_static_library = rule( }, fragments = ["cpp"], cfg = _rust_static_library_transition, - toolchains = [ - str(Label("//rust:toolchain_type")), - config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + toolchains = _RUST_TOOLCHAINS, provides = [ CcInfo, rust_common.test_crate_info, @@ -1077,10 +1085,7 @@ rust_shared_library = rule( }, fragments = ["cpp"], cfg = _rust_shared_library_transition, - toolchains = [ - str(Label("//rust:toolchain_type")), - config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + toolchains = _RUST_TOOLCHAINS, provides = [ CcInfo, rust_common.test_crate_info, @@ -1133,10 +1138,7 @@ rust_proc_macro = rule( ), ), fragments = ["cpp"], - toolchains = [ - str(Label("//rust:toolchain_type")), - config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + toolchains = _RUST_TOOLCHAINS, doc = dedent("""\ Builds a Rust proc-macro crate. """), @@ -1217,10 +1219,7 @@ rust_binary = rule( executable = True, fragments = ["cpp"], cfg = _rust_binary_transition, - toolchains = [ - str(Label("//rust:toolchain_type")), - config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + toolchains = _RUST_TOOLCHAINS, doc = dedent("""\ Builds a Rust binary crate. @@ -1360,10 +1359,7 @@ rust_binary_without_process_wrapper = rule( attrs = _common_attrs_for_binary_without_process_wrapper(_common_attrs | _rust_binary_attrs), executable = True, fragments = ["cpp"], - toolchains = [ - str(Label("//rust:toolchain_type")), - config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + toolchains = _RUST_TOOLCHAINS, ) def _rust_library_without_process_wrapper_impl(ctx): @@ -1376,10 +1372,7 @@ rust_library_without_process_wrapper = rule( provides = COMMON_PROVIDERS + [_RustBuiltWithoutProcessWrapperInfo], attrs = dict(_common_attrs_for_binary_without_process_wrapper(_common_attrs).items()), fragments = ["cpp"], - toolchains = [ - str(Label("//rust:toolchain_type")), - config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + toolchains = _RUST_TOOLCHAINS, ) def _test_attrs_for_binary_without_process_wrapper(attrs): @@ -1416,10 +1409,7 @@ rust_test_without_process_wrapper_test = rule( executable = True, fragments = ["cpp"], test = True, - toolchains = [ - str(Label("//rust:toolchain_type")), - config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + toolchains = _RUST_TOOLCHAINS, ) def _rust_test_transition_impl(settings, attr): @@ -1453,10 +1443,7 @@ rust_test = rule( fragments = ["cpp"], cfg = _rust_test_transition, test = True, - toolchains = [ - str(Label("//rust:toolchain_type")), - config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), - ], + toolchains = _RUST_TOOLCHAINS, doc = dedent("""\ Builds a Rust test crate. diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 7f301ccc30..dc46255c13 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -131,6 +131,21 @@ is_proc_macro_dep_enabled = rule( build_setting = config.bool(flag = True), ) +def _miri_enabled(attr): + return hasattr(attr, "_miri_enabled") and attr._miri_enabled[BuildSettingInfo].value + +def _find_miri_toolchain(ctx, attr): + # Host-side tools such as build scripts and proc-macros must keep using the + # normal toolchain; only target-side crates are rebuilt against the Miri + # sysroot. + if is_exec_configuration(ctx) or not _miri_enabled(attr): + return None + + toolchain = ctx.toolchains[str(Label("//rust:miri_toolchain_type"))] + if not toolchain: + fail("Rust target {} was configured for Miri, but no `@rules_rust//rust:miri_toolchain_type` is registered.".format(ctx.label)) + return toolchain + def _get_rustc_env(attr, toolchain, crate_name): """Gathers rustc environment variables @@ -217,6 +232,11 @@ def _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation return True return False +def miri_should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode): + # Keep the direct Miri launcher consistent with the normal Rust link path + # when choosing between PIC and non-PIC native libraries. + return _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode) + def _is_proc_macro(crate_info): return "proc-macro" in (crate_info.type, crate_info.wrapped_crate_type) @@ -394,6 +414,11 @@ def _collect_libs_from_linker_inputs(linker_inputs, use_pic): for lib in li.libraries ] +def miri_collect_libs_from_linker_inputs(linker_inputs, use_pic): + # The direct Miri launcher needs the same native library artifacts staged in + # runfiles as normal Rust linking would stage in the sandbox. + return _collect_libs_from_linker_inputs(linker_inputs, use_pic) + def get_cc_user_link_flags(ctx): """Get the current target's linkopt flags @@ -802,6 +827,15 @@ def collect_inputs( else: runtime_libs = cc_toolchain.static_runtime_lib(feature_configuration = feature_configuration) + miri_toolchain = _find_miri_toolchain(ctx, ctx.attr) + + # When a crate is rebuilt for Miri, Bazel must also stage the Miri sysroot + # and runtime files into the sandbox or the action will analyze correctly + # but fail once it executes. + toolchain_inputs = [toolchain.all_files] + if miri_toolchain: + toolchain_inputs.append(miri_toolchain.all_files) + nolinkstamp_compile_inputs = depset( nolinkstamp_compile_direct_inputs + ([] if experimental_use_cc_common_link else libs_from_linker_inputs), @@ -810,8 +844,7 @@ def collect_inputs( transitive_crate_outputs, crate_info.compile_data, dep_info.transitive_proc_macro_data, - toolchain.all_files, - ] + ([] if experimental_use_cc_common_link else [ + ] + toolchain_inputs + ([] if experimental_use_cc_common_link else [ runtime_libs, linker_depset, ]), @@ -1096,8 +1129,12 @@ def construct_arguments( if linker_script: rustc_flags.add(linker_script, format = "--codegen=link-arg=-T%s") - # Tell Rustc where to find the standard library (or libcore) - rustc_flags.add_all(toolchain.rust_std_paths, before_each = "-L", format_each = "%s") + # Normal Rust builds search the standard library via -L paths. In Miri + # mode that would be wrong, because target-side crates must be rebuilt + # against the dedicated Miri sysroot instead. + miri_toolchain = _find_miri_toolchain(ctx, attr) + if not miri_toolchain: + rustc_flags.add_all(toolchain.rust_std_paths, before_each = "-L", format_each = "%s") rustc_flags.add_all(rust_flags, map_each = map_flag) # Gather data path from crate_info since it is inherited from real crate for rust_doc and rust_test @@ -1203,8 +1240,11 @@ def construct_arguments( {}, )) - # Ensure the sysroot is set for the target platform - if toolchain._toolchain_generated_sysroot: + # Point target-side crates at the Miri sysroot so their metadata and std + # linkage match what the direct miri driver will interpret later on. + if miri_toolchain: + rustc_flags.add(miri_toolchain.sysroot, format = "--sysroot=%s") + elif toolchain._toolchain_generated_sysroot: rustc_flags.add(toolchain.sysroot, format = "--sysroot=%s") if toolchain._rename_first_party_crates: @@ -1284,6 +1324,11 @@ def collect_extra_rustc_flags(ctx, toolchain, crate_root, crate_type): if hasattr(ctx.attr, "_extra_exec_rustc_flag") and is_exec: flags.extend(ctx.attr._extra_exec_rustc_flag[ExtraExecRustcFlagsInfo].extra_exec_rustc_flags) + if not is_exec and _miri_enabled(ctx.attr): + # Miri may need MIR bodies from transitive dependencies at runtime, so + # target-side crates must always encode MIR in this mode. + flags.append("-Zalways-encode-mir") + return flags def rustc_compile_action( diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl index 91e3032166..144b2eec5c 100644 --- a/rust/toolchain.bzl +++ b/rust/toolchain.bzl @@ -951,3 +951,52 @@ The `select()` is evaluated against the target platform before the exec transiti allowing platform-specific linker selection while ensuring the selected linker is built for the exec platform. """, ) + +def _rust_miri_toolchain_impl(ctx): + # The Miri launcher needs both the driver and a prebuilt sysroot available + # in runfiles; the anchor file gives it a stable way to locate that sysroot + # directory at runtime. + sysroot_anchor = ctx.file.sysroot_anchor + sysroot_path = sysroot_anchor.dirname + sysroot_short_path, _, _ = sysroot_anchor.short_path.rpartition("/") + + transitive_inputs = [ctx.attr.sysroot_files[DefaultInfo].files] + if ctx.attr.runtime_files: + transitive_inputs.append(ctx.attr.runtime_files[DefaultInfo].files) + + return [platform_common.ToolchainInfo( + all_files = depset([ctx.executable.miri, sysroot_anchor], transitive = transitive_inputs), + env = ctx.attr.env, + miri = ctx.executable.miri, + sysroot = sysroot_path, + sysroot_anchor = sysroot_anchor, + sysroot_anchor_rlocationpath = sysroot_short_path + "/" + sysroot_anchor.basename if sysroot_short_path else sysroot_anchor.basename, + sysroot_short_path = sysroot_short_path, + )] + +rust_miri_toolchain = rule( + implementation = _rust_miri_toolchain_impl, + attrs = { + "env": attr.string_dict(default = {}), + "miri": attr.label( + mandatory = True, + executable = True, + allow_single_file = True, + cfg = "exec", + ), + # Miri is executed at test/run time, so shared libraries and other + # runtime-only files must ride along in the toolchain runfiles as well. + "runtime_files": attr.label( + allow_files = True, + ), + "sysroot_anchor": attr.label( + mandatory = True, + allow_single_file = True, + ), + "sysroot_files": attr.label( + mandatory = True, + allow_files = True, + ), + }, + doc = "Declares a Miri toolchain containing the `miri` driver and a prebuilt Miri sysroot.", +)