-#![feature(inner_deref)]
-
use std::env;
use std::ffi::OsString;
use std::fs::{self, File};
-use std::io::{self, BufRead, Write};
+use std::iter::TakeWhile;
+use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
use std::ops::Not;
use std::path::{Path, PathBuf};
use std::process::Command;
+use serde::{Deserialize, Serialize};
+
use rustc_version::VersionMeta;
-const XARGO_MIN_VERSION: (u32, u32, u32) = (0, 3, 20);
+const XARGO_MIN_VERSION: (u32, u32, u32) = (0, 3, 22);
-const CARGO_MIRI_HELP: &str = r#"Interprets bin crates and tests in Miri
+const CARGO_MIRI_HELP: &str = r#"Runs binary crates and tests in Miri
Usage:
- cargo miri [subcommand] [<cargo options>...] [--] [<miri options>...] [--] [<program/test suite options>...]
+ cargo miri [subcommand] [<cargo options>...] [--] [<program/test suite options>...]
Subcommands:
- run Run binaries (default)
+ run Run binaries
test Run tests
setup Only perform automatic setup, but without asking questions (for getting a proper libstd)
-Common options:
- -h, --help Print this message
- --features Features to compile for the package
- -V, --version Print version info and exit
-
-Other [options] are the same as `cargo check`. Everything after the first "--" is
-passed verbatim to Miri, which will pass everything after the second "--" verbatim
-to the interpreted program.
+The cargo options are exactly the same as for `cargo run` and `cargo test`, respectively.
Examples:
- cargo miri run -- -Zmiri-disable-stacked-borrows
- cargo miri test -- -- test-suite-filter
+ cargo miri run
+ cargo miri test -- test-suite-filter
"#;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
Setup,
}
+/// The information to run a crate with the given environment.
+#[derive(Serialize, Deserialize)]
+struct CrateRunEnv {
+ /// The command-line arguments.
+ args: Vec<String>,
+ /// The environment.
+ env: Vec<(OsString, OsString)>,
+ /// The current working directory.
+ current_dir: OsString,
+ /// The contents passed via standard input.
+ stdin: Vec<u8>,
+}
+
+impl CrateRunEnv {
+ /// Gather all the information we need.
+ 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 capture_stdin {
+ std::io::stdin().lock().read_to_end(&mut stdin).expect("cannot read 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())));
+ let file = BufWriter::new(file);
+ serde_json::ser::to_writer(file, self)
+ .unwrap_or_else(|_| show_error(format!("cannot write to `{}`", filename.display())));
+ }
+}
+
fn show_help() {
println!("{}", CARGO_MIRI_HELP);
}
println!(
"miri {} ({} {})",
env!("CARGO_PKG_VERSION"),
- env!("VERGEN_SHA_SHORT"),
- env!("VERGEN_COMMIT_DATE")
+ env!("VERGEN_GIT_SHA_SHORT"),
+ env!("VERGEN_GIT_COMMIT_DATE")
);
}
args.any(|val| val == name)
}
-/// Gets the value of a `--flag`.
-fn get_arg_flag_value(name: &str) -> Option<String> {
- // Stop searching at `--`.
- let mut args = std::env::args().take_while(|val| val != "--");
- loop {
- let arg = match args.next() {
- Some(arg) => arg,
- None => return None,
- };
- if !arg.starts_with(name) {
- continue;
+/// Yields all values of command line flag `name`.
+struct ArgFlagValueIter<'a> {
+ args: TakeWhile<env::Args, fn(&String) -> bool>,
+ name: &'a str,
+}
+
+impl<'a> ArgFlagValueIter<'a> {
+ fn new(name: &'a str) -> Self {
+ Self {
+ // Stop searching at `--`.
+ args: env::args().take_while(|val| val != "--"),
+ name,
}
- // Strip leading `name`.
- let suffix = &arg[name.len()..];
- if suffix.is_empty() {
- // This argument is exactly `name`; the next one is the value.
- return args.next();
- } else if suffix.starts_with('=') {
- // This argument is `name=value`; get the value.
- // Strip leading `=`.
- return Some(suffix[1..].to_owned());
+ }
+}
+
+impl Iterator for ArgFlagValueIter<'_> {
+ type Item = String;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ let arg = self.args.next()?;
+ if !arg.starts_with(self.name) {
+ continue;
+ }
+ // 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('=') {
+ // This argument is `name=value`; get the value.
+ // Strip leading `=`.
+ return Some(suffix[1..].to_owned());
+ }
}
}
}
+/// Gets the value of a `--flag`.
+fn get_arg_flag_value(name: &str) -> Option<String> {
+ ArgFlagValueIter::new(name).next()
+}
+
+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);
+ }
+}
+
+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") {
Command::new(env::var_os("XARGO_CHECK").unwrap_or_else(|| OsString::from("xargo-check")))
}
-fn list_targets() -> impl Iterator<Item = cargo_metadata::Target> {
- // We need to get the manifest, and then the metadata, to enumerate targets.
- let manifest_path =
- get_arg_flag_value("--manifest-path").map(|m| Path::new(&m).canonicalize().unwrap());
-
- let mut cmd = cargo_metadata::MetadataCommand::new();
- if let Some(manifest_path) = &manifest_path {
- cmd.manifest_path(manifest_path);
+/// Execute the command. If it fails, fail this process with the same exit code.
+/// Otherwise, continue.
+fn exec(mut cmd: Command) {
+ let exit_status = cmd.status().expect("failed to run command");
+ if exit_status.success().not() {
+ std::process::exit(exit_status.code().unwrap_or(-1))
}
- let mut metadata = if let Ok(metadata) = cmd.exec() {
- metadata
- } else {
- show_error(format!("Could not obtain Cargo metadata; likely an ill-formed manifest"));
- };
-
- let current_dir = std::env::current_dir();
-
- let package_index = metadata
- .packages
- .iter()
- .position(|package| {
- let package_manifest_path = Path::new(&package.manifest_path);
- if let Some(manifest_path) = &manifest_path {
- package_manifest_path == manifest_path
- } else {
- let current_dir = current_dir.as_ref().expect("could not read current directory");
- let package_manifest_directory = package_manifest_path
- .parent()
- .expect("could not find parent directory of package manifest");
- package_manifest_directory == current_dir
- }
- })
- .unwrap_or_else(|| {
- show_error(format!(
- "this seems to be a workspace, which is not supported by `cargo miri`.\n\
- Try to `cd` into the crate you want to test, and re-run `cargo miri` there."
- ))
- });
- let package = metadata.packages.remove(package_index);
+}
- // Finally we got the list of targets to build
- package.targets.into_iter()
+/// Execute the command and pipe `input` into its stdin.
+/// 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());
+ let mut child = cmd.spawn().expect("failed to spawn process");
+ {
+ let stdin = child.stdin.as_mut().expect("failed to open stdin");
+ stdin.write_all(input).expect("failed to write out test source");
+ }
+ let exit_status = child.wait().expect("failed to run command");
+ if exit_status.success().not() {
+ std::process::exit(exit_status.code().unwrap_or(-1))
+ }
}
fn xargo_version() -> Option<(u32, u32, u32)> {
fn ask_to_run(mut cmd: Command, ask: bool, text: &str) {
// Disable interactive prompts in CI (GitHub Actions, Travis, AppVeyor, etc).
- if ask && env::var_os("CI").is_none() {
+ // Azure doesn't set `CI` though (nothing to see here, just Microsoft being Microsoft),
+ // so we also check their `TF_BUILD`.
+ let is_ci = env::var_os("CI").is_some() || env::var_os("TF_BUILD").is_some();
+ if ask && !is_ci {
let mut buf = String::new();
print!("I will run `{:?}` to {}. Proceed? [Y/n] ", cmd, text);
io::stdout().flush().unwrap();
match buf.trim().to_lowercase().as_ref() {
// Proceed.
"" | "y" | "yes" => {}
- "n" | "no" => show_error(format!("Aborting as per your request")),
- a => show_error(format!("I do not understand `{}`", a)),
+ "n" | "no" => show_error(format!("aborting as per your request")),
+ a => show_error(format!("invalid answer `{}`", a)),
};
} else {
println!("Running `{:?}` to {}.", cmd, text);
}
if cmd.status().expect(&format!("failed to execute {:?}", cmd)).success().not() {
- show_error(format!("Failed to {}", text));
+ show_error(format!("failed to {}", text));
}
}
if xargo_version().map_or(true, |v| v < XARGO_MIN_VERSION) {
if std::env::var_os("XARGO_CHECK").is_some() {
// The user manually gave us a xargo binary; don't do anything automatically.
- show_error(format!("Your xargo is too old; please upgrade to the latest version"))
+ show_error(format!("xargo is too old; please upgrade to the latest version"))
}
let mut cmd = cargo();
- cmd.args(&["install", "xargo", "-f"]);
+ cmd.args(&["install", "xargo"]);
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") {
- Some(val) => {
- let path = PathBuf::from(val);
- // Make path absolute, but not via `canonicalize` (which does not work very well on Windows).
- env::current_dir().unwrap().join(path)
+ Some(path) => {
+ let path = PathBuf::from(path);
+ // Make path absolute if possible.
+ path.canonicalize().unwrap_or(path)
}
None => {
// Check for `rust-src` rustup component.
.stdout;
let sysroot = std::str::from_utf8(&sysroot).unwrap();
let sysroot = Path::new(sysroot.trim_end_matches('\n'));
- // Check for `$SYSROOT/lib/rustlib/src/rust/src`; test if that contains `libstd/lib.rs`.
+ // Check for `$SYSROOT/lib/rustlib/src/rust/library`; test if that contains `std/Cargo.toml`.
let rustup_src =
- sysroot.join("lib").join("rustlib").join("src").join("rust").join("src");
- if !rustup_src.join("libstd").join("lib.rs").exists() {
+ sysroot.join("lib").join("rustlib").join("src").join("rust").join("library");
+ if !rustup_src.join("std").join("Cargo.toml").exists() {
// Ask the user to install the `rust-src` component, and use that.
let mut cmd = Command::new("rustup");
cmd.args(&["component", "add", "rust-src"]);
}
};
if !rust_src.exists() {
- show_error(format!("Given Rust source directory `{}` does not exist.", rust_src.display()));
+ show_error(format!("given Rust source directory `{}` does not exist.", rust_src.display()));
}
// Next, we need our own libstd. Prepare a xargo project for that purpose.
br#"
[dependencies.std]
default_features = false
-# We need the `panic_unwind` feature because we use the `unwind` panic strategy.
-# Using `abort` works for libstd, but then libtest will not compile.
-features = ["panic_unwind"]
+# We support unwinding, so enable that panic runtime.
+features = ["panic_unwind", "backtrace"]
[dependencies.test]
"#,
let target = target.as_ref().unwrap_or(&host);
// Now invoke xargo.
let mut command = xargo_check();
- command.arg("build").arg("-q");
+ command.arg("check").arg("-q");
command.arg("--target").arg(target);
command.current_dir(&dir);
command.env("XARGO_HOME", &dir);
// because we still need bootstrap to distinguish between host and target crates.
// In that case we overwrite `RUSTC_REAL` instead which determines the rustc used
// 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_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", find_miri());
+ command.env("RUSTC_REAL", &cargo_miri_path);
} else {
- command.env("RUSTC", find_miri());
+ 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`
// to the sysroot either.
command.env_remove("RUSTC_WRAPPER");
command.env_remove("RUSTFLAGS");
+ // Disable debug assertions in the standard library -- Miri is already slow enough.
+ // But keep the overflow checks, they are cheap.
+ command.env("RUSTFLAGS", "-Cdebug-assertions=off -Coverflow-checks=on");
// Finally run it!
if command.status().expect("failed to run xargo").success().not() {
- show_error(format!("Failed to run xargo"));
+ show_error(format!("failed to run xargo"));
}
// That should be it! But we need to figure out where xargo built stuff.
}
}
-fn in_cargo_miri() {
- let (subcommand, skip) = match std::env::args().nth(2).as_deref() {
- Some("test") => (MiriCommand::Test, 3),
- Some("run") => (MiriCommand::Run, 3),
- Some("setup") => (MiriCommand::Setup, 3),
- // Default command, if there is an option or nothing.
- Some(s) if s.starts_with("-") => (MiriCommand::Run, 2),
- None => (MiriCommand::Run, 2),
+fn phase_cargo_miri(mut args: env::Args) {
+ // Check for version and help flags even when invoked as `cargo-miri`.
+ if has_arg_flag("--help") || has_arg_flag("-h") {
+ show_help();
+ return;
+ }
+ if has_arg_flag("--version") || has_arg_flag("-V") {
+ show_version();
+ return;
+ }
+
+ // 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,
// Invalid command.
- Some(s) => show_error(format!("Unknown command `{}`", s)),
+ _ => show_error(format!("`cargo miri` supports the following subcommands: `run`, `test`, and `setup`.")),
};
let verbose = has_arg_flag("-v");
// We always setup.
setup(subcommand);
- if subcommand == MiriCommand::Setup {
- // Stop here.
- return;
- }
- // Now run the command.
- for target in list_targets() {
- let mut args = std::env::args().skip(skip);
- let kind = target
- .kind
- .get(0)
- .expect("badly formatted cargo metadata: target::kind is an empty array");
- // Now we run `cargo check $FLAGS $ARGS`, giving the user the
- // change to add additional arguments. `FLAGS` is set to identify
- // this target. The user gets to control what gets actually passed to Miri.
- let mut cmd = cargo();
- cmd.arg("check");
- match (subcommand, kind.as_str()) {
- (MiriCommand::Run, "bin") => {
- // FIXME: we just run all the binaries here.
- // We should instead support `cargo miri --bin foo`.
- cmd.arg("--bin").arg(target.name);
- }
- (MiriCommand::Test, "test") => {
- cmd.arg("--test").arg(target.name);
- }
- (MiriCommand::Test, "lib") => {
- // There can be only one lib.
- cmd.arg("--lib").arg("--profile").arg("test");
- }
- (MiriCommand::Test, "bin") => {
- cmd.arg("--bin").arg(target.name).arg("--profile").arg("test");
- }
- // The remaining targets we do not even want to build.
- _ => continue,
- }
- // Forward user-defined `cargo` args until first `--`.
- while let Some(arg) = args.next() {
- if arg == "--" {
- break;
- }
- cmd.arg(arg);
- }
- // We want to always run `cargo` with `--target`. This later helps us detect
- // which crates are proc-macro/build-script (host crates) and which crates are
- // needed for the program itself.
- if get_arg_flag_value("--target").is_none() {
- // When no `--target` is given, default to the host.
- cmd.arg("--target");
- cmd.arg(version_info().host);
- }
+ // 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
+ // requires some extra work to make the build check-only (see all the `--emit` hacks below).
+ // <https://github.com/rust-lang/miri/pull/1540#issuecomment-693553191> describes an alternative
+ // approach that uses `cargo check`, making that part easier but target and binary handling
+ // 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::Setup => return, // `cargo miri setup` stops here.
+ };
+ let mut cmd = cargo();
+ cmd.arg(cargo_cmd);
- // Serialize the remaining args into a special environemt variable.
- // This will be read by `inside_cargo_rustc` when we go to invoke
- // our actual target crate (the binary or the test we are running).
- // Since we're using "cargo check", we have no other way of passing
- // these arguments.
- let args_vec: Vec<String> = args.collect();
- cmd.env("MIRI_ARGS", serde_json::to_string(&args_vec).expect("failed to serialize 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 existing `RUSTC_WRAPPER` environment variable, Miri does not support wrapping.");
- }
- let path = std::env::current_exe().expect("current executable path invalid");
- cmd.env("RUSTC_WRAPPER", path);
- if verbose {
- cmd.env("MIRI_VERBOSE", ""); // this makes `inside_cargo_rustc` verbose.
- eprintln!("+ {:?}", cmd);
- }
+ // Make sure we know the build target, and cargo does, too.
+ // This is needed to make the `CARGO_TARGET_*_RUNNER` env var do something,
+ // and it later helps us detect which crates are proc-macro/build-script
+ // (host crates) and which crates are needed for the program itself.
+ let host = version_info().host;
+ let target = get_arg_flag_value("--target");
+ let target = if let Some(ref target) = target {
+ target
+ } else {
+ // No target given. Pick default and tell cargo about it.
+ cmd.arg("--target");
+ cmd.arg(&host);
+ &host
+ };
- let exit_status =
- cmd.spawn().expect("could not run cargo").wait().expect("failed to wait for cargo?");
+ // Forward all further arguments to cargo.
+ cmd.args(args);
- if !exit_status.success() {
- std::process::exit(exit_status.code().unwrap_or(-1))
+ // 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.");
+ }
+ cmd.env("RUSTC_WRAPPER", &cargo_miri_path);
+
+ 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.
+ cmd.env(&target_runner_env_name, &cargo_miri_path);
+ // Unit tests of `proc-macro` crates are run on the host, so we set the host runner to
+ // us in order to skip them.
+ cmd.env(&host_runner_env_name, &cargo_miri_path);
+
+ // Set rustdoc to us as well, so we can run doctests.
+ cmd.env("RUSTDOC", &cargo_miri_path);
+
+ // Run cargo.
+ if verbose {
+ eprintln!("[cargo-miri miri] RUSTC_WRAPPER={:?}", cargo_miri_path);
+ eprintln!("[cargo-miri miri] {}={:?}", target_runner_env_name, cargo_miri_path);
+ if *target != host {
+ eprintln!("[cargo-miri miri] {}={:?}", host_runner_env_name, cargo_miri_path);
}
+ eprintln!("[cargo-miri miri] RUSTDOC={:?}", cargo_miri_path);
+ eprintln!("[cargo-miri miri] {:?}", cmd);
+ cmd.env("MIRI_VERBOSE", ""); // This makes the other phases verbose.
}
+ exec(cmd)
+}
+
+#[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 inside_cargo_rustc() {
+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
/// Cargo does not give us this information directly, so we need to check
/// various command-line flags.
fn is_runnable_crate() -> bool {
- let is_bin = get_arg_flag_value("--crate-type").as_deref() == Some("bin");
+ let is_bin = get_arg_flag_value("--crate-type").as_deref().unwrap_or("bin") == "bin";
let is_test = has_arg_flag("--test");
is_bin || is_test
}
+ fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
+ if let Some(out_dir) = get_arg_flag_value("--out-dir") {
+ let mut path = PathBuf::from(out_dir);
+ path.push(format!(
+ "{}{}{}{}",
+ prefix,
+ 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()),
+ suffix,
+ ));
+ path
+ } else {
+ let out_file = get_arg_flag_value("-o").unwrap();
+ PathBuf::from(out_file)
+ }
+ }
+
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() || has_arg_flag("-vV"); // whether this is cargo/xargo invoking rustc to get some infos
- let mut cmd = miri();
- // Forward arguments.
- cmd.args(std::env::args().skip(2)); // skip `cargo-miri rustc`
-
- // We make sure to only specify our custom Xargo sysroot for target crates - that is,
- // crates which are needed for interpretation by Miri. proc-macros and build scripts
- // should use the default sysroot.
- if target_crate {
- let sysroot =
- env::var_os("MIRI_SYSROOT").expect("The wrapper should have set MIRI_SYSROOT");
- cmd.arg("--sysroot");
- cmd.arg(sysroot);
- }
-
- // If this is a runnable target crate, we want Miri to start interpretation;
- // otherwise we want Miri to behave like rustc and build the crate as usual.
- if target_crate && is_runnable_crate() {
+ 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.
+ let dep_info_name = out_filename("", ".d");
+ if verbose {
+ eprintln!("[cargo-miri rustc] writing stub dep-info to `{}`", dep_info_name.display());
+ }
+ File::create(dep_info_name).expect("failed to create fake .d file");
+
+ let filename = out_filename("", "");
+ if verbose {
+ eprintln!("[cargo-miri rustc] writing run info to `{}`", filename.display());
+ }
+ info.store(&filename);
+ // For Windows, do the same thing again with `.exe` appended to the filename.
+ // (Need to do this here as cargo moves that "binary" to a different place before running it.)
+ info.store(&out_filename("", ".exe"));
+ };
+
+ 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.
- // (Testing `target_crate` is needed to exclude build scripts.)
- // We deserialize the arguments that are meant for Miri from the special environment
- // variable "MIRI_ARGS", and feed them to the 'miri' binary.
- //
- // `env::var` is okay here, well-formed JSON is always UTF-8.
- let magic = std::env::var("MIRI_ARGS").expect("missing MIRI_ARGS");
- let miri_args: Vec<String> =
- serde_json::from_str(&magic).expect("failed to deserialize MIRI_ARGS");
- cmd.args(miri_args);
+ // 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 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 inside_rustdoc {
+ let mut cmd = miri();
+
+ // 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=")) {
+ // For `no_run` tests, rustdoc passes a `--emit` flag; make sure it has the right shape.
+ assert_eq!(env.args[i], "--emit=metadata");
+ } else {
+ // 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", "target");
+
+ if verbose {
+ 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;
+ }
+
+ if runnable_crate && ArgFlagValueIter::new("--extern").any(|krate| krate == "proc_macro") {
+ // 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);
+ return;
+ }
+
+ let mut cmd = miri();
+ let mut emit_link_hack = false;
+ // Arguments are treated very differently depending on whether this crate is
+ // for interpretation by Miri, or for use by a build script / proc macro.
+ if !print && target_crate {
+ // 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) {
+ // 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 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") {
+ emit_link_hack = true;
+ val.remove(i);
+ if !val.iter().any(|&s| s == "metadata") {
+ val.push("metadata");
+ }
+ }
+ cmd.arg(format!("{}={}", emit_flag, val.join(",")));
+ } else if arg == "--extern" {
+ // Patch `--extern` filenames, since Cargo sometimes passes stub `.rlib` files:
+ // https://github.com/rust-lang/miri/issues/1705
+ forward_patched_extern_arg(&mut args, &mut cmd);
+ } else {
+ cmd.arg(arg);
+ }
+ }
+
+ // 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 {
- // We want to compile, not interpret.
- cmd.env("MIRI_BE_RUSTC", "1");
+ // For host crates or when we are printing, just forward everything.
+ cmd.args(args);
+ }
+
+ // 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.
+ // 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 {
+ eprintln!("[cargo-miri rustc] {:?}", cmd);
+ }
+ exec(cmd);
+
+ // Create a stub .rlib file if "link" was requested by cargo.
+ // This is necessary to prevent cargo from doing rebuilds all the time.
+ if emit_link_hack {
+ // Some platforms prepend "lib", some do not... let's just create both files.
+ File::create(out_filename("lib", ".rlib")).expect("failed to create fake .rlib file");
+ File::create(out_filename("", ".rlib")).expect("failed to create fake .rlib file");
+ // Just in case this is a cdylib or staticlib, also create those fake files.
+ File::create(out_filename("lib", ".so")).expect("failed to create fake .so file");
+ File::create(out_filename("lib", ".a")).expect("failed to create fake .a file");
+ File::create(out_filename("lib", ".dylib")).expect("failed to create fake .dylib file");
+ File::create(out_filename("", ".dll")).expect("failed to create fake .dll file");
+ File::create(out_filename("", ".lib")).expect("failed to create fake .lib file");
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+enum RunnerPhase {
+ /// `cargo` is running a binary
+ Cargo,
+ /// `rustdoc` is running a binary
+ Rustdoc,
+}
+
+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 = match info {
+ CrateRunInfo::RunWith(info) => info,
+ CrateRunInfo::SkipProcMacroTest => {
+ eprintln!("Running unit tests of `proc-macro` crates is not currently supported by Miri.");
+ return;
+ }
};
+ let mut cmd = miri();
+
+ // Set missing env vars. We prefer build-time env vars over run-time ones; see
+ // <https://github.com/rust-lang/miri/issues/1661> for the kind of issue that fixes.
+ for (name, val) in info.env {
+ 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);
+ }
+ }
+ }
+ cmd.env(name, val);
+ }
+
+ // Forward rustc arguments.
+ // We need to patch "--extern" filenames because we forced a check-only
+ // build without cargo knowing about that: replace `.rlib` suffix by
+ // `.rmeta`.
+ // We also need to remove `--error-format` as cargo specifies that to be JSON,
+ // but when we run here, cargo does not interpret the JSON any more. `--json`
+ // then also nees to be dropped.
+ let mut args = info.args.into_iter();
+ let error_format_flag = "--error-format";
+ let json_flag = "--json";
+ 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()..];
+ assert!(suffix.starts_with('='));
+ // Drop this argument.
+ } else if arg.starts_with(json_flag) {
+ let suffix = &arg[json_flag.len()..];
+ assert!(suffix.starts_with('='));
+ // Drop this argument.
+ } else {
+ cmd.arg(arg);
+ }
+ }
+ // 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);
+ cmd.args(args);
+ }
+
+ // Then pass binary arguments.
+ cmd.arg("--");
+ cmd.args(binary_args);
+
+ // Make sure we use the build-time working directory for interpreting Miri/rustc arguments.
+ // But then we need to switch to the run-time one, which we instruct Miri do do by setting `MIRI_CWD`.
+ cmd.current_dir(info.current_dir);
+ cmd.env("MIRI_CWD", env::current_dir().unwrap());
+
// Run it.
if verbose {
- eprintln!("+ {:?}", cmd);
+ eprintln!("[cargo-miri runner] {:?}", cmd);
+ }
+
+ match phase {
+ RunnerPhase::Rustdoc => {
+ exec_with_pipe(cmd, &info.stdin)
+ }
+ RunnerPhase::Cargo => {
+ exec(cmd)
+ }
+ }
+}
+
+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("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.
+ // This is fine for now, because cargo will never pass --extern arguments in the first position,
+ // but we should defensively assert that this will work.
+ 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 {
+ // Patch --extern arguments to use *.rmeta files, since phase_cargo_rustc only creates stub *.rlib files.
+ forward_patched_extern_arg(&mut args, &mut cmd);
+ } else if arg == runtool_flag {
+ // An existing --runtool flag indicates cargo is running in cross-target mode, which we don't support.
+ // Note that this is only passed when cargo is run with the unstable -Zdoctest-xcompile flag;
+ // otherwise, we won't be called as rustdoc at all.
+ crossmode = true;
+ break;
+ } else {
+ cmd.arg(arg);
+ }
}
- match cmd.status() {
- Ok(exit) =>
- if !exit.success() {
- std::process::exit(exit.code().unwrap_or(42));
- },
- Err(e) => panic!("error running {:?}:\n{:?}", cmd, e),
+
+ if crossmode {
+ show_error(format!("cross-interpreting doc-tests is not currently supported by Miri."));
+ }
+
+ // For each doc-test, 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 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);
}
+
+ exec(cmd)
}
fn main() {
- // Check for version and help flags even when invoked as `cargo-miri`.
- if has_arg_flag("--help") || has_arg_flag("-h") {
- show_help();
+ // Rustc does not support non-UTF-8 arguments so we make no attempt either.
+ // (We do support non-UTF-8 environment variables though.)
+ let mut args = std::env::args();
+ // Skip binary name.
+ args.next().unwrap();
+
+ // Dispatch running as part of sysroot compilation.
+ if env::var_os("MIRI_CALLED_FROM_XARGO").is_some() {
+ phase_rustc(args, RustcPhase::Setup);
return;
}
- if has_arg_flag("--version") || has_arg_flag("-V") {
- show_version();
+
+ // 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
+ // the test-builder unconditionally, we can just check the number of remaining arguments:
+ if args.len() == 1 {
+ let arg = args.next().unwrap();
+ let binary = Path::new(&arg);
+ if binary.exists() {
+ 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));
+ }
+ } else {
+ phase_rustc(args, RustcPhase::Rustdoc);
+ }
+
return;
}
- if let Some("miri") = std::env::args().nth(1).as_deref() {
- // This arm is for when `cargo miri` is called. We call `cargo check` for each applicable target,
- // but with the `RUSTC` env var set to the `cargo-miri` binary so that we come back in the other branch,
- // and dispatch the invocations to `rustc` and `miri`, respectively.
- in_cargo_miri();
- } else if let Some("rustc") = std::env::args().nth(1).as_deref() {
- // This arm is executed when `cargo-miri` runs `cargo check` with the `RUSTC_WRAPPER` env var set to itself:
- // dependencies get dispatched to `rustc`, the final test/binary to `miri`.
- inside_cargo_rustc();
- } else {
- show_error(format!(
- "`cargo-miri` must be called with either `miri` or `rustc` as first argument."
- ))
+ // 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),
+ 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);
+ // as rustdoc, the first argument is a flag (`--something`).
+ let binary = Path::new(arg);
+ if binary.exists() {
+ assert!(!arg.starts_with("--")); // not a flag
+ phase_runner(binary, args, RunnerPhase::Cargo);
+ } else if arg.starts_with("--") {
+ 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 without first argument; please only invoke this binary through `cargo miri`")),
}
}