diff --git a/rust/BUILD.bazel b/rust/BUILD.bazel index e26955f3b9..f712bd5532 100644 --- a/rust/BUILD.bazel +++ b/rust/BUILD.bazel @@ -11,6 +11,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 ce935ba6d0..e858b4c37d 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,15 @@ 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, + 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 8f2aa464af..6f68aec4ba 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -57,6 +57,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 @@ -626,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"), ), @@ -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. @@ -1040,10 +1051,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 +1089,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 +1112,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 +1177,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 +1317,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 +1330,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 +1387,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 +1413,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..684845042c 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -101,6 +101,21 @@ 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): + # 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 @@ -182,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) @@ -359,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 @@ -773,6 +798,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), @@ -781,8 +815,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 +1212,21 @@ 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, - ) + # 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, + 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 +1355,17 @@ 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. + # 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], + 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 +1477,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 c62263daf7..135f6be596 100644 --- a/rust/toolchain.bzl +++ b/rust/toolchain.bzl @@ -992,3 +992,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.", +)