]> git.lizzy.rs Git - rust.git/blobdiff - cargo-miri/bin.rs
no need for an exhaustive enum of subcommands
[rust.git] / cargo-miri / bin.rs
index 1f749b077ab9f141b251386a406f8bcf2f0306a6..2ab854b906a4f0fd27e2bbd8badbcb41d064bd95 100644 (file)
@@ -1,17 +1,22 @@
+#![feature(let_else)]
+#![allow(clippy::useless_format, clippy::derive_partial_eq_without_eq)]
+
+mod version;
+
 use std::env;
-use std::ffi::OsString;
+use std::ffi::{OsStr, OsString};
+use std::fmt::Write as _;
 use std::fs::{self, File};
-use std::iter::TakeWhile;
 use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
+use std::iter::TakeWhile;
 use std::ops::Not;
 use std::path::{Path, PathBuf};
-use std::process::Command;
-
-use serde::{Deserialize, Serialize};
+use std::process::{self, Command};
 
 use rustc_version::VersionMeta;
+use serde::{Deserialize, Serialize};
 
-const XARGO_MIN_VERSION: (u32, u32, u32) = (0, 3, 22);
+use version::*;
 
 const CARGO_MIRI_HELP: &str = r#"Runs binary crates and tests in Miri
 
@@ -19,8 +24,8 @@
     cargo miri [subcommand] [<cargo options>...] [--] [<program/test suite options>...]
 
 Subcommands:
-    run                      Run binaries
-    test                     Run tests
+    run, r                   Run binaries
+    test, t                  Run tests
     setup                    Only perform automatic setup, but without asking questions (for getting a proper libstd)
 
 The cargo options are exactly the same as for `cargo run` and `cargo test`, respectively.
     cargo miri test -- test-suite-filter
 "#;
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug)]
 enum MiriCommand {
-    Run,
-    Test,
+    /// Our own special 'setup' command.
     Setup,
+    /// A command to be forwarded to cargo.
+    Forward(String),
 }
 
 /// The information to run a crate with the given environment.
@@ -50,30 +56,32 @@ struct CrateRunEnv {
     stdin: Vec<u8>,
 }
 
