11use std:: env;
22use std:: ffi:: OsString ;
33use std:: fs:: { self , File } ;
4- use std:: io:: { self , BufRead , BufReader , BufWriter , Write } ;
54use std:: iter:: TakeWhile ;
5+ use std:: io:: { self , BufRead , BufReader , BufWriter , Read , Write } ;
66use std:: ops:: Not ;
77use std:: path:: { Path , PathBuf } ;
88use 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
6078impl 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
159176fn 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+
193226fn 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