Skip to content

Commit 685ad70

Browse files
committed
Auto merge of #1757 - RalfJung:rustdoc, r=RalfJung
add rustdoc support `@teryror` did all the work in #1671; I just finished things up and fixed conflicts. Also thanks to `@hyd-dev` for preemptively fixing a sysroot issue that would have taken me some time to diagnose. Fixes #584
2 parents 28f813f + 2f6dff6 commit 685ad70

9 files changed

+252
-45
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ different Miri binaries, and as such worth documenting:
290290
that the compiled `rlib`s are compatible with Miri.
291291
When set while running `cargo-miri`, it indicates that we are part of a sysroot
292292
build (for which some crates need special treatment).
293+
* `MIRI_CALLED_FROM_RUSTDOC` when set to any value tells `cargo-miri` that it is
294+
running as a child process of `rustdoc`, which invokes it twice for each doc-test
295+
and requires special treatment, most notably a check-only build before interpretation.
296+
This is set by `cargo-miri` itself when running as a `rustdoc`-wrapper.
293297
* `MIRI_CWD` when set to any value tells the Miri driver to change to the given
294298
directory after loading all the source files, but before commencing
295299
interpretation. This is useful if the interpreted program wants a different

cargo-miri/bin.rs

Lines changed: 193 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::env;
22
use std::ffi::OsString;
33
use std::fs::{self, File};
4-
use std::io::{self, BufRead, BufReader, BufWriter, Write};
54
use std::iter::TakeWhile;
5+
use std::io::{self, BufRead, BufReader, BufWriter, Read, Write};
66
use std::ops::Not;
77
use std::path::{Path, PathBuf};
88
use std::process::Command;
@@ -46,6 +46,24 @@ struct CrateRunEnv {
4646
env: Vec<(OsString, OsString)>,
4747
/// The current working directory.
4848
current_dir: OsString,
49+
/// The contents passed via standard input.
50+
stdin: Vec<u8>,
51+
}
52+
53+
impl CrateRunEnv {
54+
/// Gather all the information we need.
55+
fn collect(args: env::Args, capture_stdin: bool) -> Self {
56+
let args = args.collect();
57+
let env = env::vars_os().collect();
58+
let current_dir = env::current_dir().unwrap().into_os_string();
59+
60+
let mut stdin = Vec::new();
61+
if capture_stdin {
62+
std::io::stdin().lock().read_to_end(&mut stdin).expect("cannot read stdin");
63+
}
64+
65+
CrateRunEnv { args, env, current_dir, stdin }
66+
}
4967
}
5068

5169
/// The information Miri needs to run a crate. Stored as JSON when the crate is "compiled".
@@ -58,14 +76,6 @@ enum CrateRunInfo {
5876
}
5977