-/// The information Miri needs to run a crate. Stored as JSON when the crate is "compiled".
-#[derive(Serialize, Deserialize)]
-enum CrateRunInfo {
-    /// Run it with the given environment.
-    RunWith(CrateRunEnv),
-    /// Skip it as Miri does not support interpreting such kind of crates.
-    SkipProcMacroTest,
-}
-
-impl CrateRunInfo {
+impl CrateRunEnv {
     /// Gather all the information we need.
-    fn collect(args: env::Args) -> Self {
+    fn collect(args: env::Args, capture_stdin: bool) -> Self {
         let args = args.collect();
         let env = env::vars_os().collect();
         let current_dir = env::current_dir().unwrap().into_os_string();
 
         let mut stdin = Vec::new();
-        if env::var_os("MIRI_CALLED_FROM_RUSTDOC").is_some() {
+        if capture_stdin {
             std::io::stdin().lock().read_to_end(&mut stdin).expect("cannot read stdin");
         }
 
-        Self::RunWith(CrateRunEnv { args, env, current_dir, stdin })
+        CrateRunEnv { args, env, current_dir, stdin }
     }
+}
+
+/// The information Miri needs to run a crate. Stored as JSON when the crate is "compiled".
+#[derive(Serialize, Deserialize)]
+enum CrateRunInfo {
+    /// Run it with the given environment.
+    RunWith(CrateRunEnv),
+    /// Skip it as Miri does not support interpreting such kind of crates.
+    SkipProcMacroTest,
+}
 
+impl CrateRunInfo {
     fn store(&self, filename: &Path) {
         let file = File::create(filename)
             .unwrap_or_else(|_| show_error(format!("cannot create `{}`", filename.display())));
@@ -88,12 +96,17 @@ fn show_help() {
 }
 
 fn show_version() {
-    println!(
-        "miri {} ({} {})",
-        env!("CARGO_PKG_VERSION"),
-        env!("VERGEN_GIT_SHA_SHORT"),
-        env!("VERGEN_GIT_COMMIT_DATE")
-    );
+    let mut version = format!("miri {}", env!("CARGO_PKG_VERSION"));
+    // Only use `option_env` on vergen variables to ensure the build succeeds
+    // when vergen failed to find the git info.
+    if let Some(sha) = option_env!("VERGEN_GIT_SHA_SHORT") {
+        // This `unwrap` can never fail because if VERGEN_GIT_SHA_SHORT exists, then so does
+        // VERGEN_GIT_COMMIT_DATE.
+        #[allow(clippy::option_env_unwrap)]
+        write!(&mut version, " ({} {})", sha, option_env!("VERGEN_GIT_COMMIT_DATE").unwrap())
+            .unwrap();
+    }
+    println!("{}", version);
 }
 
 fn show_error(msg: String) -> ! {
@@ -107,40 +120,58 @@ fn has_arg_flag(name: &str) -> bool {
     args.any(|val| val == name)
 }
 
-/// Yields all values of command line flag `name`.
-struct ArgFlagValueIter<'a> {
-    args: TakeWhile<env::Args, fn(&String) -> bool>,
+/// Yields all values of command line flag `name` as `Ok(arg)`, and all other arguments except
+/// the flag as `Err(arg)`. (The flag `name` itself is not yielded at all, only its values are.)
+struct ArgSplitFlagValue<'a, I> {
+    args: TakeWhile<I, fn(&String) -> bool>,
     name: &'a str,
 }
 
-impl<'a> ArgFlagValueIter<'a> {
-    fn new(name: &'a str) -> Self {
+impl<'a, I: Iterator<Item = String>> ArgSplitFlagValue<'a, I> {
+    fn new(args: I, name: &'a str) -> Self {
         Self {
             // Stop searching at `--`.
-            args: env::args().take_while(|val| val != "--"),
+            args: args.take_while(|val| val != "--"),
             name,
         }
     }
 }
 
-impl Iterator for ArgFlagValueIter<'_> {
-    type Item = String;
+impl<I: Iterator<Item = String>> Iterator for ArgSplitFlagValue<'_, I> {
+    type Item = Result<String, String>;
 
     fn next(&mut self) -> Option<Self::Item> {
-        loop {
-            let arg = self.args.next()?;
-            if !arg.starts_with(self.name) {
-                continue;
-            }
+        let arg = self.args.next()?;
+        if let Some(suffix) = arg.strip_prefix(self.name) {
             // Strip leading `name`.
-            let suffix = &arg[self.name.len()..];
             if suffix.is_empty() {
                 // This argument is exactly `name`; the next one is the value.
-                return self.args.next();
-            } else if suffix.starts_with('=') {
+                return self.args.next().map(Ok);
+            } else if let Some(suffix) = suffix.strip_prefix('=') {
                 // This argument is `name=value`; get the value.
-                // Strip leading `=`.
-                return Some(suffix[1..].to_owned());
+                return Some(Ok(suffix.to_owned()));
+            }
+        }
+        Some(Err(arg))
+    }
+}
+
+/// Yields all values of command line flag `name`.
+struct ArgFlagValueIter<'a>(ArgSplitFlagValue<'a, env::Args>);
+
+impl<'a> ArgFlagValueIter<'a> {
+    fn new(name: &'a str) -> Self {
+        Self(ArgSplitFlagValue::new(env::args(), name))
+    }
+}
+
+impl Iterator for ArgFlagValueIter<'_> {
+    type Item = String;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        loop {
+            if let Ok(value) = self.0.next()? {
+                return Some(value);
             }
         }
     }
@@ -163,6 +194,12 @@ fn forward_patched_extern_arg(args: &mut impl Iterator<Item = String>, cmd: &mut
     }
 }
 
+fn forward_miri_sysroot(cmd: &mut Command) {
+    let sysroot = env::var_os("MIRI_SYSROOT").expect("the wrapper should have set MIRI_SYSROOT");
+    cmd.arg("--sysroot");
+    cmd.arg(sysroot);
+}
+
 /// Returns the path to the `miri` binary
 fn find_miri() -> PathBuf {
     if let Some(path) = env::var_os("MIRI") {
@@ -202,7 +239,7 @@ fn exec(mut cmd: Command) {
 /// If it fails, fail this process with the same exit code.
 /// Otherwise, continue.
 fn exec_with_pipe(mut cmd: Command, input: &[u8]) {
-    cmd.stdin(std::process::Stdio::piped());
+    cmd.stdin(process::Stdio::piped());
     let mut child = cmd.spawn().expect("failed to spawn process");
     {
         let stdin = child.stdin.as_mut().expect("failed to open stdin");
@@ -223,7 +260,7 @@ fn xargo_version() -> Option<(u32, u32, u32)> {
     let line = out
         .stderr
         .lines()
-        .nth(0)
+        .next()
         .expect("malformed `xargo --version` output: not at least one line")
         .expect("malformed `xargo --version` output: error reading first line");
     let (name, version) = {
@@ -253,7 +290,7 @@ fn xargo_version() -> Option<(u32, u32, u32)> {
         .expect("malformed `xargo --version` output: not a patch version piece")
         .parse()
         .expect("malformed `xargo --version` output: patch version is not an integer");
-    if !version_pieces.next().is_none() {
+    if version_pieces.next().is_some() {
         panic!("malformed `xargo --version` output: more than three pieces in version");
     }
     Some((major, minor, patch))
@@ -279,17 +316,35 @@ fn ask_to_run(mut cmd: Command, ask: bool, text: &str) {
         println!("Running `{:?}` to {}.", cmd, text);
     }
 
-    if cmd.status().expect(&format!("failed to execute {:?}", cmd)).success().not() {
+    if cmd.status().unwrap_or_else(|_| panic!("failed to execute {:?}", cmd)).success().not() {
         show_error(format!("failed to {}", text));
     }
 }
 
+/// Writes the given content to the given file *cross-process atomically*, in the sense that another
+/// process concurrently reading that file will see either the old content or the new content, but
+/// not some intermediate (e.g., empty) state.
+///
+/// We assume no other parts of this same process are trying to read or write that file.
+fn write_to_file(filename: &Path, content: &str) {
+    // Create a temporary file with the desired contents.
+    let mut temp_filename = filename.as_os_str().to_os_string();
+    temp_filename.push(&format!(".{}", std::process::id()));
+    let mut temp_file = File::create(&temp_filename).unwrap();
+    temp_file.write_all(content.as_bytes()).unwrap();
+    drop(temp_file);
+
+    // Move file to the desired location.
+    fs::rename(temp_filename, filename).unwrap();
+}
+
 /// Performs the setup required to make `cargo miri` work: Getting a custom-built libstd. Then sets
 /// `MIRI_SYSROOT`. Skipped if `MIRI_SYSROOT` is already set, in which case we expect the user has
 /// done all this already.
-fn setup(subcommand: MiriCommand) {
+fn setup(subcommand: &MiriCommand) {
+    let only_setup = matches!(subcommand, MiriCommand::Setup);
     if std::env::var_os("MIRI_SYSROOT").is_some() {
-        if subcommand == MiriCommand::Setup {
+        if only_setup {
             println!("WARNING: MIRI_SYSROOT already set, not doing anything.")
         }
         return;
@@ -297,7 +352,7 @@ fn setup(subcommand: MiriCommand) {
 
     // Subcommands other than `setup` will do a setup if necessary, but
     // interactively confirm first.
-    let ask_user = subcommand != MiriCommand::Setup;
+    let ask_user = !only_setup;
 
     // First, we need xargo.
     if xargo_version().map_or(true, |v| v < XARGO_MIN_VERSION) {
@@ -310,8 +365,11 @@ fn setup(subcommand: MiriCommand) {
         ask_to_run(cmd, ask_user, "install a recent enough xargo");
     }
 
-    // Determine where the rust sources are located.  `XARGO_RUST_SRC` env var trumps everything.
-    let rust_src = match std::env::var_os("XARGO_RUST_SRC") {
+    // Determine where the rust sources are located.  The env vars manually setting the source
+    // (`MIRI_LIB_SRC`, `XARGO_RUST_SRC`) trump auto-detection.
+    let rust_src_env_var =
+        std::env::var_os("MIRI_LIB_SRC").or_else(|| std::env::var_os("XARGO_RUST_SRC"));
+    let rust_src = match rust_src_env_var {
         Some(path) => {
             let path = PathBuf::from(path);
             // Make path absolute if possible.
@@ -319,12 +377,15 @@ fn setup(subcommand: MiriCommand) {
         }
         None => {
             // Check for `rust-src` rustup component.
-            let sysroot = miri()
-                .args(&["--print", "sysroot"])
-                .output()
-                .expect("failed to determine sysroot")
-                .stdout;
-            let sysroot = std::str::from_utf8(&sysroot).unwrap();
+            let output =
+                miri().args(&["--print", "sysroot"]).output().expect("failed to determine sysroot");
+            if !output.status.success() {
+                show_error(format!(
+                    "Failed to determine sysroot; Miri said:\n{}",
+                    String::from_utf8_lossy(&output.stderr).trim_end()
+                ));
+            }
+            let sysroot = std::str::from_utf8(&output.stdout).unwrap();
             let sysroot = Path::new(sysroot.trim_end_matches('\n'));
             // Check for `$SYSROOT/lib/rustlib/src/rust/library`; test if that contains `std/Cargo.toml`.
             let rustup_src =
@@ -345,6 +406,13 @@ fn setup(subcommand: MiriCommand) {
     if !rust_src.exists() {
         show_error(format!("given Rust source directory `{}` does not exist.", rust_src.display()));
     }
+    if rust_src.file_name().and_then(OsStr::to_str) != Some("library") {
+        show_error(format!(
+            "given Rust source directory `{}` does not seem to be the `library` subdirectory of \
+             a Rust source checkout.",
+            rust_src.display()
+        ));
+    }
 
     // Next, we need our own libstd. Prepare a xargo project for that purpose.
     // We will do this work in whatever is a good cache dir for this platform.
@@ -353,26 +421,25 @@ fn setup(subcommand: MiriCommand) {
     if !dir.exists() {
         fs::create_dir_all(&dir).unwrap();
     }
-    // The interesting bit: Xargo.toml
-    File::create(dir.join("Xargo.toml"))
-        .unwrap()
-        .write_all(
-            br#"
+    // The interesting bit: Xargo.toml (only needs content if we actually need std)
+    let xargo_toml = if std::env::var_os("MIRI_NO_STD").is_some() {
+        ""
+    } else {
+        r#"
 [dependencies.std]
 default_features = false
 # We support unwinding, so enable that panic runtime.
 features = ["panic_unwind", "backtrace"]
 
 [dependencies.test]
-"#,
-        )
-        .unwrap();
+"#
+    };
+    write_to_file(&dir.join("Xargo.toml"), xargo_toml);
     // The boring bits: a dummy project for xargo.
     // FIXME: With xargo-check, can we avoid doing this?
-    File::create(dir.join("Cargo.toml"))
-        .unwrap()
-        .write_all(
-            br#"
+    write_to_file(
+        &dir.join("Cargo.toml"),
+        r#"
 [package]
 name = "miri-xargo"
 description = "A dummy project for building libstd with xargo."
@@ -381,9 +448,8 @@ fn setup(subcommand: MiriCommand) {
 [lib]
 path = "lib.rs"
 "#,
-        )
-        .unwrap();
-    File::create(dir.join("lib.rs")).unwrap();
+    );
+    write_to_file(&dir.join("lib.rs"), "#![no_std]");
 
     // Determine architectures.
     // We always need to set a target so rustc bootstrap can tell apart host from target crates.
@@ -404,14 +470,14 @@ fn setup(subcommand: MiriCommand) {
     // for target crates.
     // We set ourselves (`cargo-miri`) instead of Miri directly to be able to patch the flags
     // for `libpanic_abort` (usually this is done by bootstrap but we have to do it ourselves).
-    // The `MIRI_BE_RUSTC` will mean we dispatch to `phase_setup_rustc`.
+    // The `MIRI_CALLED_FROM_XARGO` will mean we dispatch to `phase_setup_rustc`.
     let cargo_miri_path = std::env::current_exe().expect("current executable path invalid");
     if env::var_os("RUSTC_STAGE").is_some() {
         command.env("RUSTC_REAL", &cargo_miri_path);
     } else {
         command.env("RUSTC", &cargo_miri_path);
     }
-    command.env("MIRI_BE_RUSTC", "1");
+    command.env("MIRI_CALLED_FROM_XARGO", "1");
     // Make sure there are no other wrappers or flags getting in our way
     // (Cc https://github.com/rust-lang/miri/issues/1421).
     // This is consistent with normal `cargo build` that does not apply `RUSTFLAGS`
@@ -432,28 +498,68 @@ fn setup(subcommand: MiriCommand) {
     let sysroot = if target == &host { dir.join("HOST") } else { PathBuf::from(dir) };
     std::env::set_var("MIRI_SYSROOT", &sysroot); // pass the env var to the processes we spawn, which will turn it into "--sysroot" flags
     // Figure out what to print.
-    let print_sysroot = subcommand == MiriCommand::Setup && has_arg_flag("--print-sysroot"); // whether we just print the sysroot path
+    let print_sysroot = only_setup && has_arg_flag("--print-sysroot"); // whether we just print the sysroot path
     if print_sysroot {
         // Print just the sysroot and nothing else; this way we do not need any escaping.
         println!("{}", sysroot.display());
-    } else if subcommand == MiriCommand::Setup {
+    } else if only_setup {
         println!("A libstd for Miri is now available in `{}`.", sysroot.display());
     }
 }
 
-fn phase_setup_rustc(args: env::Args) {
-    // Mostly we just forward everything.
-    // `MIRI_BE_RUST` is already set.
-    let mut cmd = miri();
-    cmd.args(args);
+#[derive(Deserialize)]
+struct Metadata {
+    target_directory: PathBuf,
+    workspace_members: Vec<String>,
+}
 
-    // Patch the panic runtime for `libpanic_abort` (mirroring what bootstrap usually does).
-    if get_arg_flag_value("--crate-name").as_deref() == Some("panic_abort") {
-        cmd.arg("-C").arg("panic=abort");
+fn get_cargo_metadata() -> Metadata {
+    let mut cmd = cargo();
+    // `-Zunstable-options` is required by `--config`.
+    cmd.args(["metadata", "--no-deps", "--format-version=1", "-Zunstable-options"]);
+    // The `build.target-dir` config can be passed by `--config` flags, so forward them to
+    // `cargo metadata`.
+    let config_flag = "--config";
+    for arg in ArgSplitFlagValue::new(
+        env::args().skip(3), // skip the program name, "miri" and "run" / "test"
+        config_flag,
+    )
+    // Only look at `Ok`
+    .flatten()
+    {
+        cmd.arg(config_flag).arg(arg);
     }
+    let mut child = cmd
+        .stdin(process::Stdio::null())
+        .stdout(process::Stdio::piped())
+        .spawn()
+        .expect("failed ro run `cargo metadata`");
+    // Check this `Result` after `status.success()` is checked, so we don't print the error
+    // to stderr if `cargo metadata` is also printing to stderr.
+    let metadata: Result<Metadata, _> = serde_json::from_reader(child.stdout.take().unwrap());
+    let status = child.wait().expect("failed to wait for `cargo metadata` to exit");
+    if !status.success() {
+        std::process::exit(status.code().unwrap_or(-1));
+    }
+    metadata.unwrap_or_else(|e| show_error(format!("invalid `cargo metadata` output: {}", e)))
+}
 
-    // Run it!
-    exec(cmd);
+/// Pulls all the crates in this workspace from the cargo metadata.
+/// Workspace members are emitted like "miri 0.1.0 (path+file:///path/to/miri)"
+/// Additionally, somewhere between cargo metadata and TyCtxt, '-' gets replaced with '_' so we
+/// make that same transformation here.
+fn local_crates(metadata: &Metadata) -> String {
+    assert!(!metadata.workspace_members.is_empty());
+    let mut local_crates = String::new();
+    for member in &metadata.workspace_members {
+        let name = member.split(' ').next().unwrap();
+        let name = name.replace('-', "_");
+        local_crates.push_str(&name);
+        local_crates.push(',');
+    }
+    local_crates.pop(); // Remove the trailing ','
+
+    local_crates
 }
 
 fn phase_cargo_miri(mut args: env::Args) {
@@ -470,17 +576,22 @@ fn phase_cargo_miri(mut args: env::Args) {
     // Require a subcommand before any flags.
     // We cannot know which of those flags take arguments and which do not,
     // so we cannot detect subcommands later.
-    let subcommand = match args.next().as_deref() {
-        Some("test") => MiriCommand::Test,
-        Some("run") => MiriCommand::Run,
-        Some("setup") => MiriCommand::Setup,
+    let Some(subcommand) = args.next() else {
+        show_error(format!("`cargo miri` needs to be called with a subcommand (`run`, `test`)"));
+    };
+    let subcommand = match &*subcommand {
+        "setup" => MiriCommand::Setup,
+        "test" | "t" | "run" | "r" => MiriCommand::Forward(subcommand),
         // Invalid command.
-        _ => show_error(format!("`cargo miri` supports the following subcommands: `run`, `test`, and `setup`.")),
+        _ =>
+            show_error(format!(
+                "`cargo miri` supports the following subcommands: `run`, `test`, and `setup`."
+            )),
     };
     let verbose = has_arg_flag("-v");
 
     // We always setup.
-    setup(subcommand);
+    setup(&subcommand);
 
     // Invoke actual cargo for the job, but with different flags.
     // We re-use `cargo test` and `cargo run`, which makes target and binary handling very easy but
@@ -490,8 +601,7 @@ fn phase_cargo_miri(mut args: env::Args) {
     // harder.
     let cargo_miri_path = std::env::current_exe().expect("current executable path invalid");
     let cargo_cmd = match subcommand {
-        MiriCommand::Test => "test",
-        MiriCommand::Run => "run",
+        MiriCommand::Forward(s) => s,
         MiriCommand::Setup => return, // `cargo miri setup` stops here.
     };
     let mut cmd = cargo();
@@ -512,58 +622,55 @@ fn phase_cargo_miri(mut args: env::Args) {
         &host
     };
 
-    // Forward all further arguments. We do some processing here because we want to
-    // detect people still using the old way of passing flags to Miri
-    // (`cargo miri -- -Zmiri-foo`).
-    while let Some(arg) = args.next() {
-        cmd.arg(&arg);
-        if arg == "--" {
-            // Check if the next argument starts with `-Zmiri`. If yes, we assume
-            // this is an old-style invocation.
-            if let Some(next_arg) = args.next() {
-                if next_arg.starts_with("-Zmiri") || next_arg == "--" {
-                    eprintln!(
-                        "WARNING: it seems like you are setting Miri's flags in `cargo miri` the old way,\n\
-                        i.e., by passing them after the first `--`. This style is deprecated; please set\n\
-                        the MIRIFLAGS environment variable instead. `cargo miri run/test` now interprets\n\
-                        arguments the exact same way as `cargo run/test`."
-                    );
-                    // Old-style invocation. Turn these into MIRIFLAGS, if there are any.
-                    if next_arg != "--" {
-                        let mut miriflags = env::var("MIRIFLAGS").unwrap_or_default();
-                        miriflags.push(' ');
-                        miriflags.push_str(&next_arg);
-                        while let Some(further_arg) = args.next() {
-                            if further_arg == "--" {
-                                // End of the Miri flags!
-                                break;
-                            }
-                            miriflags.push(' ');
-                            miriflags.push_str(&further_arg);
-                        }
-                        env::set_var("MIRIFLAGS", miriflags);
-                    }
-                    // Pass the remaining flags to cargo.
-                    cmd.args(args);
-                    break;
+    let mut target_dir = None;
+
+    // Forward all arguments before `--` other than `--target-dir` and its value to Cargo.
+    for arg in ArgSplitFlagValue::new(&mut args, "--target-dir") {
+        match arg {
+            Ok(value) => {
+                if target_dir.is_some() {
+                    show_error(format!("`--target-dir` is provided more than once"));
                 }
-                // Not a Miri argument after all, make sure we pass it to cargo.
-                cmd.arg(next_arg);
+                target_dir = Some(value.into());
+            }
+            Err(arg) => {
+                cmd.arg(arg);
             }
         }
     }
 
+    let metadata = get_cargo_metadata();
+
+    // Detect the target directory if it's not specified via `--target-dir`.
+    let target_dir = target_dir.get_or_insert_with(|| metadata.target_directory.clone());
+
+    // Set `--target-dir` to `miri` inside the original target directory.
+    target_dir.push("miri");
+    cmd.arg("--target-dir").arg(target_dir);
+
+    // Forward all further arguments after `--` to cargo.
+    cmd.arg("--").args(args);
+
     // Set `RUSTC_WRAPPER` to ourselves.  Cargo will prepend that binary to its usual invocation,
     // i.e., the first argument is `rustc` -- which is what we use in `main` to distinguish
     // the two codepaths. (That extra argument is why we prefer this over setting `RUSTC`.)
     if env::var_os("RUSTC_WRAPPER").is_some() {
-        println!("WARNING: Ignoring `RUSTC_WRAPPER` environment variable, Miri does not support wrapping.");
+        println!(
+            "WARNING: Ignoring `RUSTC_WRAPPER` environment variable, Miri does not support wrapping."
+        );
     }
     cmd.env("RUSTC_WRAPPER", &cargo_miri_path);
+    // Having both `RUSTC_WRAPPER` and `RUSTC` set does some odd things, so let's avoid that.
+    // See <https://github.com/rust-lang/miri/issues/2238>.
+    if env::var_os("RUSTC").is_some() && env::var_os("MIRI").is_none() {
+        println!(
+            "WARNING: Ignoring `RUSTC` environment variable; set `MIRI` if you want to control the binary used as the driver."
+        );
+    }
+    cmd.env_remove("RUSTC");
 
-    let runner_env_name = |triple: &str| {
-        format!("CARGO_TARGET_{}_RUNNER", triple.to_uppercase().replace('-', "_"))
-    };
+    let runner_env_name =
+        |triple: &str| format!("CARGO_TARGET_{}_RUNNER", triple.to_uppercase().replace('-', "_"));
     let host_runner_env_name = runner_env_name(&host);
     let target_runner_env_name = runner_env_name(target);
     // Set the target runner to us, so we can interpret the binaries.
@@ -572,9 +679,11 @@ fn phase_cargo_miri(mut args: env::Args) {
     // us in order to skip them.
     cmd.env(&host_runner_env_name, &cargo_miri_path);
 
-    // Set rustdoc to us as well, so we can make it do nothing (see issue #584).
+    // Set rustdoc to us as well, so we can run doctests.
     cmd.env("RUSTDOC", &cargo_miri_path);
 
+    cmd.env("MIRI_LOCAL_CRATES", local_crates(&metadata));
+
     // Run cargo.
     if verbose {
         eprintln!("[cargo-miri miri] RUSTC_WRAPPER={:?}", cargo_miri_path);
@@ -589,7 +698,17 @@ fn phase_cargo_miri(mut args: env::Args) {
     exec(cmd)
 }
 
-fn phase_cargo_rustc(mut args: env::Args) {
+#[derive(Debug, Copy, Clone, PartialEq)]
+enum RustcPhase {
+    /// `rustc` called via `xargo` for sysroot build.
+    Setup,
+    /// `rustc` called by `cargo` for regular build.
+    Build,
+    /// `rustc` called by `rustdoc` for doctest.
+    Rustdoc,
+}
+
+fn phase_rustc(mut args: env::Args, phase: RustcPhase) {
     /// Determines if we are being invoked (as rustc) to build a crate for
     /// the "target" architecture, in contrast to the "host" architecture.
     /// Host crates are for build scripts and proc macros and still need to
@@ -623,7 +742,7 @@ fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
                 get_arg_flag_value("--crate-name").unwrap(),
                 // This is technically a `-C` flag but the prefix seems unique enough...
                 // (and cargo passes this before the filename so it should be unique)
-                get_arg_flag_value("extra-filename").unwrap_or(String::new()),
+                get_arg_flag_value("extra-filename").unwrap_or_default(),
                 suffix,
             ));
             path
@@ -635,9 +754,9 @@ fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
 
     let verbose = std::env::var_os("MIRI_VERBOSE").is_some();
     let target_crate = is_target_crate();
-    let print = get_arg_flag_value("--print").is_some(); // whether this is cargo passing `--print` to get some infos
+    let print = get_arg_flag_value("--print").is_some() || has_arg_flag("-vV"); // whether this is cargo/xargo invoking rustc to get some infos
 
-    let store_json = |info: &CrateRunInfo| {
+    let store_json = |info: CrateRunInfo| {
         // Create a stub .d file to stop Cargo from "rebuilding" the crate:
         // https://github.com/rust-lang/miri/issues/1724#issuecomment-787115693
         // As we store a JSON file instead of building the crate here, an empty file is fine.
@@ -660,52 +779,50 @@ fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
     let runnable_crate = !print && is_runnable_crate();
 
     if runnable_crate && target_crate {
+        assert!(
+            phase != RustcPhase::Setup,
+            "there should be no interpretation during sysroot build"
+        );
+        let inside_rustdoc = phase == RustcPhase::Rustdoc;
         // This is the binary or test crate that we want to interpret under Miri.
         // But we cannot run it here, as cargo invoked us as a compiler -- our stdin and stdout are not
         // like we want them.
         // Instead of compiling, we write JSON into the output file with all the relevant command-line flags
         // and environment variables; this is used when cargo calls us again in the CARGO_TARGET_RUNNER phase.
-        let info = CrateRunInfo::collect(args);
-        store_json(&info);
+        let env = CrateRunEnv::collect(args, inside_rustdoc);
 
         // Rustdoc expects us to exit with an error code if the test is marked as `compile_fail`,
         // just creating the JSON file is not enough: we need to detect syntax errors,
         // so we need to run Miri with `MIRI_BE_RUSTC` for a check-only build.
-        if std::env::var_os("MIRI_CALLED_FROM_RUSTDOC").is_some() {
+        if inside_rustdoc {
             let mut cmd = miri();
-            let env = if let CrateRunInfo::RunWith(env) = info {
-                env
-            } else {
-                return;
-            };
-
-            // use our own sysroot
-            if !has_arg_flag("--sysroot") {
-                let sysroot = env::var_os("MIRI_SYSROOT")
-                    .expect("the wrapper should have set MIRI_SYSROOT");
-                cmd.arg("--sysroot").arg(sysroot);
-            }
-            
-            // ensure --emit argument for a check-only build is present
+
+            // Ensure --emit argument for a check-only build is present.
+            // We cannot use the usual helpers since we need to check specifically in `env.args`.
             if let Some(i) = env.args.iter().position(|arg| arg.starts_with("--emit=")) {
-                // We need to make sure we're not producing a binary that overwrites the JSON file.
-                // rustdoc should only ever pass an --emit=metadata argument for tests marked as `no_run`:
+                // For `no_run` tests, rustdoc passes a `--emit` flag; make sure it has the right shape.
                 assert_eq!(env.args[i], "--emit=metadata");
             } else {
-                cmd.arg("--emit=dep-info,metadata");
+                // For all other kinds of tests, we can just add our flag.
+                cmd.arg("--emit=metadata");
             }
 
-            cmd.args(env.args);
-            cmd.env("MIRI_BE_RUSTC", "1");
+            cmd.args(&env.args);
+            cmd.env("MIRI_BE_RUSTC", "target");
 
             if verbose {
-                eprintln!("[cargo-miri rustc] captured input:\n{}", std::str::from_utf8(&env.stdin).unwrap());
+                eprintln!(
+                    "[cargo-miri rustc] captured input:\n{}",
+                    std::str::from_utf8(&env.stdin).unwrap()
+                );
                 eprintln!("[cargo-miri rustc] {:?}", cmd);
             }
-            
+
             exec_with_pipe(cmd, &env.stdin);
         }
 
+        store_json(CrateRunInfo::RunWith(env));
+
         return;
     }
 
@@ -713,7 +830,7 @@ fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
         // This is a "runnable" `proc-macro` crate (unit tests). We do not support
         // interpreting that under Miri now, so we write a JSON file to (display a
         // helpful message and) skip it in the runner phase.
-        store_json(&CrateRunInfo::SkipProcMacroTest);
+        store_json(CrateRunInfo::SkipProcMacroTest);
         return;
     }
 
@@ -725,11 +842,10 @@ fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
         // Forward arguments, but remove "link" from "--emit" to make this a check-only build.
         let emit_flag = "--emit";
         while let Some(arg) = args.next() {
-            if arg.starts_with(emit_flag) {
+            if let Some(val) = arg.strip_prefix(emit_flag) {
                 // Patch this argument. First, extract its value.
-                let val = &arg[emit_flag.len()..];
-                assert!(val.starts_with("="), "`cargo` should pass `--emit=X` as one argument");
-                let val = &val[1..];
+                let val =
+                    val.strip_prefix('=').expect("`cargo` should pass `--emit=X` as one argument");
                 let mut val: Vec<_> = val.split(',').collect();
                 // Now make sure "link" is not in there, but "metadata" is.
                 if let Some(i) = val.iter().position(|&s| s == "link") {
@@ -749,11 +865,17 @@ fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
             }
         }
 
-        // Use our custom sysroot.
-        let sysroot =
-            env::var_os("MIRI_SYSROOT").expect("the wrapper should have set MIRI_SYSROOT");
-        cmd.arg("--sysroot");
-        cmd.arg(sysroot);
+        // Use our custom sysroot (but not if that is what we are currently building).
+        if phase != RustcPhase::Setup {
+            forward_miri_sysroot(&mut cmd);
+        }
+
+        // During setup, patch the panic runtime for `libpanic_abort` (mirroring what bootstrap usually does).
+        if phase == RustcPhase::Setup
+            && get_arg_flag_value("--crate-name").as_deref() == Some("panic_abort")
+        {
+            cmd.arg("-C").arg("panic=abort");
+        }
     } else {
         // For host crates or when we are printing, just forward everything.
         cmd.args(args);
@@ -761,7 +883,9 @@ fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
 
     // We want to compile, not interpret. We still use Miri to make sure the compiler version etc
     // are the exact same as what is used for interpretation.
-    cmd.env("MIRI_BE_RUSTC", "1");
+    // MIRI_DEFAULT_ARGS should not be used to build host crates, hence setting "target" or "host"
+    // as the value here to help Miri differentiate them.
+    cmd.env("MIRI_BE_RUSTC", if target_crate { "target" } else { "host" });
 
     // Run it.
     if verbose {
@@ -784,31 +908,33 @@ fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
     }
 }
 
-fn forward_patched_extern_arg(args: &mut impl Iterator<Item = String>, cmd: &mut Command) {
-    cmd.arg("--extern"); // always forward flag, but adjust filename:
-    let path = args.next().expect("`--extern` should be followed by a filename");
-    if let Some(lib) = path.strip_suffix(".rlib") {
-        // If this is an rlib, make it an rmeta.
-        cmd.arg(format!("{}.rmeta", lib));
-    } else {
-        // Some other extern file (e.g. a `.so`). Forward unchanged.
-        cmd.arg(path);
-    }
+#[derive(Debug, Copy, Clone, PartialEq)]
+enum RunnerPhase {
+    /// `cargo` is running a binary
+    Cargo,
+    /// `rustdoc` is running a binary
+    Rustdoc,
 }
 
-fn phase_cargo_runner(binary: &Path, binary_args: env::Args) {
+fn phase_runner(binary: &Path, binary_args: env::Args, phase: RunnerPhase) {
     let verbose = std::env::var_os("MIRI_VERBOSE").is_some();
 
     let file = File::open(&binary)
         .unwrap_or_else(|_| show_error(format!("file {:?} not found or `cargo-miri` invoked incorrectly; please only invoke this binary through `cargo miri`", binary)));
     let file = BufReader::new(file);
 
-    let info = serde_json::from_reader(file)
-        .unwrap_or_else(|_| show_error(format!("file {:?} contains outdated or invalid JSON; try `cargo clean`", binary)));
+    let info = serde_json::from_reader(file).unwrap_or_else(|_| {
+        show_error(format!(
+            "file {:?} contains outdated or invalid JSON; try `cargo clean`",
+            binary
+        ))
+    });
     let info = match info {
         CrateRunInfo::RunWith(info) => info,
         CrateRunInfo::SkipProcMacroTest => {
-            eprintln!("Running unit tests of `proc-macro` crates is not currently supported by Miri.");
+            eprintln!(
+                "Running unit tests of `proc-macro` crates is not currently supported by Miri."
+            );
             return;
         }
     };
@@ -821,7 +947,10 @@ fn phase_cargo_runner(binary: &Path, binary_args: env::Args) {
         if verbose {
             if let Some(old_val) = env::var_os(&name) {
                 if old_val != val {
-                    eprintln!("[cargo-miri runner] Overwriting run-time env var {:?}={:?} with build-time value {:?}", name, old_val, val);
+                    eprintln!(
+                        "[cargo-miri runner] Overwriting run-time env var {:?}={:?} with build-time value {:?}",
+                        name, old_val, val
+                    );
                 }
             }
         }
@@ -841,31 +970,24 @@ fn phase_cargo_runner(binary: &Path, binary_args: env::Args) {
     while let Some(arg) = args.next() {
         if arg == "--extern" {
             forward_patched_extern_arg(&mut args, &mut cmd);
-        } else if arg.starts_with(error_format_flag) {
-            let suffix = &arg[error_format_flag.len()..];
+        } else if let Some(suffix) = arg.strip_prefix(error_format_flag) {
             assert!(suffix.starts_with('='));
             // Drop this argument.
-        } else if arg.starts_with(json_flag) {
-            let suffix = &arg[json_flag.len()..];
+        } else if let Some(suffix) = arg.strip_prefix(json_flag) {
             assert!(suffix.starts_with('='));
             // Drop this argument.
         } else {
             cmd.arg(arg);
         }
     }
-    // Set sysroot.
-    let sysroot =
-        env::var_os("MIRI_SYSROOT").expect("the wrapper should have set MIRI_SYSROOT");
-    cmd.arg("--sysroot");
-    cmd.arg(sysroot);
+    // Set sysroot (if we are inside rustdoc, we already did that in `phase_cargo_rustdoc`).
+    if phase != RunnerPhase::Rustdoc {
+        forward_miri_sysroot(&mut cmd);
+    }
     // Respect `MIRIFLAGS`.
     if let Ok(a) = env::var("MIRIFLAGS") {
         // This code is taken from `RUSTFLAGS` handling in cargo.
-        let args = a
-            .split(' ')
-            .map(str::trim)
-            .filter(|s| !s.is_empty())
-            .map(str::to_string);
+        let args = a.split(' ').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string);
         cmd.args(args);
     }
 
@@ -883,19 +1005,18 @@ fn phase_cargo_runner(binary: &Path, binary_args: env::Args) {
         eprintln!("[cargo-miri runner] {:?}", cmd);
     }
 
-    if std::env::var_os("MIRI_CALLED_FROM_RUSTDOC").is_some() {
-        exec_with_pipe(cmd, &info.stdin)
-    } else {
-        exec(cmd)
+    match phase {
+        RunnerPhase::Rustdoc => exec_with_pipe(cmd, &info.stdin),
+        RunnerPhase::Cargo => exec(cmd),
     }
 }
 
-fn phase_cargo_rustdoc(fst_arg: &str, mut args: env::Args) {
+fn phase_rustdoc(fst_arg: &str, mut args: env::Args) {
     let verbose = std::env::var_os("MIRI_VERBOSE").is_some();
 
     // phase_cargo_miri sets the RUSTDOC env var to ourselves, so we can't use that here;
     // just default to a straight-forward invocation for now:
-    let mut cmd = Command::new(OsString::from("rustdoc"));
+    let mut cmd = Command::new("rustdoc");
 
     // Because of the way the main function is structured, we have to take the first argument spearately
     // from the rest; to simplify the following argument patching loop, we'll just skip that one.
@@ -904,8 +1025,9 @@ fn phase_cargo_rustdoc(fst_arg: &str, mut args: env::Args) {
     let extern_flag = "--extern";
     assert!(fst_arg != extern_flag);
     cmd.arg(fst_arg);
-    
+
     let runtool_flag = "--runtool";
+    // `crossmode` records if *any* argument matches `runtool_flag`; here we check the first one.
     let mut crossmode = fst_arg == runtool_flag;
     while let Some(arg) = args.next() {
         if arg == extern_flag {
@@ -923,28 +1045,40 @@ fn phase_cargo_rustdoc(fst_arg: &str, mut args: env::Args) {
     }
 
     if crossmode {
-        eprintln!("Cross-interpreting doc-tests is not currently supported by Miri.");
+        show_error(format!("cross-interpreting doctests is not currently supported by Miri."));
+    }
+
+    // Doctests of `proc-macro` crates (and their dependencies) are always built for the host,
+    // so we are not able to run them in Miri.
+    if ArgFlagValueIter::new("--crate-type").any(|crate_type| crate_type == "proc-macro") {
+        eprintln!("Running doctests of `proc-macro` crates is not currently supported by Miri.");
         return;
     }
 
-    // For each doc-test, rustdoc starts two child processes: first the test is compiled,
+    // For each doctest, rustdoc starts two child processes: first the test is compiled,
     // then the produced executable is invoked. We want to reroute both of these to cargo-miri,
     // such that the first time we'll enter phase_cargo_rustc, and phase_cargo_runner second.
-    // 
+    //
     // rustdoc invokes the test-builder by forwarding most of its own arguments, which makes
     // it difficult to determine when phase_cargo_rustc should run instead of phase_cargo_rustdoc.
     // Furthermore, the test code is passed via stdin, rather than a temporary file, so we need
     // to let phase_cargo_rustc know to expect that. We'll use this environment variable as a flag:
     cmd.env("MIRI_CALLED_FROM_RUSTDOC", "1");
-    
+
     // The `--test-builder` and `--runtool` arguments are unstable rustdoc features,
     // which are disabled by default. We first need to enable them explicitly:
     cmd.arg("-Z").arg("unstable-options");
-    
+
+    // rustdoc needs to know the right sysroot.
+    forward_miri_sysroot(&mut cmd);
+    // make sure the 'miri' flag is set for rustdoc
+    cmd.arg("--cfg").arg("miri");
+
+    // Make rustdoc call us back.
     let cargo_miri_path = std::env::current_exe().expect("current executable path invalid");
     cmd.arg("--test-builder").arg(&cargo_miri_path); // invoked by forwarding most arguments
     cmd.arg("--runtool").arg(&cargo_miri_path); // invoked with just a single path argument
-    
+
     if verbose {
         eprintln!("[cargo-miri rustdoc] {:?}", cmd);
     }
@@ -959,17 +1093,28 @@ fn main() {
     // Skip binary name.
     args.next().unwrap();
 
+    // Dispatch to `cargo-miri` phase. There are four phases:
+    // - When we are called via `cargo miri`, we run as the frontend and invoke the underlying
+    //   cargo. We set RUSTDOC, RUSTC_WRAPPER and CARGO_TARGET_RUNNER to ourselves.
+    // - When we are executed due to RUSTDOC, we run rustdoc and set both `--test-builder` and
+    //   `--runtool` to ourselves.
+    // - When we are executed due to RUSTC_WRAPPER (or as the rustdoc test builder), we build crates
+    //   or store the flags of binary crates for later interpretation.
+    // - When we are executed due to CARGO_TARGET_RUNNER (or as the rustdoc runtool), we start
+    //   interpretation based on the flags that were stored earlier.
+    //
+    // Additionally, we also set ourselves as RUSTC when calling xargo to build the sysroot, which
+    // has to be treated slightly differently than when we build regular crates.
+
     // Dispatch running as part of sysroot compilation.
-    if env::var_os("MIRI_BE_RUSTC").is_some() {
-        phase_setup_rustc(args);
+    if env::var_os("MIRI_CALLED_FROM_XARGO").is_some() {
+        phase_rustc(args, RustcPhase::Setup);
         return;
     }
 
-    // The way rustdoc invokes rustc is indistuingishable from the way cargo invokes rustdoc
-    // by the arguments alone, and we can't take from the args iterator in this case.
-    // phase_cargo_rustdoc sets this environment variable to let us disambiguate here
-    let invoked_by_rustdoc = env::var_os("MIRI_CALLED_FROM_RUSTDOC").is_some();
-    if invoked_by_rustdoc {
+    // The way rustdoc invokes rustc is indistuingishable from the way cargo invokes rustdoc by the
+    // arguments alone. `phase_cargo_rustdoc` sets this environment variable to let us disambiguate.
+    if env::var_os("MIRI_CALLED_FROM_RUSTDOC").is_some() {
         // ...however, we then also see this variable when rustdoc invokes us as the testrunner!
         // The runner is invoked as `$runtool ($runtool-arg)* output_file`;
         // since we don't specify any runtool-args, and rustdoc supplies multiple arguments to
@@ -978,28 +1123,23 @@ fn main() {
             let arg = args.next().unwrap();
             let binary = Path::new(&arg);
             if binary.exists() {
-                phase_cargo_runner(binary, args);
+                phase_runner(binary, args, RunnerPhase::Rustdoc);
             } else {
-                show_error(format!("`cargo-miri` called with non-existing path argument `{}` in rustdoc mode; please invoke this binary through `cargo miri`", arg));
+                show_error(format!(
+                    "`cargo-miri` called with non-existing path argument `{}` in rustdoc mode; please invoke this binary through `cargo miri`",
+                    arg
+                ));
             }
         } else {
-            phase_cargo_rustc(args);
+            phase_rustc(args, RustcPhase::Rustdoc);
         }
 
         return;
     }
 
-    // Dispatch to `cargo-miri` phase. There are three phases:
-    // - When we are called via `cargo miri`, we run as the frontend and invoke the underlying
-    //   cargo. We set RUSTC_WRAPPER and CARGO_TARGET_RUNNER to ourselves.
-    // - When we are executed due to RUSTC_WRAPPER, we build crates or store the flags of
-    //   binary crates for later interpretation.
-    // - When we are executed due to CARGO_TARGET_RUNNER, we start interpretation based on the
-    //   flags that were stored earlier.
-    // On top of that, we are also called as RUSTDOC, but that is just a stub currently.
     match args.next().as_deref() {
         Some("miri") => phase_cargo_miri(args),
-        Some("rustc") => phase_cargo_rustc(args),
+        Some("rustc") => phase_rustc(args, RustcPhase::Build),
         Some(arg) => {
             // We have to distinguish the "runner" and "rustdoc" cases.
             // As runner, the first argument is the binary (a file that should exist, with an absolute path);
@@ -1007,13 +1147,19 @@ fn main() {
             let binary = Path::new(arg);
             if binary.exists() {
                 assert!(!arg.starts_with("--")); // not a flag
-                phase_cargo_runner(binary, args);
+                phase_runner(binary, args, RunnerPhase::Cargo);
             } else if arg.starts_with("--") {
-                phase_cargo_rustdoc(arg, args);
+                phase_rustdoc(arg, args);
             } else {
-                show_error(format!("`cargo-miri` called with unexpected first argument `{}`; please only invoke this binary through `cargo miri`", arg));
+                show_error(format!(
+                    "`cargo-miri` called with unexpected first argument `{}`; please only invoke this binary through `cargo miri`",
+                    arg
+                ));
             }
         }
-        _ => show_error(format!("`cargo-miri` called without first argument; please only invoke this binary through `cargo miri`")),
+        _ =>
+            show_error(format!(
+                "`cargo-miri` called without first argument; please only invoke this binary through `cargo miri`"
+            )),
     }
 }