diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 889c584ba7c1..28cab45be405 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,12 @@ jobs: --host ${{ matrix.host_target }} rustup default master + # We need a nightly Cargo to run tests that depend on unstable Cargo features. + - name: Install latest nightly + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + - name: Show Rust version run: | rustup show diff --git a/cargo-miri/bin.rs b/cargo-miri/bin.rs index 7836b26ea5f9..6195a917cb8d 100644 --- a/cargo-miri/bin.rs +++ b/cargo-miri/bin.rs @@ -6,7 +6,7 @@ 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 std::process::{Command, Stdio}; use serde::{Deserialize, Serialize}; @@ -112,40 +112,60 @@ 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 bool>, +/// Yields all values of command line flag `name` as `Ok(arg)`, and all other arguments except +/// the flag as `Err(arg)`. +struct ArgFlagValueWithOtherArgsIter<'a, I> { + args: TakeWhile bool>, name: &'a str, } -impl<'a> ArgFlagValueIter<'a> { - fn new(name: &'a str) -> Self { +impl<'a, I: Iterator> ArgFlagValueWithOtherArgsIter<'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 ArgFlagValueWithOtherArgsIter<'_, I> { + type Item = Result; + + fn next(&mut self) -> Option { + let arg = self.args.next()?; + if arg.starts_with(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('=') { + // This argument is `name=value`; get the value. + // Strip leading `=`. + return Some(Ok(suffix[1..].to_owned())); + } + } + Some(Err(arg)) + } +} + +/// Yields all values of command line flag `name`. +struct ArgFlagValueIter<'a>(ArgFlagValueWithOtherArgsIter<'a, env::Args>); + +impl<'a> ArgFlagValueIter<'a> { + fn new(name: &'a str) -> Self { + Self(ArgFlagValueWithOtherArgsIter::new(env::args(), name)) + } +} + impl Iterator for ArgFlagValueIter<'_> { type Item = String; fn next(&mut self) -> Option { 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()); + if let Ok(value) = self.0.next()? { + return Some(value); } } } @@ -510,8 +530,59 @@ fn phase_cargo_miri(mut args: env::Args) { &host }; - // Forward all further arguments to cargo. - cmd.args(args); + let mut target_dir = None; + + // Forward all arguments before `--` other than `--target-dir` and its value to Cargo. + for arg in ArgFlagValueWithOtherArgsIter::new(&mut args, "--target-dir") { + match arg { + Ok(value) => target_dir = Some(value.into()), + Err(arg) => drop(cmd.arg(arg)), + } + } + + // Detect the target directory if it's not specified via `--target-dir`. + let target_dir = target_dir.get_or_insert_with(|| { + #[derive(Deserialize)] + struct Metadata { + target_directory: PathBuf, + } + 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 by passed by `--config` flags, so forward them to + // `cargo metadata`. + let config_flag = "--config"; + for arg in ArgFlagValueWithOtherArgsIter::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); + } + } + let mut child = cmd + .stdin(Stdio::null()) + .stdout(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 = serde_json::from_reader(child.stdout.take().unwrap()); + let status = child.wait().expect("failed to wait `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 + }); + + // 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 diff --git a/test-cargo-miri/.gitignore b/test-cargo-miri/.gitignore index 56f307a7fb13..af5854e0c3fd 100644 --- a/test-cargo-miri/.gitignore +++ b/test-cargo-miri/.gitignore @@ -1 +1,4 @@ *.real +custom-run +custom-test +config-cli diff --git a/test-cargo-miri/run-test.py b/test-cargo-miri/run-test.py index c850e7d14591..369941787a14 100755 --- a/test-cargo-miri/run-test.py +++ b/test-cargo-miri/run-test.py @@ -101,6 +101,11 @@ def test_cargo_miri_run(): "run.subcrate.stdout.ref", "run.subcrate.stderr.ref", env={'MIRIFLAGS': "-Zmiri-disable-isolation"}, ) + test("`cargo miri run` (custom target dir)", + # Attempt to confuse the argument parser. + cargo_miri("run") + ["--target-dir=custom-run", "--", "--target-dir=target/custom-run"], + "run.args.stdout.ref", "run.custom-target-dir.stderr.ref", + ) def test_cargo_miri_test(): # rustdoc is not run on foreign targets @@ -144,8 +149,18 @@ def test_cargo_miri_test(): cargo_miri("test") + ["-p", "subcrate", "--doc"], "test.stdout-empty.ref", "test.stderr-proc-macro-doctest.ref", ) + test("`cargo miri test` (custom target dir)", + cargo_miri("test") + ["--target-dir=custom-test"], + default_ref, "test.stderr-empty.ref", + ) + del os.environ["CARGO_TARGET_DIR"] # this overrides `build.target-dir` passed by `--config`, so unset it + test("`cargo miri test` (config-cli)", + cargo_miri("test") + ["--config=build.target-dir=\"config-cli\"", "-Zunstable-options"], + default_ref, "test.stderr-empty.ref", + ) os.chdir(os.path.dirname(os.path.realpath(__file__))) +os.environ["CARGO_TARGET_DIR"] = "target" # this affects the location of the target directory that we need to check os.environ["RUST_TEST_NOCAPTURE"] = "0" # this affects test output, so make sure it is not set os.environ["RUST_TEST_THREADS"] = "1" # avoid non-deterministic output due to concurrent test runs @@ -158,6 +173,10 @@ if not 'MIRI_SYSROOT' in os.environ: subprocess.run(cargo_miri("setup"), check=True) test_cargo_miri_run() test_cargo_miri_test() +for target_dir in ["target", "custom-run", "custom-test", "config-cli"]: + if os.listdir(target_dir) != ["miri"]: + fail(f"`{target_dir}` contains unexpected files") + os.access(os.path.join(target_dir, "miri", "debug", "deps"), os.F_OK) print("\nTEST SUCCESSFUL!") sys.exit(0) diff --git a/test-cargo-miri/run.custom-target-dir.stderr.ref b/test-cargo-miri/run.custom-target-dir.stderr.ref new file mode 100644 index 000000000000..4395ff8879b9 --- /dev/null +++ b/test-cargo-miri/run.custom-target-dir.stderr.ref @@ -0,0 +1,2 @@ +main +--target-dir=target/custom-run