Conversation
### 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>
Added comments into the code for better understanding Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
- 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>
|
I added you two as reviewers here as you also contributed to this repo. If you think it's okay I can click approve if needed, but beyond that I can't do much here. |
There was a problem hiding this comment.
Pull request overview
Adds first-class Miri support to rules_rust by introducing wrapper rules that re-analyze an existing Rust target graph in a Miri-specific mode and execute it via the direct miri driver, backed by a dedicated Miri toolchain type.
Changes:
- Introduces
miri_test/miri_binarywrapper rules that generate runfiles-based launchers invoking the directmiridriver. - Adds a
miri_toolchain_typeandrust_miri_toolchainrule to model Miri’s driver + sysroot + runtime closure separately from the normal Rust toolchain. - Adds a Miri build setting + transition and updates Rust compilation to rebuild target-side crates against the Miri sysroot and include
-Zalways-encode-mir.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| rust/toolchain.bzl | Adds rust_miri_toolchain rule producing ToolchainInfo for Miri driver + sysroot inputs. |
| rust/private/rustc.bzl | Plumbs Miri mode into compile inputs/args: sysroot selection, -Zalways-encode-mir, toolchain lookup helpers. |
| rust/private/rust.bzl | Makes all Rust rules optionally aware of the Miri toolchain and threads the Miri build setting through common attrs. |
| rust/private/miri.bzl | Implements miri_test/miri_binary rules and runfiles-based launcher script generation. |
| rust/private/miri_config.bzl | Adds the Miri transition and rlocationpath helper for runfiles resolution. |
| rust/private/BUILD.bazel | Declares the miri_enabled build setting (bool_flag). |
| rust/defs.bzl | Exports miri_test and miri_binary as public rules. |
| rust/BUILD.bazel | Adds miri_toolchain_type. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| transitive_inputs = [ctx.attr.sysroot_files[DefaultInfo].files] | ||
| if ctx.attr.runtime_files: | ||
| transitive_inputs.append(ctx.attr.runtime_files[DefaultInfo].files) |
There was a problem hiding this comment.
sysroot_files/runtime_files are declared as attr.label(allow_files = True), but the implementation assumes these are rule targets by indexing [DefaultInfo].files. If a user passes a single file label, this will fail at analysis time. Consider using ctx.files.sysroot_files / ctx.files.runtime_files (and wrapping in a depset) so both file and filegroup-like targets are supported.
| transitive_inputs = [ctx.attr.sysroot_files[DefaultInfo].files] | |
| if ctx.attr.runtime_files: | |
| transitive_inputs.append(ctx.attr.runtime_files[DefaultInfo].files) | |
| transitive_inputs = [depset(ctx.files.sysroot_files)] | |
| if ctx.files.runtime_files: | |
| transitive_inputs.append(depset(ctx.files.runtime_files)) |
| return [ | ||
| DefaultInfo(executable = script, runfiles = runfiles), | ||
| RunEnvironmentInfo( | ||
| environment = dict(ctx.attr.env), |
There was a problem hiding this comment.
The rust_miri_toolchain exposes an env dict, but miri_test/miri_binary currently ignore it and only use ctx.attr.env. If the Miri driver requires runtime env (e.g. loader paths), this will silently break. Merge miri_toolchain.env (and possibly the Rust toolchain env if needed) into RunEnvironmentInfo.environment, with a clear precedence rule.
| return [ | |
| DefaultInfo(executable = script, runfiles = runfiles), | |
| RunEnvironmentInfo( | |
| environment = dict(ctx.attr.env), | |
| # Merge environment variables from the Rust toolchain, the Miri toolchain, | |
| # and the rule's own `env` attribute. Precedence (later overrides earlier): | |
| # 1. Rust toolchain env | |
| # 2. Miri toolchain env | |
| # 3. User-specified ctx.attr.env | |
| merged_env = {} | |
| if hasattr(toolchain, "env"): | |
| merged_env.update(toolchain.env) | |
| if hasattr(miri_toolchain, "env"): | |
| merged_env.update(miri_toolchain.env) | |
| merged_env.update(ctx.attr.env) | |
| return [ | |
| DefaultInfo(executable = script, runfiles = runfiles), | |
| RunEnvironmentInfo( | |
| environment = merged_env, |
| 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], | ||
| ) |
There was a problem hiding this comment.
PR description says the direct-driver implementation rejects targets with native linker inputs, but _native_runfiles currently stages dep_info.transitive_noncrates instead of failing. Either update the description or enforce the limitation by fail()ing when transitive_noncrates is non-empty (and keep the staging logic only once native/mixed-mode support is actually implemented).
| # 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("""\ |
There was a problem hiding this comment.
New public rules + transition (miri_test, miri_binary, miri_transition) introduce non-trivial behavior (toolchain resolution, runfiles staging, command-line reconstruction) but this PR doesn't add repository tests exercising them. Add at least one integration test that wraps a simple rust_test and verifies it runs under the generated launcher (and one negative test that fails analysis when no Miri toolchain is registered).
|
We've been using cargo miri for Eclipse Kuksa and for future migration to a Bazel/S-Core setup, this PR seems to be helpful for Kuksa as well. |
Summary
This PR adds first-class Miri support to
rules_rustthrough two new public rules:miri_testmiri_binaryThe implementation uses the direct
miridriver, notcargo miri.That means Bazel remains in control of the crate graph and action inputs, while Miri is only used for the final interpreted
execution step.
What This PR Adds
1. New public rules
The following rules are now exported from
@rules_rust//rust:defs.bzl:miri_testmiri_binaryThese are currently wrapper rules around already-declared Rust targets.
Example:
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:
This is separate from the normal Rust toolchain because Miri requires a different runtime contract:
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:
Host-side units are not rebuilt in Miri mode:
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:
For miri_test, the generated launcher also forwards libtest-style filtering and runs with:
Why This Is Structured as a Separate Toolchain
Miri is not just another flag on the normal Rust toolchain.
The normal Rust toolchain defines:
The Miri toolchain defines:
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:
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:
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:
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:
but not targets that pull in native link artifacts such as:
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:
Result: