]> 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 e9596e56f742451b74ab0c1a3b198c33c5ac5308..2ab854b906a4f0fd27e2bbd8badbcb41d064bd95 100644 (file)
@@ -1,18 +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::io::{self, BufRead, BufReader, BufWriter, Read, Write};
 use std::iter::TakeWhile;
 use std::ops::Not;
 use std::path::{Path, PathBuf};
-use std::process::{Command, Stdio};
-
-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
 
@@ -20,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.
@@ -95,6 +100,9 @@ fn show_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();
     }
@@ -113,7 +121,7 @@ fn has_arg_flag(name: &str) -> bool {
 }
 
 /// Yields all values of command line flag `name` as `Ok(arg)`, and all other arguments except
-/// the flag as `Err(arg)`.
+/// 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,
@@ -134,16 +142,14 @@ impl<I: Iterator<Item = String>> Iterator for ArgSplitFlagValue<'_, I> {
 
     fn next(&mut self) -> Option<Self::Item> {
         let arg = self.args.next()?;
-        if arg.starts_with(self.name) {
+        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().map(Ok);
-            } else if suffix.starts_with('=') {
+            } else if let Some(suffix) = suffix.strip_prefix('=') {
                 // This argument is `name=value`; get the value.
-                // Strip leading `=`.
-                return Some(Ok(suffix[1..].to_owned()));
+                return Some(Ok(suffix.to_owned()));
             }
         }
         Some(Err(arg))
@@ -233,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");
@@ -254,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) = {
@@ -284,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))
@@ -310,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;
@@ -328,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) {
@@ -341,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.
@@ -350,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 =
@@ -376,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.
@@ -384,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."
@@ -412,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.
@@ -463,21 +498,22 @@ 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());
     }
 }
 
-/// Detect the target directory by calling `cargo metadata`.
-fn detect_target_dir() -> PathBuf {
-    #[derive(Deserialize)]
-    struct Metadata {
-        target_directory: PathBuf,
-    }
+#[derive(Deserialize)]
+struct Metadata {
+    target_directory: PathBuf,
+    workspace_members: Vec<String>,
+}
+
+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"]);
@@ -487,26 +523,43 @@ struct Metadata {
     for arg in ArgSplitFlagValue::new(
         env::args().skip(3), // skip the program name, "miri" and "run" / "test"
         config_flag,
-    ) {
-        if let Ok(config) = arg {
-            cmd.arg(config_flag).arg(config);
-        }
+    )
+    // Only look at `Ok`
+    .flatten()
+    {
+        cmd.arg(config_flag).arg(arg);
     }
     let mut child = cmd
-        .stdin(Stdio::null())
-        .stdout(Stdio::piped())
+        .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 `cargo metadata` to exit");
+    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)))
-        .target_directory
+    metadata.unwrap_or_else(|e| show_error(format!("invalid `cargo metadata` output: {}", e)))
+}
+
+/// 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) {
@@ -523,19 +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
@@ -545,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();
@@ -572,13 +627,22 @@ fn phase_cargo_miri(mut args: env::Args) {
     // 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) => target_dir = Some(value.into()),
-            Err(arg) => drop(cmd.arg(arg)),
+            Ok(value) => {
+                if target_dir.is_some() {
+                    show_error(format!("`--target-dir` is provided more than once"));
+                }
+                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(detect_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");
@@ -596,6 +660,14 @@ fn phase_cargo_miri(mut args: env::Args) {
         );
     }
     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('-', "_"));
@@ -610,6 +682,8 @@ fn phase_cargo_miri(mut args: env::Args) {
     // 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);
@@ -668,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
@@ -768,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") {
@@ -897,12 +970,10 @@ fn phase_runner(binary: &Path, binary_args: env::Args, phase: RunnerPhase) {
     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 {
@@ -1000,6 +1071,8 @@ fn phase_rustdoc(fst_arg: &str, mut args: env::Args) {
 
     // 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");
@@ -1020,6 +1093,19 @@ 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_CALLED_FROM_XARGO").is_some() {
         phase_rustc(args, RustcPhase::Setup);
@@ -1051,14 +1137,6 @@ fn main() {
         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_rustc(args, RustcPhase::Build),
@@ -1079,8 +1157,9 @@ fn main() {
                 ));
             }
         }
-        _ => 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`"
+            )),
     }
 }