Skip to content

Commit 6f65d83

Browse files
committed
Auto merge of #123317 - RalfJung:test-in-miri, r=m-ou-se,saethlin,onur-ozkan
Support running library tests in Miri This adds a new bootstrap subcommand `./x.py miri` which can test libraries in Miri. This is in preparation for eventually doing that as part of bors CI, but this PR only adds the infrastructure, and doesn't enable it yet. `@rust-lang/bootstrap` should this be `x.py test --miri library/core` or `x.py miri library/core`? The flag has the advantage that we don't have to copy all the arguments from `Subcommand::Test`. It has the disadvantage that most test steps just ignore `--miri` and still run tests the regular way. For clippy you went the route of making it a separate subcommand. ~~I went with a flag now as that seemed easier, but I can change this.~~ I made it a new subcommand. Note however that the regular cargo invocation would be `cargo miri test ...`, so `x.py` is still going to be different in that the `test` is omitted. That said, we could also make it `./x.py miri-test` to make that difference smaller -- that's in fact more consistent with the internal name of the command when bootstrap invokes cargo. `@rust-lang/libs` ~~unfortunately this PR does some unholy things to the `lib.rs` files of our library crates.~~ `@m-ou-se` found a way that entirely avoids library-level hacks, except for some new small `lib.miri.rs` files that hopefully you will never have to touch. There's a new hack in cargo-miri but there it is in good company...
2 parents 3e496a0 + f5eae9c commit 6f65d83

File tree

3 files changed

+90
-34
lines changed

3 files changed

+90
-34
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ binaries, and as such worth documenting:
507507
crate currently being compiled.
508508
* `MIRI_ORIG_RUSTDOC` is set and read by different phases of `cargo-miri` to remember the
509509
value of `RUSTDOC` from before it was overwritten.
510+
* `MIRI_REPLACE_LIBRS_IF_NOT_TEST` when set to any value enables a hack that helps bootstrap
511+
run the standard library tests in Miri.
510512
* `MIRI_VERBOSE` when set to any value tells the various `cargo-miri` phases to
511513
perform verbose logging.
512514
* `MIRI_HOST_SYSROOT` is set by bootstrap to tell `cargo-miri` which sysroot to use for *host*

cargo-miri/src/phases.rs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::env;
44
use std::fs::{self, File};
55
use std::io::BufReader;
6-
use std::path::PathBuf;
6+
use std::path::{Path, PathBuf};
77
use std::process::Command;
88

