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");
268 output.lines().filter(|l| !l.is_empty()).map(|l| path.join(l)).collect()
272 // phase_cargo_miri set `MIRI_BE_RUSTC` for when build scripts directly invoke the driver;
273 // however, if we get called back by cargo here, we'll carefully compute the right flags
274 // ourselves, so we first un-do what the earlier phase did.
275 env::remove_var("MIRI_BE_RUSTC");
277 let verbose = std::env::var("MIRI_VERBOSE")
278 .map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
279 let target_crate = is_target_crate();
280 // Determine whether this is cargo invoking rustc to get some infos.
281 let info_query = get_arg_flag_value("--print").is_some() || has_arg_flag("-vV");
283 let store_json = |info: CrateRunInfo| {
284 if get_arg_flag_value("--emit").unwrap_or_default().split(',').any(|e| e == "dep-info") {
285 // Create a stub .d file to stop Cargo from "rebuilding" the crate:
286 // https://github.com/rust-lang/miri/issues/1724#issuecomment-787115693
287 // As we store a JSON file instead of building the crate here, an empty file is fine.
288 let dep_info_name = format!(
290 get_arg_flag_value("--out-dir").unwrap(),
291 get_arg_flag_value("--crate-name").unwrap(),
292 get_arg_flag_value("extra-filename").unwrap_or_default(),
295 eprintln!("[cargo-miri rustc] writing stub dep-info to `{dep_info_name}`");
297 File::create(dep_info_name).expect("failed to create fake .d file");
300 for filename in out_filenames() {
302 eprintln!("[cargo-miri rustc] writing run info to `{}`", filename.display());
304 info.store(&filename);
308 let runnable_crate = !info_query && is_runnable_crate();
310 if runnable_crate && target_crate {
312 phase != RustcPhase::Setup,
313 "there should be no interpretation during sysroot build"
315 let inside_rustdoc = phase == RustcPhase::Rustdoc;
316 // This is the binary or test crate that we want to interpret under Miri.
317 // But we cannot run it here, as cargo invoked us as a compiler -- our stdin and stdout are not
318 // like we want them.
319 // Instead of compiling, we write JSON into the output file with all the relevant command-line flags
320 // and environment variables; this is used when cargo calls us again in the CARGO_TARGET_RUNNER phase.
321 let env = CrateRunEnv::collect(args, inside_rustdoc);
323 store_json(CrateRunInfo::RunWith(env.clone()));
325 // Rustdoc expects us to exit with an error code if the test is marked as `compile_fail`,
326 // just creating the JSON file is not enough: we need to detect syntax errors,
327 // so we need to run Miri with `MIRI_BE_RUSTC` for a check-only build.
329 let mut cmd = miri();
331 // Ensure --emit argument for a check-only build is present.
333 ArgFlagValueIter::from_str_iter(env.args.iter().map(|s| s as &str), "--emit").next()
335 // For `no_run` tests, rustdoc passes a `--emit` flag; make sure it has the right shape.
336 assert_eq!(val, "metadata");
338 // For all other kinds of tests, we can just add our flag.
339 cmd.arg("--emit=metadata");
342 // Alter the `-o` parameter so that it does not overwrite the JSON file we stored above.
343 let mut args = env.args;
344 let mut out_filename = None;
345 for i in 0..args.len() {
347 out_filename = Some(args[i + 1].clone());
348 args[i + 1].push_str(".miri");
351 let out_filename = out_filename.expect("rustdoc must pass `-o`");
354 cmd.env("MIRI_BE_RUSTC", "target");
358 "[cargo-miri rustc inside rustdoc] captured input:\n{}",
359 std::str::from_utf8(&env.stdin).unwrap()
361 eprintln!("[cargo-miri rustc inside rustdoc] going to run:\n{cmd:?}");
364 exec_with_pipe(cmd, &env.stdin, format!("{out_filename}.stdin"));
370 if runnable_crate && get_arg_flag_values("--extern").any(|krate| krate == "proc_macro") {
371 // This is a "runnable" `proc-macro` crate (unit tests). We do not support
372 // interpreting that under Miri now, so we write a JSON file to (display a
373 // helpful message and) skip it in the runner phase.
374 store_json(CrateRunInfo::SkipProcMacroTest);
378 let mut cmd = miri();
379 let mut emit_link_hack = false;
380 // Arguments are treated very differently depending on whether this crate is
381 // for interpretation by Miri, or for use by a build script / proc macro.
382 if !info_query && target_crate {
383 // Forward arguments, but remove "link" from "--emit" to make this a check-only build.
384 let emit_flag = "--emit";
385 while let Some(arg) = args.next() {
386 if let Some(val) = arg.strip_prefix(emit_flag) {
387 // Patch this argument. First, extract its value.
389 val.strip_prefix('=').expect("`cargo` should pass `--emit=X` as one argument");
390 let mut val: Vec<_> = val.split(',').collect();
391 // Now make sure "link" is not in there, but "metadata" is.
392 if let Some(i) = val.iter().position(|&s| s == "link") {
393 emit_link_hack = true;
395 if !val.iter().any(|&s| s == "metadata") {
396 val.push("metadata");
399 cmd.arg(format!("{emit_flag}={}", val.join(",")));
400 } else if arg == "--extern" {
401 // Patch `--extern` filenames, since Cargo sometimes passes stub `.rlib` files:
402 // https://github.com/rust-lang/miri/issues/1705
403 forward_patched_extern_arg(&mut args, &mut cmd);
409 // During setup, patch the panic runtime for `libpanic_abort` (mirroring what bootstrap usually does).
410 if phase == RustcPhase::Setup
411 && get_arg_flag_value("--crate-name").as_deref() == Some("panic_abort")
413 cmd.arg("-C").arg("panic=abort");
416 // For host crates (but not when we are just printing some info),
417 // we might still have to set the sysroot.
419 // When we're running `cargo-miri` from `x.py` we need to pass the sysroot explicitly
420 // due to bootstrap complications.
421 if let Some(sysroot) = std::env::var_os("MIRI_HOST_SYSROOT") {
422 cmd.arg("--sysroot").arg(sysroot);
426 // For host crates or when we are printing, just forward everything.
430 // We want to compile, not interpret. We still use Miri to make sure the compiler version etc
431 // are the exact same as what is used for interpretation.
432 // MIRI_DEFAULT_ARGS should not be used to build host crates, hence setting "target" or "host"
433 // as the value here to help Miri differentiate them.
434 cmd.env("MIRI_BE_RUSTC", if target_crate { "target" } else { "host" });
439 "[cargo-miri rustc] target_crate={target_crate} runnable_crate={runnable_crate} info_query={info_query}"
443 // Create a stub .rlib file if "link" was requested by cargo.
444 // This is necessary to prevent cargo from doing rebuilds all the time.
446 for filename in out_filenames() {
448 eprintln!("[cargo-miri rustc] creating fake lib file at `{}`", filename.display());
450 File::create(filename).expect("failed to create fake lib file");
454 debug_cmd("[cargo-miri rustc]", verbose, &cmd);
458 #[derive(Debug, Copy, Clone, PartialEq)]
459 pub enum RunnerPhase {
460 /// `cargo` is running a binary
462 /// `rustdoc` is running a binary
466 pub fn phase_runner(mut binary_args: impl Iterator<Item = String>, phase: RunnerPhase) {
467 // phase_cargo_miri set `MIRI_BE_RUSTC` for when build scripts directly invoke the driver;
468 // however, if we get called back by cargo here, we'll carefully compute the right flags
469 // ourselves, so we first un-do what the earlier phase did.
470 env::remove_var("MIRI_BE_RUSTC");
472 let verbose = std::env::var("MIRI_VERBOSE")
473 .map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
475 let binary = binary_args.next().unwrap();
476 let file = File::open(&binary)
477 .unwrap_or_else(|_| show_error!(
478 "file {:?} not found or `cargo-miri` invoked incorrectly; please only invoke this binary through `cargo miri`", binary
480 let file = BufReader::new(file);
482 let info = serde_json::from_reader(file).unwrap_or_else(|_| {
483 show_error!("file {:?} contains outdated or invalid JSON; try `cargo clean`", binary)
485 let info = match info {
486 CrateRunInfo::RunWith(info) => info,
487 CrateRunInfo::SkipProcMacroTest => {
489 "Running unit tests of `proc-macro` crates is not currently supported by Miri."
495 let mut cmd = miri();
497 // Set missing env vars. We prefer build-time env vars over run-time ones; see
498 // <https://github.com/rust-lang/miri/issues/1661> for the kind of issue that fixes.
499 for (name, val) in info.env {
500 if let Some(old_val) = env::var_os(&name) {
502 // This one did not actually change, no need to re-set it.
503 // (This keeps the `debug_cmd` below more manageable.)
505 } else if verbose > 0 {
507 "[cargo-miri runner] Overwriting run-time env var {name:?}={old_val:?} with build-time value {val:?}"
514 // Forward rustc arguments.
515 // We need to patch "--extern" filenames because we forced a check-only
516 // build without cargo knowing about that: replace `.rlib` suffix by
518 // We also need to remove `--error-format` as cargo specifies that to be JSON,
519 // but when we run here, cargo does not interpret the JSON any more. `--json`
520 // then also nees to be dropped.
521 let mut args = info.args.into_iter();
522 let error_format_flag = "--error-format";
523 let json_flag = "--json";
524 while let Some(arg) = args.next() {
525 if arg == "--extern" {
526 forward_patched_extern_arg(&mut args, &mut cmd);
527 } else if let Some(suffix) = arg.strip_prefix(error_format_flag) {
528 assert!(suffix.starts_with('='));
529 // Drop this argument.
530 } else if let Some(suffix) = arg.strip_prefix(json_flag) {
531 assert!(suffix.starts_with('='));
532 // Drop this argument.
537 // Respect `MIRIFLAGS`.
538 if let Ok(a) = env::var("MIRIFLAGS") {
539 // This code is taken from `RUSTFLAGS` handling in cargo.
540 let args = a.split(' ').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string);
544 // Then pass binary arguments.
546 cmd.args(binary_args);
548 // Make sure we use the build-time working directory for interpreting Miri/rustc arguments.
549 // But then we need to switch to the run-time one, which we instruct Miri to do by setting `MIRI_CWD`.
550 cmd.current_dir(info.current_dir);
551 cmd.env("MIRI_CWD", env::current_dir().unwrap());
554 debug_cmd("[cargo-miri runner]", verbose, &cmd);
556 RunnerPhase::Rustdoc => exec_with_pipe(cmd, &info.stdin, format!("{binary}.stdin")),
557 RunnerPhase::Cargo => exec(cmd),
561 pub fn phase_rustdoc(mut args: impl Iterator<Item = String>) {
562 let verbose = std::env::var("MIRI_VERBOSE")
563 .map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
565 // phase_cargo_miri sets the RUSTDOC env var to ourselves, so we can't use that here;
566 // just default to a straight-forward invocation for now:
567 let mut cmd = Command::new("rustdoc");
569 let extern_flag = "--extern";
570 let runtool_flag = "--runtool";
571 while let Some(arg) = args.next() {
572 if arg == extern_flag {
573 // Patch --extern arguments to use *.rmeta files, since phase_cargo_rustc only creates stub *.rlib files.
574 forward_patched_extern_arg(&mut args, &mut cmd);
575 } else if arg == runtool_flag {
576 // An existing --runtool flag indicates cargo is running in cross-target mode, which we don't support.
577 // Note that this is only passed when cargo is run with the unstable -Zdoctest-xcompile flag;
578 // otherwise, we won't be called as rustdoc at all.
579 show_error!("cross-interpreting doctests is not currently supported by Miri.");
585 // Doctests of `proc-macro` crates (and their dependencies) are always built for the host,
586 // so we are not able to run them in Miri.
587 if get_arg_flag_values("--crate-type").any(|crate_type| crate_type == "proc-macro") {
588 eprintln!("Running doctests of `proc-macro` crates is not currently supported by Miri.");
592 // For each doctest, rustdoc starts two child processes: first the test is compiled,
593 // then the produced executable is invoked. We want to reroute both of these to cargo-miri,
594 // such that the first time we'll enter phase_cargo_rustc, and phase_cargo_runner second.
596 // rustdoc invokes the test-builder by forwarding most of its own arguments, which makes
597 // it difficult to determine when phase_cargo_rustc should run instead of phase_cargo_rustdoc.
598 // Furthermore, the test code is passed via stdin, rather than a temporary file, so we need
599 // to let phase_cargo_rustc know to expect that. We'll use this environment variable as a flag:
600 cmd.env("MIRI_CALLED_FROM_RUSTDOC", "1");
602 // The `--test-builder` and `--runtool` arguments are unstable rustdoc features,
603 // which are disabled by default. We first need to enable them explicitly:
604 cmd.arg("-Z").arg("unstable-options");
606 // rustdoc needs to know the right sysroot.
607 cmd.arg("--sysroot").arg(env::var_os("MIRI_SYSROOT").unwrap());
608 // make sure the 'miri' flag is set for rustdoc
609 cmd.arg("--cfg").arg("miri");
611 // Make rustdoc call us back.
612 let cargo_miri_path = std::env::current_exe().expect("current executable path invalid");
613 cmd.arg("--test-builder").arg(&cargo_miri_path); // invoked by forwarding most arguments
614 cmd.arg("--runtool").arg(&cargo_miri_path); // invoked with just a single path argument
616 debug_cmd("[cargo-miri rustdoc]", verbose, &cmd);