]> git.lizzy.rs Git - rust.git/blob - src/tools/miri/cargo-miri/src/phases.rs
e51bfa3798f3ecdd0b6a4ed9abf06bc8a8024366
[rust.git] / src / tools / miri / cargo-miri / src / phases.rs
1 //! Implements the various phases of `cargo miri run/test`.
2
3 use std::env;
4 use std::fs::{self, File};
5 use std::io::BufReader;
6 use std::path::PathBuf;
7 use std::process::Command;
8
9 use rustc_version::VersionMeta;
10
11 use crate::{setup::*, util::*};
12
13 const CARGO_MIRI_HELP: &str = r#"Runs binary crates and tests in Miri
14
15 Usage:
16     cargo miri [subcommand] [<cargo options>...] [--] [<program/test suite options>...]
17
18 Subcommands:
19     run, r                   Run binaries
20     test, t                  Run tests
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)
23
24 The cargo options are exactly the same as for `cargo run` and `cargo test`, respectively.
25
26 Examples:
27     cargo miri run
28     cargo miri test -- test-suite-filter
29
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.
33
34 "#;
35
36 fn show_help() {
37     println!("{CARGO_MIRI_HELP}");
38 }
39
40 fn show_version() {
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})");
46     }
47     println!();
48 }
49
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"));
56     } else {
57         // Some other extern file (e.g. a `.so`). Forward unchanged.
58         cmd.arg(path);
59     }
60 }
61
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") {
65         show_help();
66         return;
67     }
68     if has_arg_flag("--version") || has_arg_flag("-V") {
69         show_version();
70         return;
71     }
72
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`)");
78     };
79     let subcommand = match &*subcommand {
80         "setup" => MiriCommand::Setup,
81         "test" | "t" | "run" | "r" | "nextest" => MiriCommand::Forward(subcommand),
82         _ =>
83             show_error!(
84                 "`cargo miri` supports the following subcommands: `run`, `test`, `nextest`, and `setup`."
85             ),
86     };
87     let verbose = num_arg_flag("-v");
88
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);
95
96     // We always setup.
97     setup(&subcommand, target, &rustc_version, verbose);
98
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
104     // harder.
105     let cargo_miri_path = std::env::current_exe()
106         .expect("current executable path invalid")
107         .into_os_string()
108         .into_string()
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.
113     };
114     let metadata = get_cargo_metadata();
115     let mut cmd = cargo();
116     cmd.arg(cargo_cmd);
117
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") {
122         match arg {
123             Ok(value) => {
124                 if target_dir.is_some() {
125                     show_error!("`--target-dir` is provided more than once");
126                 }
127                 target_dir = Some(value.into());
128             }
129             Err(arg) => {
130                 cmd.arg(arg);
131             }
132         }
133     }
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);
140
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.
147         cmd.arg("--target");
148         cmd.arg(host);
149     }
150
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);
154     cmd.arg("--config")
155         .arg(format!("target.'cfg(all())'.runner=[{cargo_miri_path_for_toml}, 'runner']"));
156
157     // Forward all further arguments after `--` to cargo.
158     cmd.arg("--").args(args);
159
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() {
164         println!(
165             "WARNING: Ignoring `RUSTC_WRAPPER` environment variable, Miri does not support wrapping."
166         );
167     }
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() {
171         println!(
172             "WARNING: Ignoring `RUSTC` environment variable; set `MIRI` if you want to control the binary used as the driver."
173         );
174     }
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.
181     //
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
187     // builds.
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!
190
191     // Set rustdoc to us as well, so we can run doctests.
192     cmd.env("RUSTDOC", &cargo_miri_path);
193
194     cmd.env("MIRI_LOCAL_CRATES", local_crates(&metadata));
195     if verbose > 0 {
196         cmd.env("MIRI_VERBOSE", verbose.to_string()); // This makes the other phases verbose.
197     }
198
199     // Run cargo.
200     debug_cmd("[cargo-miri miri]", verbose, &cmd);
201     exec(cmd)
202 }
203
204 #[derive(Debug, Copy, Clone, PartialEq)]
205 pub enum RustcPhase {
206     /// `rustc` called during sysroot build.
207     Setup,
208     /// `rustc` called by `cargo` for regular build.
209     Build,
210     /// `rustc` called by `rustdoc` for doctest.
211     Rustdoc,
212 }
213
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
219     /// by Miri.
220     ///
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()
227     }
228
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");
236         is_bin || is_test
237     }
238
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)]
243         } else {
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);
252                 }
253             }
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}"));
258             }
259             rustc.arg("-");
260
261             let output = rustc.output().expect("cannot run rustc to determine file name");
262             assert!(
263                 output.status.success(),
264                 "rustc failed when determining file name:\n{output:?}"
265             );
266             let 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()
269         }
270     }
271
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");
276
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");
282
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!(
289                 "{}/{}{}.d",
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(),
293             );
294             if verbose > 0 {
295                 eprintln!("[cargo-miri rustc] writing stub dep-info to `{dep_info_name}`");
296             }
297             File::create(dep_info_name).expect("failed to create fake .d file");
298         }
299
300         for filename in out_filenames() {
301             if verbose > 0 {
302                 eprintln!("[cargo-miri rustc] writing run info to `{}`", filename.display());
303             }
304             info.store(&filename);
305         }
306     };
307
308     let runnable_crate = !info_query && is_runnable_crate();
309
310     if runnable_crate && target_crate {
311         assert!(
312             phase != RustcPhase::Setup,
313             "there should be no interpretation during sysroot build"
314         );
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);
322
323         store_json(CrateRunInfo::RunWith(env.clone()));
324
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.
328         if inside_rustdoc {
329             let mut cmd = miri();
330
331             // Ensure --emit argument for a check-only build is present.
332             if let Some(val) =
333                 ArgFlagValueIter::from_str_iter(env.args.iter().map(|s| s as &str), "--emit").next()
334             {
335                 // For `no_run` tests, rustdoc passes a `--emit` flag; make sure it has the right shape.
336                 assert_eq!(val, "metadata");
337             } else {
338                 // For all other kinds of tests, we can just add our flag.
339                 cmd.arg("--emit=metadata");
340             }
341
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() {
346                 if args[i] == "-o" {
347                     out_filename = Some(args[i + 1].clone());
348                     args[i + 1].push_str(".miri");
349                 }
350             }
351             let out_filename = out_filename.expect("rustdoc must pass `-o`");
352
353             cmd.args(&args);
354             cmd.env("MIRI_BE_RUSTC", "target");
355
356             if verbose > 0 {
357                 eprintln!(
358                     "[cargo-miri rustc inside rustdoc] captured input:\n{}",
359                     std::str::from_utf8(&env.stdin).unwrap()
360                 );
361                 eprintln!("[cargo-miri rustc inside rustdoc] going to run:\n{cmd:?}");
362             }
363
364             exec_with_pipe(cmd, &env.stdin, format!("{out_filename}.stdin"));
365         }
366
367         return;
368     }
369
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);
375         return;
376     }
377
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.
388                 let val =
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;
394                     val.remove(i);
395                     if !val.iter().any(|&s| s == "metadata") {
396                         val.push("metadata");
397                     }
398                 }
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);
404             } else {
405                 cmd.arg(arg);
406             }
407         }
408
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")
412         {
413             cmd.arg("-C").arg("panic=abort");
414         }
415     } else {
416         // For host crates (but not when we are just printing some info),
417         // we might still have to set the sysroot.
418         if !info_query {
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);
423             }
424         }
425
426         // For host crates or when we are printing, just forward everything.
427         cmd.args(args);
428     }
429
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" });
435
436     // Run it.
437     if verbose > 0 {
438         eprintln!(
439             "[cargo-miri rustc] target_crate={target_crate} runnable_crate={runnable_crate} info_query={info_query}"
440         );
441     }
442
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.
445     if emit_link_hack {
446         for filename in out_filenames() {
447             if verbose > 0 {
448                 eprintln!("[cargo-miri rustc] creating fake lib file at `{}`", filename.display());
449             }
450             File::create(filename).expect("failed to create fake lib file");
451         }
452     }
453
454     debug_cmd("[cargo-miri rustc]", verbose, &cmd);
455     exec(cmd);
456 }
457
458 #[derive(Debug, Copy, Clone, PartialEq)]
459 pub enum RunnerPhase {
460     /// `cargo` is running a binary
461     Cargo,
462     /// `rustdoc` is running a binary
463     Rustdoc,
464 }
465
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");
471
472     let verbose = std::env::var("MIRI_VERBOSE")
473         .map_or(0, |verbose| verbose.parse().expect("verbosity flag must be an integer"));
474
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
479         ));
480     let file = BufReader::new(file);
481
482     let info = serde_json::from_reader(file).unwrap_or_else(|_| {
483         show_error!("file {:?} contains outdated or invalid JSON; try `cargo clean`", binary)
484     });
485     let info = match info {
486         CrateRunInfo::RunWith(info) => info,
487         CrateRunInfo::SkipProcMacroTest => {
488             eprintln!(
489                 "Running unit tests of `proc-macro` crates is not currently supported by Miri."
490             );
491             return;
492         }
493     };
494
495     let mut cmd = miri();
496
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) {
501             if old_val == val {
502                 // This one did not actually change, no need to re-set it.
503                 // (This keeps the `debug_cmd` below more manageable.)
504                 continue;
505             } else if verbose > 0 {
506                 eprintln!(
507                     "[cargo-miri runner] Overwriting run-time env var {name:?}={old_val:?} with build-time value {val:?}"
508                 );
509             }
510         }
511         cmd.env(name, val);
512     }
513
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
517     // `.rmeta`.
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.
533         } else {
534             cmd.arg(arg);
535         }
536     }
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);
541         cmd.args(args);
542     }
543
544     // Then pass binary arguments.
545     cmd.arg("--");
546     cmd.args(binary_args);
547
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());
552
553     // Run it.
554     debug_cmd("[cargo-miri runner]", verbose, &cmd);
555     match phase {
556         RunnerPhase::Rustdoc => exec_with_pipe(cmd, &info.stdin, format!("{binary}.stdin")),
557         RunnerPhase::Cargo => exec(cmd),
558     }
559 }
560
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"));
564
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");
568
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.");
580         } else {
581             cmd.arg(arg);
582         }
583     }
584
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.");
589         return;
590     }
591
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.
595     //
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");
601
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");
605
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");
610
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
615
616     debug_cmd("[cargo-miri rustdoc]", verbose, &cmd);
617     exec(cmd)
618 }