6078
impl CrateRunInfo {
61-
/// Gather all the information we need.
62-
fn collect(args: env::Args) -> Self {
63-
let args = args.collect();
64-
let env = env::vars_os().collect();
65-
let current_dir = env::current_dir().unwrap().into_os_string();
66-
Self::RunWith(CrateRunEnv { args, env, current_dir })
67-
}
68-
6979
fn store(&self, filename: &Path) {
7080
let file = File::create(filename)
7181
.unwrap_or_else(|_| show_error(format!("cannot create `{}`", filename.display())));
@@ -155,6 +165,13 @@ fn forward_patched_extern_arg(args: &mut impl Iterator<Item = String>, cmd: &mut
155165
}
156166
}
157167

168+
fn forward_miri_sysroot(cmd: &mut Command) {
169+
let sysroot =
170+
env::var_os("MIRI_SYSROOT").expect("the wrapper should have set MIRI_SYSROOT");
171+
cmd.arg("--sysroot");
172+
cmd.arg(sysroot);
173+
}
174+
158175
/// Returns the path to the `miri` binary
159176
fn find_miri() -> PathBuf {
160177
if let Some(path) = env::var_os("MIRI") {
@@ -190,6 +207,22 @@ fn exec(mut cmd: Command) {
190207
}
191208
}
192209

210+
/// Execute the command and pipe `input` into its stdin.
211+
/// If it fails, fail this process with the same exit code.
212+
/// Otherwise, continue.
213+
fn exec_with_pipe(mut cmd: Command, input: &[u8]) {
214+
cmd.stdin(std::process::Stdio::piped());
215+
let mut child = cmd.spawn().expect("failed to spawn process");
216+
{
217+
let stdin = child.stdin.as_mut().expect("failed to open stdin");
218+
stdin.write_all(input).expect("failed to write out test source");
219+
}
220+
let exit_status = child.wait().expect("failed to run command");
221+
if exit_status.success().not() {
222+
std::process::exit(exit_status.code().unwrap_or(-1))
223+
}
224+
}
225+
193226
fn xargo_version() -> Option<(u32, u32, u32)> {
194227
let out = xargo_check().arg("--version").output().ok()?;
195228
if !out.status.success() {
@@ -548,7 +581,7 @@ fn phase_cargo_miri(mut args: env::Args) {
548581
// us in order to skip them.
549582
cmd.env(&host_runner_env_name, &cargo_miri_path);
550583

551-
// Set rustdoc to us as well, so we can make it do nothing (see issue #584).
584+
// Set rustdoc to us as well, so we can run doctests.
552585
cmd.env("RUSTDOC", &cargo_miri_path);
553586

554587
// Run cargo.
@@ -591,17 +624,22 @@ fn phase_cargo_rustc(mut args: env::Args) {
591624
}
592625

593626
fn out_filename(prefix: &str, suffix: &str) -> PathBuf {
594-
let mut path = PathBuf::from(get_arg_flag_value("--out-dir").unwrap());
595-
path.push(format!(
596-
"{}{}{}{}",
597-
prefix,
598-
get_arg_flag_value("--crate-name").unwrap(),
599-
// This is technically a `-C` flag but the prefix seems unique enough...
600-
// (and cargo passes this before the filename so it should be unique)
601-
get_arg_flag_value("extra-filename").unwrap_or(String::new()),
602-
suffix,
603-
));
604-
path
627+
if let Some(out_dir) = get_arg_flag_value("--out-dir") {
628+
let mut path = PathBuf::from(out_dir);
629+
path.push(format!(
630+
"{}{}{}{}",
631+
prefix,
632+
get_arg_flag_value("--crate-name").unwrap(),
633+
// This is technically a `-C` flag but the prefix seems unique enough...
634+
// (and cargo passes this before the filename so it should be unique)
635+
get_arg_flag_value("extra-filename").unwrap_or(String::new()),
636+
suffix,
637+
));
638+
path
639+
} else {
640+
let out_file = get_arg_flag_value("-o").unwrap();
641+
PathBuf::from(out_file)
642+
}
605643
}
606644

607645
let verbose = std::env::var_os("MIRI_VERBOSE").is_some();
@@ -631,12 +669,43 @@ fn phase_cargo_rustc(mut args: env::Args) {
631669
let runnable_crate = !print && is_runnable_crate();
632670

633671
if runnable_crate && target_crate {
672+
let inside_rustdoc = env::var_os("MIRI_CALLED_FROM_RUSTDOC").is_some();
634673
// This is the binary or test crate that we want to interpret under Miri.
635674
// But we cannot run it here, as cargo invoked us as a compiler -- our stdin and stdout are not
636675
// like we want them.
637676
// Instead of compiling, we write JSON into the output file with all the relevant command-line flags
638677
// and environment variables; this is used when cargo calls us again in the CARGO_TARGET_RUNNER phase.
639-
store_json(CrateRunInfo::collect(args));
678+
let env = CrateRunEnv::collect(args, inside_rustdoc);
679+
680+
// Rustdoc expects us to exit with an error code if the test is marked as `compile_fail`,
681+
// just creating the JSON file is not enough: we need to detect syntax errors,
682+
// so we need to run Miri with `MIRI_BE_RUSTC` for a check-only build.
683+
if inside_rustdoc {
684+
let mut cmd = miri();
685+
686+
// Ensure --emit argument for a check-only build is present.
687+
// We cannot use the usual helpers since we need to check specifically in `env.args`.
688+
if let Some(i) = env.args.iter().position(|arg| arg.starts_with("--emit=")) {
689+
// For `no_run` tests, rustdoc passes a `--emit` flag; make sure it has the right shape.
690+
assert_eq!(env.args[i], "--emit=metadata");
691+
} else {
692+
// For all other kinds of tests, we can just add our flag.
693+
cmd.arg("--emit=metadata");
694+
}
695+
696+
cmd.args(&env.args);
697+
cmd.env("MIRI_BE_RUSTC", "1");
698+
699+
if verbose {
700+
eprintln!("[cargo-miri rustc] captured input:\n{}", std::str::from_utf8(&env.stdin).unwrap());
701+
eprintln!("[cargo-miri rustc] {:?}", cmd);
702+
}
703+
704+
exec_with_pipe(cmd, &env.stdin);
705+
}
706+
707+
store_json(CrateRunInfo::RunWith(env));
708+
640709
return;
641710
}
642711

@@ -681,10 +750,7 @@ fn phase_cargo_rustc(mut args: env::Args) {
681750
}
682751

683752
// Use our custom sysroot.
684-
let sysroot =
685-
env::var_os("MIRI_SYSROOT").expect("the wrapper should have set MIRI_SYSROOT");
686-
cmd.arg("--sysroot");
687-
cmd.arg(sysroot);
753+
forward_miri_sysroot(&mut cmd);
688754
} else {
689755
// For host crates or when we are printing, just forward everything.
690756
cmd.args(args);
@@ -772,11 +838,10 @@ fn phase_cargo_runner(binary: &Path, binary_args: env::Args) {
772838
cmd.arg(arg);
773839
}
774840
}
775-
// Set sysroot.
776-
let sysroot =
777-
env::var_os("MIRI_SYSROOT").expect("the wrapper should have set MIRI_SYSROOT");
778-
cmd.arg("--sysroot");
779-
cmd.arg(sysroot);
841+
if env::var_os("MIRI_CALLED_FROM_RUSTDOC").is_none() {
842+
// Set sysroot (if we are inside rustdoc, we already did that in `phase_cargo_rustdoc`).
843+
forward_miri_sysroot(&mut cmd);
844+
}
780845
// Respect `MIRIFLAGS`.
781846
if let Ok(a) = env::var("MIRIFLAGS") {
782847
// This code is taken from `RUSTFLAGS` handling in cargo.
@@ -801,6 +866,77 @@ fn phase_cargo_runner(binary: &Path, binary_args: env::Args) {
801866
if verbose {
802867
eprintln!("[cargo-miri runner] {:?}", cmd);
803868
}
869+
870+
if std::env::var_os("MIRI_CALLED_FROM_RUSTDOC").is_some() {
871+
exec_with_pipe(cmd, &info.stdin)
872+
} else {
873+
exec(cmd)
874+
}
875+
}
876+
877+
fn phase_cargo_rustdoc(fst_arg: &str, mut args: env::Args) {
878+
let verbose = std::env::var_os("MIRI_VERBOSE").is_some();
879+
880+
// phase_cargo_miri sets the RUSTDOC env var to ourselves, so we can't use that here;
881+
// just default to a straight-forward invocation for now:
882+
let mut cmd = Command::new("rustdoc");
883+
884+
// Because of the way the main function is structured, we have to take the first argument spearately
885+
// from the rest; to simplify the following argument patching loop, we'll just skip that one.
886+
// This is fine for now, because cargo will never pass --extern arguments in the first position,
887+
// but we should defensively assert that this will work.
888+
let extern_flag = "--extern";
889+
assert!(fst_arg != extern_flag);
890+
cmd.arg(fst_arg);
891+
892+
let runtool_flag = "--runtool";
893+
// `crossmode` records if *any* argument matches `runtool_flag`; here we check the first one.
894+
let mut crossmode = fst_arg == runtool_flag;
895+
while let Some(arg) = args.next() {
896+
if arg == extern_flag {
897+
// Patch --extern arguments to use *.rmeta files, since phase_cargo_rustc only creates stub *.rlib files.
898+
forward_patched_extern_arg(&mut args, &mut cmd);
899+
} else if arg == runtool_flag {
900+
// An existing --runtool flag indicates cargo is running in cross-target mode, which we don't support.
901+
// Note that this is only passed when cargo is run with the unstable -Zdoctest-xcompile flag;
902+
// otherwise, we won't be called as rustdoc at all.
903+
crossmode = true;
904+
break;
905+
} else {
906+
cmd.arg(arg);
907+
}
908+
}
909+
910+
if crossmode {
911+
show_error(format!("cross-interpreting doc-tests is not currently supported by Miri."));
912+
}
913+
914+
// For each doc-test, rustdoc starts two child processes: first the test is compiled,
915+
// then the produced executable is invoked. We want to reroute both of these to cargo-miri,
916+
// such that the first time we'll enter phase_cargo_rustc, and phase_cargo_runner second.
917+
//
918+
// rustdoc invokes the test-builder by forwarding most of its own arguments, which makes
919+
// it difficult to determine when phase_cargo_rustc should run instead of phase_cargo_rustdoc.
920+
// Furthermore, the test code is passed via stdin, rather than a temporary file, so we need
921+
// to let phase_cargo_rustc know to expect that. We'll use this environment variable as a flag:
922+
cmd.env("MIRI_CALLED_FROM_RUSTDOC", "1");
923+
924+
// The `--test-builder` and `--runtool` arguments are unstable rustdoc features,
925+
// which are disabled by default. We first need to enable them explicitly:
926+
cmd.arg("-Z").arg("unstable-options");
927+
928+
// rustdoc needs to know the right sysroot.
929+
forward_miri_sysroot(&mut cmd);
930+
931+
// Make rustdoc call us back.
932+
let cargo_miri_path = std::env::current_exe().expect("current executable path invalid");
933+
cmd.arg("--test-builder").arg(&cargo_miri_path); // invoked by forwarding most arguments
934+
cmd.arg("--runtool").arg(&cargo_miri_path); // invoked with just a single path argument
935+
936+
if verbose {
937+
eprintln!("[cargo-miri rustdoc] {:?}", cmd);
938+
}
939+
804940
exec(cmd)
805941
}
806942

@@ -817,6 +953,29 @@ fn main() {
817953
return;
818954
}
819955

956+
// The way rustdoc invokes rustc is indistuingishable from the way cargo invokes rustdoc by the
957+
// arguments alone. `phase_cargo_rustdoc` sets this environment variable to let us disambiguate.
958+
let invoked_by_rustdoc = env::var_os("MIRI_CALLED_FROM_RUSTDOC").is_some();
959+
if invoked_by_rustdoc {
960+
// ...however, we then also see this variable when rustdoc invokes us as the testrunner!
961+
// The runner is invoked as `$runtool ($runtool-arg)* output_file`;
962+
// since we don't specify any runtool-args, and rustdoc supplies multiple arguments to
963+
// the test-builder unconditionally, we can just check the number of remaining arguments:
964+
if args.len() == 1 {
965+
let arg = args.next().unwrap();
966+
let binary = Path::new(&arg);
967+
if binary.exists() {
968+
phase_cargo_runner(binary, args);
969+
} else {
970+
show_error(format!("`cargo-miri` called with non-existing path argument `{}` in rustdoc mode; please invoke this binary through `cargo miri`", arg));
971+
}
972+
} else {
973+
phase_cargo_rustc(args);
974+
}
975+
976+
return;
977+
}
978+
820979
// Dispatch to `cargo-miri` phase. There are three phases:
821980
// - When we are called via `cargo miri`, we run as the frontend and invoke the underlying
822981
// cargo. We set RUSTC_WRAPPER and CARGO_TARGET_RUNNER to ourselves.
@@ -829,16 +988,15 @@ fn main() {
829988
Some("miri") => phase_cargo_miri(args),
830989
Some("rustc") => phase_cargo_rustc(args),
831990
Some(arg) => {
832-
// We have to distinguish the "runner" and "rustfmt" cases.
991+
// We have to distinguish the "runner" and "rustdoc" cases.
833992
// As runner, the first argument is the binary (a file that should exist, with an absolute path);
834-
// as rustfmt, the first argument is a flag (`--something`).
993+
// as rustdoc, the first argument is a flag (`--something`).
835994
let binary = Path::new(arg);
836995
if binary.exists() {
837996
assert!(!arg.starts_with("--")); // not a flag
838997
phase_cargo_runner(binary, args);
839998
} else if arg.starts_with("--") {
840-
// We are rustdoc.
841-
eprintln!("Running doctests is not currently supported by Miri.")
999+
phase_cargo_rustdoc(arg, args);
8421000
} else {
8431001
show_error(format!("`cargo-miri` called with unexpected first argument `{}`; please only invoke this binary through `cargo miri`", arg));
8441002
}

0 commit comments

Comments
 (0)