From d976b7b5597e1146aec56ef29eee78836374acbc Mon Sep 17 00:00:00 2001 From: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:53:54 +0300 Subject: [PATCH 1/3] miri: added support for MIRI ### 1. New public rules The following rules are now exported from `@rules_rust//rust:defs.bzl`: - `miri_test` - `miri_binary` These are currently **wrapper rules** around already-declared Rust targets. Example: ```bzl load("@rules_rust//rust:defs.bzl", "rust_test", "miri_test") rust_test( name = "tests", crate = "my_lib", ) miri_test( name = "tests_miri", crate = ":tests", ) This first version intentionally wraps existing Rust targets instead of re-exposing the full rust_test / rust_binary attribute surface. ### 2. New Miri toolchain type A new toolchain type is introduced: - @rules_rust//rust:miri_toolchain_type This is separate from the normal Rust toolchain because Miri requires a different runtime contract: - bin/miri - a prebuilt Miri sysroot - the runtime files needed to execute the Miri driver inside Bazel runfiles/sandbox ### 3. Miri-specific rebuild mode for target-side crates A new internal build setting / transition enables a Miri-specific compilation mode for target-side Rust crates. When enabled: - target-side crates are rebuilt with the Miri sysroot - target-side crates are compiled with: - -Zalways-encode-mir Host-side units are not rebuilt in Miri mode: - build scripts remain native - proc-macro crates remain native - other exec-configuration helpers remain native This split is required for real projects to work correctly under Miri. ### 4. Direct miri launchers miri_test and miri_binary generate Bazel launchers that invoke the direct miri driver with a rustc-shaped command line: - --sysroot=... - --crate-name=... - --crate-type=... - --edition=... - --target=... - --extern=... - -Ldependency=... For miri_test, the generated launcher also forwards libtest-style filtering and runs with: - --test - --test-threads=1 ## Why This Is Structured as a Separate Toolchain Miri is not just another flag on the normal Rust toolchain. The normal Rust toolchain defines: - rustc - normal std/sysroot - normal compile-time/runtime inputs The Miri toolchain defines: - miri - a Miri-specific sysroot - Miri runtime files Keeping these as separate toolchain types makes the model explicit and keeps normal Rust builds unchanged for users who do not use Miri. ## How Consuming Repos Use It Consuming repos need two things: 1. a normal Rust toolchain 2. a registered Miri toolchain In our S-CORE ecosyste, the Miri toolchain is provided by score_toolchains_rust. ### 1. Register the Rust and Miri toolchains Example .bazelrc configuration: build:per-x86_64-linux --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_unknown_linux_gnu build:per-x86_64-linux --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_unknown_linux_gnu_miri The first toolchain is the normal Ferrocene Rust toolchain. The second toolchain provides: - miri - the prebuilt Miri sysroot - runtime files required by the Miri launcher ### 2. Wrap existing Rust targets #### Tests load("@rules_rust//rust:defs.bzl", "rust_test", "miri_test") rust_test( name = "tests", crate = "my_lib", ) miri_test( name = "tests_miri", crate = ":tests", tags = ["manual"], ) #### Binaries load("@rules_rust//rust:defs.bzl", "rust_binary", "miri_binary") rust_binary( name = "app", srcs = ["src/main.rs"], ) miri_binary( name = "app_miri", crate = ":app", tags = ["manual"], ) ### 3. Run them with Bazel bazel test --config=per-x86_64-linux //path/to:tests_miri bazel run --config=per-x86_64-linux //path/to:app_miri ## Rule Parameters ### miri_test Current useful parameters: - crate - existing Rust target to execute under Miri - typically wrap a rust_test(...) - miri_flags - flags passed directly to the miri driver - defaults to: - -Zmiri-disable-isolation - miri_args - arguments forwarded after -- - for tests, this is useful for libtest filtering - env - extra runtime environment variables - env_inherit - runtime environment variables inherited from the outer environment - platform - optional platform override for the wrapped target before Miri-mode rebuild Example with a test filter: miri_test( name = "tests_miri_json_backend", crate = ":tests", miri_args = ["json_backend::"], tags = ["manual"], ) ### miri_binary Current useful parameters are the same as miri_test, except it wraps a rust_binary-like target and executes it under Miri. ## Current Limitations This PR is intentionally scoped as a first version. ### 1. Not full rust_test / rust_binary parity These rules do not yet expose the entire attribute surface of rust_test() or rust_binary(). They currently wrap existing Rust targets instead. ### 2. Pure-Rust graphs only The current direct-driver implementation rejects targets with native linker inputs. That means this version supports: - pure-Rust test targets - pure-Rust binaries but not targets that pull in native link artifacts such as: - cc_library - other non-Rust linker inputs ### 3. Requires a registered Miri toolchain If miri_test or miri_binary is used without registering a Miri toolchain, analysis fails with a clear error. ## Validation This implementation was validated end-to-end against a real consumer repo (persistency) using a Ferrocene-backed Miri toolchain from score_toolchains_rust. Validated flow: - wrap an existing rust_test(...) target with miri_test(...) - run it with Bazel - execute the full rust_kvs test harness under Miri Result: - all non-ignored tests passed under Miri - the sharded Miri suite also passed Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> --- rust/BUILD.bazel | 4 + rust/defs.bzl | 11 ++ rust/private/BUILD.bazel | 7 + rust/private/miri.bzl | 323 +++++++++++++++++++++++++++++++++++ rust/private/miri_config.bzl | 38 +++++ rust/private/rust.bzl | 54 ++---- rust/private/rustc.bzl | 50 ++++-- rust/toolchain.bzl | 44 +++++ 8 files changed, 484 insertions(+), 47 deletions(-) create mode 100644 rust/private/miri.bzl create mode 100644 rust/private/miri_config.bzl diff --git a/rust/BUILD.bazel b/rust/BUILD.bazel index e26955f3b9..8a64aefac1 100644 --- a/rust/BUILD.bazel +++ b/rust/BUILD.bazel @@ -11,6 +11,10 @@ toolchain_type( name = "toolchain_type", ) +toolchain_type( + name = "miri_toolchain_type", +) + alias( name = "toolchain", actual = "toolchain_type", diff --git a/rust/defs.bzl b/rust/defs.bzl index 4f2ef72582..182f6438c5 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,12 @@ rust_test = _rust_test rust_test_suite = _rust_test_suite # See @rules_rust//rust/private:rust.bzl for a complete description. +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 ce935ba6d0..8ac9862c63 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") # Exported for docs @@ -31,6 +32,12 @@ bzl_library( ], ) +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..73f31fe8fd --- /dev/null +++ b/rust/private/miri.bzl @@ -0,0 +1,323 @@ +# 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:utils.bzl", "dedent", "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): + 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 _script_content(ctx, *, crate, dep_info, miri_toolchain, is_test, miri_flags): + 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)) + + if dep_info.transitive_noncrates.to_list(): + fail("{} depends on native linker inputs. Direct `miri` execution is only supported for pure-Rust dependency graphs right now.".format(ctx.attr.crate.label)) + + 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", + "", + "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}\")", + "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([ + '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): + 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] + + 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, + ) + + 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, + 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._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_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"), + ), + "_test_setup": attr.label( + default = Label("@bazel_tools//tools/test:test_setup"), + ), + "_bazel_test_setup_script": attr.label( + default = Label("@bazel_tools//tools/test:test-setup.sh"), + allow_single_file = True, + ), +} + +miri_test = rule( + implementation = _miri_test_impl, + executable = True, + test = True, + attrs = _MIRI_COMMON_ATTRS, + cfg = miri_transition, + toolchains = [ + str(Label("//rust:toolchain_type")), + str(Label("//rust:miri_toolchain_type")), + ], + 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 = rule( + implementation = _miri_binary_impl, + executable = True, + attrs = _MIRI_COMMON_ATTRS, + cfg = miri_transition, + toolchains = [ + str(Label("//rust:toolchain_type")), + str(Label("//rust:miri_toolchain_type")), + ], + 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..b85f5bf2a6 --- /dev/null +++ b/rust/private/miri_config.bzl @@ -0,0 +1,38 @@ +# 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): + 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): + 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 8f2aa464af..582cbe28d9 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -57,6 +57,12 @@ load( # TODO(marco): Separate each rule into its own file. +_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 @@ -639,6 +645,9 @@ RUSTC_ATTRS = { "_rustc_output_diagnostics": attr.label( default = Label("//rust/settings:rustc_output_diagnostics"), ), + "_miri_enabled": attr.label( + default = Label("//rust/private:miri_enabled"), + ), } _COMMON_ATTRS = { @@ -930,10 +939,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. @@ -1040,10 +1046,7 @@ rust_static_library = rule( attrs = _COMMON_ATTRS | _PLATFORM_ATTRS, 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, @@ -1081,10 +1084,7 @@ rust_shared_library = rule( attrs = _COMMON_ATTRS | _PLATFORM_ATTRS | _EXPERIMENTAL_USE_CC_COMMON_LINK_ATTRS, 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, @@ -1107,10 +1107,7 @@ rust_proc_macro = rule( provides = COMMON_PROVIDERS, attrs = _COMMON_ATTRS, 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. """), @@ -1175,10 +1172,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. @@ -1318,10 +1312,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): @@ -1334,10 +1325,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 _rust_static_library_without_process_wrapper_impl(ctx): @@ -1394,10 +1382,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): @@ -1423,10 +1408,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 b9687c5deb..b5139b314e 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -101,6 +101,17 @@ PerCrateRustcFlagsInfo = provider( fields = {"per_crate_rustc_flags": "List[string] Extra flags to pass to rustc in non-exec configuration"}, ) +def _miri_enabled(attr): + return hasattr(attr, "_miri_enabled") and attr._miri_enabled[BuildSettingInfo].value + +def _find_miri_toolchain(ctx, attr): + 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 @@ -773,6 +784,12 @@ def collect_inputs( else: runtime_libs = cc_toolchain.static_runtime_lib(feature_configuration = feature_configuration) + miri_toolchain = _find_miri_toolchain(ctx, ctx.attr) + + 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), @@ -781,8 +798,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, ]), @@ -1179,15 +1195,18 @@ def construct_arguments( if linker_script: rustc_flags.add(linker_script, format = "--codegen=link-arg=-T%s") + miri_toolchain = _find_miri_toolchain(ctx, attr) + # Tell Rustc where to find the standard library (or libcore). Use the # underlying `File`s with a `map_each` so Bazel's path mapping # (`--experimental_output_paths=strip`) can rewrite the dirnames. - rustc_flags.add_all( - toolchain.rust_std, - before_each = "-L", - map_each = _get_dirname, - uniquify = True, - ) + if not miri_toolchain: + rustc_flags.add_all( + toolchain.rust_std, + before_each = "-L", + map_each = _get_dirname, + uniquify = True, + ) # `rust_flags` is either a plain `list[str]` or a `ctx.actions.args()` # `Args` object. Lists are folded into the main `rustc_flags` `Args` @@ -1316,9 +1335,15 @@ def construct_arguments( )) # Ensure the sysroot is set for the target platform. Compute the dirname - # from the underlying `sysroot_anchor` `File` via `map_each` so Bazel's - # path mapping can rewrite it. - if toolchain._toolchain_generated_sysroot: + # from the underlying anchor `File` via `map_each` so Bazel's path mapping + # can rewrite it. + if miri_toolchain: + rustc_flags.add_all( + [miri_toolchain.sysroot_anchor], + map_each = _get_dirname, + format_each = "--sysroot=%s", + ) + elif toolchain._toolchain_generated_sysroot: rustc_flags.add_all( [toolchain.sysroot_anchor], map_each = _get_dirname, @@ -1430,6 +1455,9 @@ 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): + flags.append("-Zalways-encode-mir") + return flags def rustc_compile_action( diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl index c62263daf7..d535276a9e 100644 --- a/rust/toolchain.bzl +++ b/rust/toolchain.bzl @@ -992,3 +992,47 @@ 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): + 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", + ), + "sysroot_anchor": attr.label( + mandatory = True, + allow_single_file = True, + ), + "sysroot_files": attr.label( + mandatory = True, + allow_files = True, + ), + "runtime_files": attr.label( + allow_files = True, + ), + }, + doc = "Declares a Miri toolchain containing the `miri` driver and a prebuilt Miri sysroot.", +) From aee3593f8a6655e964b083b8e332d1ee0ec2854b Mon Sep 17 00:00:00 2001 From: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:39:16 +0300 Subject: [PATCH 2/3] miri: added comments Added comments into the code for better understanding Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> --- rust/BUILD.bazel | 2 ++ rust/defs.bzl | 2 ++ rust/private/BUILD.bazel | 3 +++ rust/private/miri.bzl | 31 ++++++++++++++++++++++++++++--- rust/private/miri_config.bzl | 4 ++++ rust/private/rust.bzl | 11 ++++++++--- rust/private/rustc.bzl | 14 ++++++++++++++ rust/toolchain.bzl | 11 ++++++++--- 8 files changed, 69 insertions(+), 9 deletions(-) diff --git a/rust/BUILD.bazel b/rust/BUILD.bazel index 8a64aefac1..f712bd5532 100644 --- a/rust/BUILD.bazel +++ b/rust/BUILD.bazel @@ -11,6 +11,8 @@ 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", ) diff --git a/rust/defs.bzl b/rust/defs.bzl index 182f6438c5..6368135e0c 100644 --- a/rust/defs.bzl +++ b/rust/defs.bzl @@ -107,6 +107,8 @@ 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. diff --git a/rust/private/BUILD.bazel b/rust/private/BUILD.bazel index 8ac9862c63..e858b4c37d 100644 --- a/rust/private/BUILD.bazel +++ b/rust/private/BUILD.bazel @@ -32,6 +32,9 @@ bzl_library( ], ) +# 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, diff --git a/rust/private/miri.bzl b/rust/private/miri.bzl index 73f31fe8fd..1a278aad85 100644 --- a/rust/private/miri.bzl +++ b/rust/private/miri.bzl @@ -33,6 +33,8 @@ 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 @@ -59,6 +61,9 @@ def _target_flag_lines(ctx, toolchain): return ["TARGET_FLAG={}".format(_shell_quote(toolchain.target_flag_value))] 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 @@ -66,8 +71,14 @@ def _script_content(ctx, *, crate, dep_info, miri_toolchain, is_test, miri_flags fail("miri_binary requires a wrapped `rust_binary`-like target. {} has crate type {}".format(ctx.attr.crate.label, crate.type)) if dep_info.transitive_noncrates.to_list(): + # The current launcher only knows how to feed Rust crate artifacts to + # Miri. Mixed Rust/native graphs need extra modeling that this V1 does + # not implement yet. fail("{} depends on native linker inputs. Direct `miri` execution is only supported for pure-Rust dependency graphs right now.".format(ctx.attr.crate.label)) + # 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) @@ -96,6 +107,8 @@ def _script_content(ctx, *, crate, dep_info, miri_toolchain, is_test, miri_flags "", "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))), @@ -160,6 +173,8 @@ def _script_content(ctx, *, crate, dep_info, miri_toolchain, is_test, miri_flags ]) 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}")', @@ -190,6 +205,9 @@ def _script_content(ctx, *, crate, dep_info, miri_toolchain, is_test, miri_flags 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: @@ -212,6 +230,8 @@ def _miri_impl(ctx, *, is_test): 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 = [ @@ -263,6 +283,8 @@ _MIRI_COMMON_ATTRS = { "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.", ), @@ -280,15 +302,17 @@ _MIRI_COMMON_ATTRS = { "_bash_runfiles": attr.label( default = Label("@bazel_tools//tools/bash/runfiles"), ), - "_test_setup": attr.label( - default = Label("@bazel_tools//tools/test:test_setup"), - ), "_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, @@ -308,6 +332,7 @@ miri_test = rule( """), ) +# `miri_binary` mirrors the same wrapper approach for runnable binary crates. miri_binary = rule( implementation = _miri_binary_impl, executable = True, diff --git a/rust/private/miri_config.bzl b/rust/private/miri_config.bzl index b85f5bf2a6..694c168628 100644 --- a/rust/private/miri_config.bzl +++ b/rust/private/miri_config.bzl @@ -15,6 +15,8 @@ """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, @@ -32,6 +34,8 @@ miri_transition = transition( ) 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("../"):] diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl index 582cbe28d9..6f68aec4ba 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -57,6 +57,9 @@ 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), @@ -632,6 +635,11 @@ RUSTC_ATTRS = { "_extra_rustc_flags": attr.label( default = Label("//rust/settings:extra_rustc_flags"), ), + # 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"), ), @@ -645,9 +653,6 @@ RUSTC_ATTRS = { "_rustc_output_diagnostics": attr.label( default = Label("//rust/settings:rustc_output_diagnostics"), ), - "_miri_enabled": attr.label( - default = Label("//rust/private:miri_enabled"), - ), } _COMMON_ATTRS = { diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index b5139b314e..802e7f511d 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -105,6 +105,9 @@ 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 @@ -112,6 +115,7 @@ def _find_miri_toolchain(ctx, attr): 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 @@ -786,6 +790,9 @@ def collect_inputs( 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) @@ -1200,6 +1207,9 @@ def construct_arguments( # Tell Rustc where to find the standard library (or libcore). Use the # underlying `File`s with a `map_each` so Bazel's path mapping # (`--experimental_output_paths=strip`) can rewrite the dirnames. + # 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. if not miri_toolchain: rustc_flags.add_all( toolchain.rust_std, @@ -1337,6 +1347,8 @@ def construct_arguments( # Ensure the sysroot is set for the target platform. Compute the dirname # from the underlying anchor `File` via `map_each` so Bazel's path mapping # can rewrite it. + # 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_all( [miri_toolchain.sysroot_anchor], @@ -1456,6 +1468,8 @@ def collect_extra_rustc_flags(ctx, toolchain, crate_root, crate_type): 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 diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl index d535276a9e..135f6be596 100644 --- a/rust/toolchain.bzl +++ b/rust/toolchain.bzl @@ -994,6 +994,9 @@ allowing platform-specific linker selection while ensuring the selected linker i ) 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("/") @@ -1022,6 +1025,11 @@ rust_miri_toolchain = rule( 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, @@ -1030,9 +1038,6 @@ rust_miri_toolchain = rule( mandatory = True, allow_files = True, ), - "runtime_files": attr.label( - allow_files = True, - ), }, doc = "Declares a Miri toolchain containing the `miri` driver and a prebuilt Miri sysroot.", ) From e902748b1dd08dcad3bdd08a244dc1321055fa3b Mon Sep 17 00:00:00 2001 From: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:56:50 +0300 Subject: [PATCH 3/3] miri: Support native inputs in direct Miri launchers - stage native linker inputs and C++ runtime libs in Miri runfiles - allow miri_test and miri_binary to inspect the C++ toolchain - reuse rustc native library selection helpers for PIC/runtime handling - export RULES_RUST_MIRI from the launcher for runtime-specific workarounds - fix Bazel test runfiles initialization for the Miri launcher Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com> --- rust/private/miri.bzl | 52 ++++++++++++++++++++++++++++++++++++------ rust/private/rustc.bzl | 10 ++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/rust/private/miri.bzl b/rust/private/miri.bzl index 1a278aad85..48b05ced29 100644 --- a/rust/private/miri.bzl +++ b/rust/private/miri.bzl @@ -16,7 +16,12 @@ load("//rust/private:common.bzl", "rust_common") load("//rust/private:miri_config.bzl", "miri_transition", "rlocationpath") -load("//rust/private:utils.bzl", "dedent", "find_toolchain") +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 @@ -60,6 +65,31 @@ def _target_flag_lines(ctx, toolchain): 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 @@ -70,11 +100,9 @@ def _script_content(ctx, *, crate, dep_info, miri_toolchain, is_test, miri_flags 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)) - if dep_info.transitive_noncrates.to_list(): - # The current launcher only knows how to feed Rust crate artifacts to - # Miri. Mixed Rust/native graphs need extra modeling that this V1 does - # not implement yet. - fail("{} depends on native linker inputs. Direct `miri` execution is only supported for pure-Rust dependency graphs right now.".format(ctx.attr.crate.label)) + # 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 @@ -117,6 +145,10 @@ def _script_content(ctx, *, crate, dep_info, miri_toolchain, is_test, miri_flags 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) @@ -215,6 +247,7 @@ def _miri_impl(ctx, *, is_test): 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( @@ -240,6 +273,7 @@ def _miri_impl(ctx, *, is_test): 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, @@ -247,7 +281,7 @@ def _miri_impl(ctx, *, is_test): ctx.attr._bazel_test_setup_script[DefaultInfo].files, ], ), - ).merge(ctx.attr._test_setup[DefaultInfo].default_runfiles) + ).merge(ctx.attr._bash_runfiles[DefaultInfo].default_runfiles).merge(ctx.attr._test_setup[DefaultInfo].default_runfiles) return [ DefaultInfo(executable = script, runfiles = runfiles), @@ -316,12 +350,14 @@ _MIRI_COMMON_ATTRS = { 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. @@ -336,11 +372,13 @@ miri_test = rule( 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/rustc.bzl b/rust/private/rustc.bzl index 802e7f511d..684845042c 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -197,6 +197,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) @@ -374,6 +379,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