1 //! Implements the various phases of `cargo miri run/test`.
4 use std::fs::{self, File};
5 use std::io::BufReader;
6 use std::path::PathBuf;
7 use std::process::Command;
9 use rustc_version::VersionMeta;
11 use crate::{setup::*, util::*};
13 const CARGO_MIRI_HELP: &str = r#"Runs binary crates and tests in Miri
16 cargo miri [subcommand] [<cargo options>...] [--] [<program/test suite options>...]
21 nextest Run tests with nextest (requires cargo-nextest installed)
22 setup Only perform automatic setup, but without asking questions (for getting a proper libstd)
24 The cargo options are exactly the same as for `cargo run` and `cargo test`, respectively.
28 cargo miri test -- test-suite-filter
30 cargo miri setup --print sysroot
31 This will print the path to the generated sysroot (and nothing else) on stdout.
32 stderr will still contain progress information about how the build is doing.
37 println!("{CARGO_MIRI_HELP}");
41 print!("miri {}", env!("CARGO_PKG_VERSION"));
42 let version = format!("{} {}", env!("GIT_HASH"), env!("COMMIT_DATE"));
43 if version.len() > 1 {
44 // If there is actually something here, print it.
45 print!(" ({version})");
50 fn forward_patched_extern_arg(args: &mut impl Iterator<Item = String>, cmd: &mut Command) {
51 cmd.arg("--extern"); // always forward flag, but adjust filename:
52 let path = args.next().expect("`--extern` should be followed by a filename");
53 if let Some(lib) = path.strip_suffix(".rlib") {
54 // If this is an rlib, make it an rmeta.
55 cmd.arg(format!("{lib}.rmeta"));
57 // Some other extern file (e.g. a `.so`). Forward unchanged.
62 pub fn phase_cargo_miri(mut args: impl Iterator<Item = String>) {
63 // Check for version and help flags even when invoked as `cargo-miri`.
64 if has_arg_flag("--help") || has_arg_flag("-h") {
68 if has_arg_flag("--version") || has_arg_flag("-V") {
73 // Require a subcommand before any flags.
74 // We cannot know which of those flags take arguments and which do not,
75 // so we cannot detect subcommands later.
76 let Some(subcommand) = args.next() else {
77 show_error!("`cargo miri` needs to be called with a subcommand (`run`, `test`)");
79 let subcommand = match &*subcommand {
80 "setup" => MiriCommand::Setup,
81 "test" | "t" | "run" | "r" | "nextest" => MiriCommand::Forward(subcommand),
84 "`cargo miri` supports the following subcommands: `run`, `test`, `nextest`, and `setup`."
87 let verbose = num_arg_flag("-v");
89 // Determine the involved architectures.
90 let rustc_version = VersionMeta::for_command(miri_for_host())
91 .expect("failed to determine underlying rustc version of Miri");
92 let host = &rustc_version.host;
93 let target = get_arg_flag_value("--target");
94 let target = target.as_ref().unwrap_or(host);
97 setup(&subcommand, target, &rustc_version, verbose);
99 // Invoke actual cargo for the job, but with different flags.
100 // We re-use `cargo test` and `cargo run`, which makes target and binary handling very easy but
101 // requires some extra work to make the build check-only (see all the `--emit` hacks below).
102 // <https://github.com/rust-lang/miri/pull/1540#issuecomment-693553191> describes an alternative
103 // approach that uses `cargo check`, making that part easier but target and binary handling
105 let cargo_miri_path = std::env::current_exe()
106 .expect("current executable path invalid")
109 .expect("current executable path is not valid UTF-8");
110 let cargo_cmd = match subcommand {
111 MiriCommand::Forward(s) => s,
112 MiriCommand::Setup => return, // `cargo miri setup` stops here.
114 let metadata = get_cargo_metadata();
115 let mut cmd = cargo();
118 // Forward all arguments before `--` other than `--target-dir` and its value to Cargo.
119 // (We want to *change* the target-dir value, so we must not forward it.)
120 let mut target_dir = None;
121 for arg in ArgSplitFlagValue::from_string_iter(&mut args, "--target-dir") {
124 if target_dir.is_some() {
125 show_error!("`--target-dir` is provided more than once");
127 target_dir = Some(value.into());
134 // Detect the target directory if it's not specified via `--target-dir`.
135 // (`cargo metadata` does not support `--target-dir`, that's why we have to handle this ourselves.)
136 let target_dir = target_dir.get_or_insert_with(|| metadata.target_directory.clone());
137 // Set `--target-dir` to `miri` inside the original target directory.
138 target_dir.push("miri");
139 cmd.arg("--target-dir").arg(target_dir);
141 // Make sure the build target is explicitly set.
142 // This is needed to make the `target.runner` settings do something,
143 // and it later helps us detect which crates are proc-macro/build-script
144 // (host crates) and which crates are needed for the program itself.
145 if get_arg_flag_value("--target").is_none() {
146 // No target given. Explicitly pick the host.
151 // Set ourselves as runner for al binaries invoked by cargo.
152 // We use `all()` since `true` is not a thing in cfg-lang, but the empty conjunction is. :)
153 let cargo_miri_path_for_toml = escape_for_toml(&cargo_miri_path);
155 .arg(format!("target.'cfg(all())'.runner=[{cargo_miri_path_for_toml}, 'runner']"));
157 // Forward all further arguments after `--` to cargo.
158 cmd.arg("--").args(args);
160 // Set `RUSTC_WRAPPER` to ourselves. Cargo will prepend that binary to its usual invocation,
161 // i.e., the first argument is `rustc` -- which is what we use in `main` to distinguish
162 // the two codepaths. (That extra argument is why we prefer this over setting `RUSTC`.)
163 if env::var_os("RUSTC_WRAPPER").is_some() {
165 "WARNING: Ignoring `RUSTC_WRAPPER` environment variable, Miri does not support wrapping."
168 cmd.env("RUSTC_WRAPPER", &cargo_miri_path);
169 // We are going to invoke `MIRI` for everything, not `RUSTC`.
170 if env::var_os("RUSTC").is_some() && env::var_os("MIRI").is_none() {
172 "WARNING: Ignoring `RUSTC` environment variable; set `MIRI` if you want to control the binary used as the driver."
175 // Build scripts (and also cargo: https://github.com/rust-lang/cargo/issues/10885) will invoke
176 // `rustc` even when `RUSTC_WRAPPER` is set. To make sure everything is coherent, we want that
177 // to be the Miri driver, but acting as rustc, on the target level. (Target, rather than host,
178 // is needed for cross-interpretation situations.) This is not a perfect emulation of real rustc
179 // (it might be unable to produce binaries since the sysroot is check-only), but it's as close
180 // as we can get, and it's good enough for autocfg.
182 // In `main`, we need the value of `RUSTC` to distinguish RUSTC_WRAPPER invocations from rustdoc
183 // or TARGET_RUNNER invocations, so we canonicalize it here to make it exceedingly unlikely that
184 // there would be a collision with other invocations of cargo-miri (as rustdoc or as runner). We
185 // explicitly do this even if RUSTC_STAGE is set, since for these builds we do *not* want the
186 // bootstrap `rustc` thing in our way! Instead, we have MIRI_HOST_SYSROOT to use for host
188 cmd.env("RUSTC", &fs::canonicalize(find_miri()).unwrap());
189 cmd.env("MIRI_BE_RUSTC", "target"); // we better remember to *unset* this in the other phases!
191 // Set rustdoc to us as well, so we can run doctests.
192 cmd.env("RUSTDOC", &cargo_miri_path);
194 cmd.env("MIRI_LOCAL_CRATES", local_crates(&metadata));
196 cmd.env("MIRI_VERBOSE", verbose.to_string()); // This makes the other phases verbose.
200 debug_cmd("[cargo-miri miri]", verbose, &cmd);
204 #[derive(Debug, Copy, Clone, PartialEq)]
205 pub enum RustcPhase {
206 /// `rustc` called during sysroot build.
208 /// `rustc` called by `cargo` for regular build.
210 /// `rustc` called by `rustdoc` for doctest.
214 pub fn phase_rustc(mut args: impl Iterator<Item = String>, phase: RustcPhase) {
215 /// Determines if we are being invoked (as rustc) to build a crate for
216 /// the "target" architecture, in contrast to the "host" architecture.
217 /// Host crates are for build scripts and proc macros and still need to
218 /// be built like normal; target crates need to be built for or interpreted
221 /// Currently, we detect this by checking for "--target=", which is
222 /// never set for host crates. This matches what rustc bootstrap does,
223 /// which hopefully makes it "reliable enough". This relies on us always
224 /// invoking cargo itself with `--target`, which `in_cargo_miri` ensures.
225 fn is_target_crate() -> bool {
226 get_arg_flag_value("--target").is_some()
229 /// Returns whether or not Cargo invoked the wrapper (this binary) to compile
230 /// the final, binary crate (either a test for 'cargo test', or a binary for 'cargo run')
231 /// Cargo does not give us this information directly, so we need to check
232 /// various command-line flags.
233 fn is_runnable_crate() -> bool {
234 let is_bin = get_arg_flag_value("--crate-type").as_deref().unwrap_or("bin") == "bin";
235 let is_test = has_arg_flag("--test");
239 fn out_filenames() -> Vec<PathBuf> {
240 if let Some(out_file) = get_arg_flag_value("-o") {
241 // `-o` has precedence over `--out-dir`.
242 vec![PathBuf::from(out_file)]
244 let out_dir = get_arg_flag_value("--out-dir").unwrap_or_default();
245 let path = PathBuf::from(out_dir);
246 // Ask rustc for the filename (since that is target-dependent).
247 let mut rustc = miri_for_host(); // sysroot doesn't matter for this so we just use the host
248 rustc.arg("--print").arg("file-names");
249 for flag in ["--crate-name", "--crate-type", "--target"] {
250 for val in get_arg_flag_values(flag) {
251 rustc.arg(flag).arg(val);
254 // This is technically passed as `-C extra-filename=...`, but the prefix seems unique
255 // enough... (and cargo passes this before the filename so it should be unique)
256 if let Some(extra) = get_arg_flag_value("extra-filename") {
257 rustc.arg("-C").arg(format!("extra-filename={extra}"));
261 let output = rustc.output().expect("cannot run rustc to determine file name");
263 output.status.success(),
264 "rustc failed when determining file name:\n{output:?}"
267 String::from_utf8(output.stdout).expect("rustc returned non-UTF-8 filename");
270 .filter(|l| !l.is_empty())
272 let mut p = path.clone();
280 // phase_cargo_miri set `MIRI_BE_RUSTC` for when build scripts directly invoke the driver;
281 // however, if we get called back by cargo here, we'll carefully compute the right flags
282 // ourselves, so we first un-do what the earlier phase did.
283 env::remove_var("MIRI_BE_RUSTC");
285 let verbose = std::env::var("MIRI_VERBOSE")
286 .map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
287 let target_crate = is_target_crate();
288 // Determine whether this is cargo invoking rustc to get some infos.
289 let info_query = get_arg_flag_value("--print").is_some() || has_arg_flag("-vV");
291 let store_json = |info: CrateRunInfo| {
292 if get_arg_flag_value("--emit").unwrap_or_default().split(',').any(|e| e == "dep-info") {
293 // Create a stub .d file to stop Cargo from "rebuilding" the crate:
294 // https://github.com/rust-lang/miri/issues/1724#issuecomment-787115693
295 // As we store a JSON file instead of building the crate here, an empty file is fine.
296 let dep_info_name = format!(
298 get_arg_flag_value("--out-dir").unwrap(),
299 get_arg_flag_value("--crate-name").unwrap(),
300 get_arg_flag_value("extra-filename").unwrap_or_default(),
303 eprintln!("[cargo-miri rustc] writing stub dep-info to `{dep_info_name}`");
305 File::create(dep_info_name).expect("failed to create fake .d file");
308 for filename in out_filenames() {
310 eprintln!("[cargo-miri rustc] writing run info to `{}`", filename.display());
312 info.store(&filename);
316 let runnable_crate = !info_query && is_runnable_crate();
318 if runnable_crate && target_crate {
320 phase != RustcPhase::Setup,
321 "there should be no interpretation during sysroot build"
323 let inside_rustdoc = phase == RustcPhase::Rustdoc;
324 // This is the binary or test crate that we want to interpret under Miri.
325 // But we cannot run it here, as cargo invoked us as a compiler -- our stdin and stdout are not
326 // like we want them.
327 // Instead of compiling, we write JSON into the output file with all the relevant command-line flags
328 // and environment variables; this is used when cargo calls us again in the CARGO_TARGET_RUNNER phase.
329 let env = CrateRunEnv::collect(args, inside_rustdoc);
331 store_json(CrateRunInfo::RunWith(env.clone()));
333 // Rustdoc expects us to exit with an error code if the test is marked as `compile_fail`,
334 // just creating the JSON file is not enough: we need to detect syntax errors,
335 // so we need to run Miri with `MIRI_BE_RUSTC` for a check-only build.
337 let mut cmd = miri();
339 // Ensure --emit argument for a check-only build is present.
341 ArgFlagValueIter::from_str_iter(env.args.iter().map(|s| s as &str), "--emit").next()
343 // For `no_run` tests, rustdoc passes a `--emit` flag; make sure it has the right shape.
344 assert_eq!(val, "metadata");
346 // For all other kinds of tests, we can just add our flag.
347 cmd.arg("--emit=metadata");
350 // Alter the `-o` parameter so that it does not overwrite the JSON file we stored above.
351 let mut args = env.args;
352 let mut out_filename = None;
353 for i in 0..args.len() {
355 out_filename = Some(args[i + 1].clone());
356 args[i + 1].push_str(".miri");
359 let out_filename = out_filename.expect("rustdoc must pass `-o`");
362 cmd.env("MIRI_BE_RUSTC", "target");
366 "[cargo-miri rustc inside rustdoc] captured input:\n{}",
367 std::str::from_utf8(&env.stdin).unwrap()
369 eprintln!("[cargo-miri rustc inside rustdoc] going to run:\n{cmd:?}");
372 exec_with_pipe(cmd, &env.stdin, format!("{out_filename}.stdin"));
378 if runnable_crate && get_arg_flag_values("--extern").any(|krate| krate == "proc_macro") {
379 // This is a "runnable" `proc-macro` crate (unit tests). We do not support
380 // interpreting that under Miri now, so we write a JSON file to (display a
381 // helpful message and) skip it in the runner phase.
382 store_json(CrateRunInfo::SkipProcMacroTest);
386 let mut cmd = miri();
387 let mut emit_link_hack = false;
388 // Arguments are treated very differently depending on whether this crate is
389 // for interpretation by Miri, or for use by a build script / proc macro.
390 if !info_query && target_crate {
391 // Forward arguments, but remove "link" from "--emit" to make this a check-only build.
392 let emit_flag = "--emit";
393 while let Some(arg) = args.next() {
394 if let Some(val) = arg.strip_prefix(emit_flag) {
395 // Patch this argument. First, extract its value.
397 val.strip_prefix('=').expect("`cargo` should pass `--emit=X` as one argument");
398 let mut val: Vec<_> = val.split(',').collect();
399 // Now make sure "link" is not in there, but "metadata" is.
400 if let Some(i) = val.iter().position(|&s| s == "link") {
401 emit_link_hack = true;
403 if !val.iter().any(|&s| s == "metadata") {
404 val.push("metadata");
407 cmd.arg(format!("{emit_flag}={}", val.join(",")));
408 } else if arg == "--extern" {
409 // Patch `--extern` filenames, since Cargo sometimes passes stub `.rlib` files:
410 // https://github.com/rust-lang/miri/issues/1705
411 forward_patched_extern_arg(&mut args, &mut cmd);
417 // During setup, patch the panic runtime for `libpanic_abort` (mirroring what bootstrap usually does).
418 if phase == RustcPhase::Setup
419 && get_arg_flag_value("--crate-name").as_deref() == Some("panic_abort")
421 cmd.arg("-C").arg("panic=abort");
424 // For host crates (but not when we are just printing some info),
425 // we might still have to set the sysroot.
427 // When we're running `cargo-miri` from `x.py` we need to pass the sysroot explicitly
428 // due to bootstrap complications.
429 if let Some(sysroot) = std::env::var_os("MIRI_HOST_SYSROOT") {
430 cmd.arg("--sysroot").arg(sysroot);
434 // For host crates or when we are printing, just forward everything.
438 // We want to compile, not interpret. We still use Miri to make sure the compiler version etc
439 // are the exact same as what is used for interpretation.
440 // MIRI_DEFAULT_ARGS should not be used to build host crates, hence setting "target" or "host"
441 // as the value here to help Miri differentiate them.
442 cmd.env("MIRI_BE_RUSTC", if target_crate { "target" } else { "host" });
447 "[cargo-miri rustc] target_crate={target_crate} runnable_crate={runnable_crate} info_query={info_query}"
451 // Create a stub .rlib file if "link" was requested by cargo.
452 // This is necessary to prevent cargo from doing rebuilds all the time.
454 for filename in out_filenames() {
456 eprintln!("[cargo-miri rustc] creating fake lib file at `{}`", filename.display());
458 File::create(filename).expect("failed to create fake lib file");
462 debug_cmd("[cargo-miri rustc]", verbose, &cmd);
466 #[derive(Debug, Copy, Clone, PartialEq)]
467 pub enum RunnerPhase {
468 /// `cargo` is running a binary
470 /// `rustdoc` is running a binary
474 pub fn phase_runner(mut binary_args: impl Iterator<Item = String>, phase: RunnerPhase) {
475 // phase_cargo_miri set `MIRI_BE_RUSTC` for when build scripts directly invoke the driver;
476 // however, if we get called back by cargo here, we'll carefully compute the right flags
477 // ourselves, so we first un-do what the earlier phase did.
478 env::remove_var("MIRI_BE_RUSTC");
480 let verbose = std::env::var("MIRI_VERBOSE")
481 .map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
483 let binary = binary_args.next().unwrap();
484 let file = File::open(&binary)
485 .unwrap_or_else(|_| show_error!(
486 "file {:?} not found or `cargo-miri` invoked incorrectly; please only invoke this binary through `cargo miri`", binary
488 let file = BufReader::new(file);
490 let info = serde_json::from_reader(file).unwrap_or_else(|_| {
491 show_error!("file {:?} contains outdated or invalid JSON; try `cargo clean`", binary)
493 let info = match info {
494 CrateRunInfo::RunWith(info) => info,
495 CrateRunInfo::SkipProcMacroTest => {
497 "Running unit tests of `proc-macro` crates is not currently supported by Miri."
503 let mut cmd = miri();
505 // Set missing env vars. We prefer build-time env vars over run-time ones; see
506 // <https://github.com/rust-lang/miri/issues/1661> for the kind of issue that fixes.
507 for (name, val) in info.env {
508 if let Some(old_val) = env::var_os(&name) {
510 // This one did not actually change, no need to re-set it.
511 // (This keeps the `debug_cmd` below more manageable.)
513 } else if verbose > 0 {
515 "[cargo-miri runner] Overwriting run-time env var {name:?}={old_val:?} with build-time value {val:?}"
522 // Forward rustc arguments.
523 // We need to patch "--extern" filenames because we forced a check-only
524 // build without cargo knowing about that: replace `.rlib` suffix by
526 // We also need to remove `--error-format` as cargo specifies that to be JSON,
527 // but when we run here, cargo does not interpret the JSON any more. `--json`
528 // then also nees to be dropped.
529 let mut args = info.args.into_iter();
530 let error_format_flag = "--error-format";
531 let json_flag = "--json";
532 while let Some(arg) = args.next() {
533 if arg == "--extern" {
534 forward_patched_extern_arg(&mut args, &mut cmd);
535 } else if let Some(suffix) = arg.strip_prefix(error_format_flag) {
536 assert!(suffix.starts_with('='));
537 // Drop this argument.
538 } else if let Some(suffix) = arg.strip_prefix(json_flag) {
539 assert!(suffix.starts_with('='));
540 // Drop this argument.
545 // Respect `MIRIFLAGS`.
546 if let Ok(a) = env::var("MIRIFLAGS") {
547 // This code is taken from `RUSTFLAGS` handling in cargo.
548 let args = a.split(' ').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string);
552 // Then pass binary arguments.
554 cmd.args(binary_args);
556 // Make sure we use the build-time working directory for interpreting Miri/rustc arguments.
557 // But then we need to switch to the run-time one, which we instruct Miri to do by setting `MIRI_CWD`.
558 cmd.current_dir(info.current_dir);
559 cmd.env("MIRI_CWD", env::current_dir().unwrap());
562 debug_cmd("[cargo-miri runner]", verbose, &cmd);
564 RunnerPhase::Rustdoc => exec_with_pipe(cmd, &info.stdin, format!("{binary}.stdin")),
565 RunnerPhase::Cargo => exec(cmd),
569 pub fn phase_rustdoc(mut args: impl Iterator<Item = String>) {
570 let verbose = std::env::var("MIRI_VERBOSE")
571 .map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
573 // phase_cargo_miri sets the RUSTDOC env var to ourselves, so we can't use that here;
574 // just default to a straight-forward invocation for now:
575 let mut cmd = Command::new("rustdoc");
577 let extern_flag = "--extern";
578 let runtool_flag = "--runtool";
579 while let Some(arg) = args.next() {
580 if arg == extern_flag {
581 // Patch --extern arguments to use *.rmeta files, since phase_cargo_rustc only creates stub *.rlib files.
582 forward_patched_extern_arg(&mut args, &mut cmd);
583 } else if arg == runtool_flag {
584 // An existing --runtool flag indicates cargo is running in cross-target mode, which we don't support.
585 // Note that this is only passed when cargo is run with the unstable -Zdoctest-xcompile flag;
586 // otherwise, we won't be called as rustdoc at all.
587 show_error!("cross-interpreting doctests is not currently supported by Miri.");
593 // Doctests of `proc-macro` crates (and their dependencies) are always built for the host,
594 // so we are not able to run them in Miri.
595 if get_arg_flag_values("--crate-type").any(|crate_type| crate_type == "proc-macro") {
596 eprintln!("Running doctests of `proc-macro` crates is not currently supported by Miri.");
600 // For each doctest, rustdoc starts two child processes: first the test is compiled,
601 // then the produced executable is invoked. We want to reroute both of these to cargo-miri,
602 // such that the first time we'll enter phase_cargo_rustc, and phase_cargo_runner second.
604 // rustdoc invokes the test-builder by forwarding most of its own arguments, which makes
605 // it difficult to determine when phase_cargo_rustc should run instead of phase_cargo_rustdoc.
606 // Furthermore, the test code is passed via stdin, rather than a temporary file, so we need
607 // to let phase_cargo_rustc know to expect that. We'll use this environment variable as a flag:
608 cmd.env("MIRI_CALLED_FROM_RUSTDOC", "1");
610 // The `--test-builder` and `--runtool` arguments are unstable rustdoc features,
611 // which are disabled by default. We first need to enable them explicitly:
612 cmd.arg("-Z").arg("unstable-options");
614 // rustdoc needs to know the right sysroot.
615 cmd.arg("--sysroot").arg(env::var_os("MIRI_SYSROOT").unwrap());
616 // make sure the 'miri' flag is set for rustdoc
617 cmd.arg("--cfg").arg("miri");
619 // Make rustdoc call us back.
620 let cargo_miri_path = std::env::current_exe().expect("current executable path invalid");
621 cmd.arg("--test-builder").arg(&cargo_miri_path); // invoked by forwarding most arguments
622 cmd.arg("--runtool").arg(&cargo_miri_path); // invoked with just a single path argument
624 debug_cmd("[cargo-miri rustdoc]", verbose, &cmd);