99
use rustc_version::VersionMeta;
@@ -412,9 +412,25 @@ pub fn phase_rustc(mut args: impl Iterator<Item = String>, phase: RustcPhase) {
412412
// Arguments are treated very differently depending on whether this crate is
413413
// for interpretation by Miri, or for use by a build script / proc macro.
414414
if target_crate {
415-
// Forward arguments, but remove "link" from "--emit" to make this a check-only build.
415+
// Forward arguments, but patched.
416416
let emit_flag = "--emit";
417+
// This hack helps bootstrap run standard library tests in Miri. The issue is as follows:
418+
// when running `cargo miri test` on libcore, cargo builds a local copy of core and makes it
419+
// a dependency of the integration test crate. This copy duplicates all the lang items, so
420+
// the build fails. (Regular testing avoids this because the sysroot is a literal copy of
421+
// what `cargo build` produces, but since Miri builds its own sysroot this does not work for
422+
// us.) So we need to make it so that the locally built libcore contains all the items from
423+
// `core`, but does not re-define them -- we want to replace the entire crate but a
424+
// re-export of the sysroot crate. We do this by swapping out the source file: if
425+
// `MIRI_REPLACE_LIBRS_IF_NOT_TEST` is set and we are building a `lib.rs` file, and a
426+
// `lib.miri.rs` file exists in the same folder, we build that instead. But crucially we
427+
// only do that for the library, not the unit test crate (which would be runnable) or
428+
// rustdoc (which would have a different `phase`).
429+
let replace_librs = env::var_os("MIRI_REPLACE_LIBRS_IF_NOT_TEST").is_some()
430+
&& !runnable_crate
431+
&& phase == RustcPhase::Build;
417432
while let Some(arg) = args.next() {
433+
// Patch `--emit`: remove "link" from "--emit" to make this a check-only build.
418434
if let Some(val) = arg.strip_prefix(emit_flag) {
419435
// Patch this argument. First, extract its value.
420436
let val =
@@ -429,13 +445,36 @@ pub fn phase_rustc(mut args: impl Iterator<Item = String>, phase: RustcPhase) {
429445
}
430446
}
431447
cmd.arg(format!("{emit_flag}={}", val.join(",")));
432-
} else if arg == "--extern" {
433-
// Patch `--extern` filenames, since Cargo sometimes passes stub `.rlib` files:
434-
// https://github.com/rust-lang/miri/issues/1705
448+
continue;
449+
}
450+
// Patch `--extern` filenames, since Cargo sometimes passes stub `.rlib` files:
451+
// https://github.com/rust-lang/miri/issues/1705
452+
if arg == "--extern" {
435453
forward_patched_extern_arg(&mut args, &mut cmd);
436-
} else {
437-
cmd.arg(arg);
454+
continue;
438455
}
456+
// If the REPLACE_LIBRS hack is enabled and we are building a `lib.rs` file, and a
457+
// `lib.miri.rs` file exists, then build that instead. We only consider relative paths
458+
// as cargo uses those for files in the workspace; dependencies from crates.io get
459+
// absolute paths.
460+
if replace_librs {
461+
let path = Path::new(&arg);
462+
if path.is_relative()
463+
&& path.file_name().is_some_and(|f| f == "lib.rs")
464+
&& path.is_file()
465+
{
466+
let miri_rs = Path::new(&arg).with_extension("miri.rs");
467+
if miri_rs.is_file() {
468+
if verbose > 0 {
469+
eprintln!("Performing REPLACE_LIBRS hack: {arg:?} -> {miri_rs:?}");
470+
}
471+
cmd.arg(miri_rs);
472+
continue;
473+
}
474+
}
475+
}
476+
// Fallback: just propagate the argument.
477+
cmd.arg(arg);
439478
}
440479

441480
// During setup, patch the panic runtime for `libpanic_abort` (mirroring what bootstrap usually does).

src/helpers.rs

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -105,24 +105,41 @@ fn try_resolve_did(tcx: TyCtxt<'_>, path: &[&str], namespace: Option<Namespace>)
105105
(path, None)
106106
};
107107

108-
// First find the crate.
109-
let krate =
110-
tcx.crates(()).iter().find(|&&krate| tcx.crate_name(krate).as_str() == crate_name)?;
111-
let mut cur_item = DefId { krate: *krate, index: CRATE_DEF_INDEX };
112-
// Then go over the modules.
113-
for &segment in modules {
114-
cur_item = find_children(tcx, cur_item, segment)
115-
.find(|item| tcx.def_kind(item) == DefKind::Mod)?;
116-
}
117-
// Finally, look up the desired item in this module, if any.
118-
match item {
119-
Some((item_name, namespace)) =>
120-
Some(
121-
find_children(tcx, cur_item, item_name)
122-
.find(|item| tcx.def_kind(item).ns() == Some(namespace))?,
123-
),
124-
None => Some(cur_item),
108+
// There may be more than one crate with this name. We try them all.
109+
// (This is particularly relevant when running `std` tests as then there are two `std` crates:
110+
// the one in the sysroot and the one locally built by `cargo test`.)
111+
// FIXME: can we prefer the one from the sysroot?
112+
'crates: for krate in
113+
tcx.crates(()).iter().filter(|&&krate| tcx.crate_name(krate).as_str() == crate_name)
114+
{
115+
let mut cur_item = DefId { krate: *krate, index: CRATE_DEF_INDEX };
116+
// Go over the modules.
117+
for &segment in modules {
118+
let Some(next_item) = find_children(tcx, cur_item, segment)
119+
.find(|item| tcx.def_kind(item) == DefKind::Mod)
120+
else {
121+
continue 'crates;
122+
};
123+
cur_item = next_item;
124+
}
125+
// Finally, look up the desired item in this module, if any.
126+
match item {
127+
Some((item_name, namespace)) => {
128+
let Some(item) = find_children(tcx, cur_item, item_name)
129+
.find(|item| tcx.def_kind(item).ns() == Some(namespace))
130+
else {
131+
continue 'crates;
132+
};
133+
return Some(item);
134+
}
135+
None => {
136+
// Just return the module.
137+
return Some(cur_item);
138+
}
139+
}
125140
}
141+
// Item not found in any of the crates with the right name.
142+
None
126143
}
127144

128145
/// Convert a softfloat type to its corresponding hostfloat type.
@@ -968,10 +985,6 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
968985

969986
fn frame_in_std(&self) -> bool {
970987
let this = self.eval_context_ref();
971-
let Some(start_fn) = this.tcx.lang_items().start_fn() else {
972-
// no_std situations
973-
return false;
974-
};
975988
let frame = this.frame();
976989
// Make an attempt to get at the instance of the function this is inlined from.
977990
let instance: Option<_> = try {
@@ -982,13 +995,15 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
982995
};
983996
// Fall back to the instance of the function itself.
984997
let instance = instance.unwrap_or(frame.instance);
985-
// Now check if this is in the same crate as start_fn.
986-
// As a special exception we also allow unit tests from
987-
// <https://github.com/rust-lang/miri-test-libstd/tree/master/std_miri_test> to call these
988-
// shims.
998+
// Now check the crate it is in. We could try to be clever here and e.g. check if this is
999+
// the same crate as `start_fn`, but that would not work for running std tests in Miri, so
1000+
// we'd need some more hacks anyway. So we just check the name of the crate. If someone
1001+
// calls their crate `std` then we'll just let them keep the pieces.
9891002
let frame_crate = this.tcx.def_path(instance.def_id()).krate;
990-
frame_crate == this.tcx.def_path(start_fn).krate
991-
|| this.tcx.crate_name(frame_crate).as_str() == "std_miri_test"
1003+
let crate_name = this.tcx.crate_name(frame_crate);
1004+
let crate_name = crate_name.as_str();
1005+
// On miri-test-libstd, the name of the crate is different.
1006+
crate_name == "std" || crate_name == "std_miri_test"
9921007
}
9931008

9941009
/// Handler that should be called when unsupported functionality is encountered.

0 commit comments

Comments
 (0)