1
1
use std:: env;
2
2
use std:: ffi:: OsString ;
3
3
use std:: fs:: { self , File } ;
4
- use std:: io:: { self , BufRead , BufReader , BufWriter , Write } ;
5
4
use std:: iter:: TakeWhile ;
5
+ use std:: io:: { self , BufRead , BufReader , BufWriter , Read , Write } ;
6
6
use std:: ops:: Not ;
7
7
use std:: path:: { Path , PathBuf } ;
8
8
use std:: process:: Command ;
@@ -46,6 +46,8 @@ struct CrateRunEnv {
46
46
env : Vec < ( OsString , OsString ) > ,
47
47
/// The current working directory.
48
48
current_dir : OsString ,
49
+ /// The contents passed via standard input.
50
+ stdin : Vec < u8 > ,
49
51
}
50
52
51
53
/// The information Miri needs to run a crate. Stored as JSON when the crate is "compiled".
@@ -63,7 +65,13 @@ impl CrateRunInfo {
63
65
let args = args. collect ( ) ;
64
66
let env = env:: vars_os ( ) . collect ( ) ;
65
67
let current_dir = env:: current_dir ( ) . unwrap ( ) . into_os_string ( ) ;
66
- Self :: RunWith ( CrateRunEnv { args, env, current_dir } )
68
+
69
+ let mut stdin = Vec :: new ( ) ;
70
+ if env:: var_os ( "MIRI_CALLED_FROM_RUSTDOC" ) . is_some ( ) {
71
+ std:: io:: stdin ( ) . lock ( ) . read_to_end ( & mut stdin) . expect ( "cannot read stdin" ) ;
72
+ }
73
+
74
+ Self :: RunWith ( CrateRunEnv { args, env, current_dir, stdin } )
67
75
}
68
76
69
77
fn store ( & self , filename : & Path ) {
@@ -190,6 +198,22 @@ fn exec(mut cmd: Command) {
190
198
}
191
199
}
192
200
201
+ /// Execute the command and pipe `input` into its stdin.
202
+ /// If it fails, fail this process with the same exit code.
203
+ /// Otherwise, continue.
204
+ fn exec_with_pipe ( mut cmd : Command , input : & [ u8 ] ) {
205
+ cmd. stdin ( std:: process:: Stdio :: piped ( ) ) ;
206
+ let mut child = cmd. spawn ( ) . expect ( "failed to spawn process" ) ;
207
+ {
208
+ let stdin = child. stdin . as_mut ( ) . expect ( "failed to open stdin" ) ;
209
+ stdin. write_all ( input) . expect ( "failed to write out test source" ) ;
210
+ }
211
+ let exit_status = child. wait ( ) . expect ( "failed to run command" ) ;
212
+ if exit_status. success ( ) . not ( ) {
213
+ std:: process:: exit ( exit_status. code ( ) . unwrap_or ( -1 ) )
214
+ }
215
+ }
216
+
193
217
fn xargo_version ( ) -> Option < ( u32 , u32 , u32 ) > {
194
218
let out = xargo_check ( ) . arg ( "--version" ) . output ( ) . ok ( ) ?;
195
219
if !out. status . success ( ) {
@@ -591,24 +615,29 @@ fn phase_cargo_rustc(mut args: env::Args) {
591
615
}
592
616
593
617
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
618
+ if let Some ( out_dir) = get_arg_flag_value ( "--out-dir" ) {
619
+ let mut path = PathBuf :: from ( out_dir) ;
620
+ path. push ( format ! (
621
+ "{}{}{}{}" ,
622
+ prefix,
623
+ get_arg_flag_value( "--crate-name" ) . unwrap( ) ,
624
+ // This is technically a `-C` flag but the prefix seems unique enough...
625
+ // (and cargo passes this before the filename so it should be unique)
626
+ get_arg_flag_value( "extra-filename" ) . unwrap_or( String :: new( ) ) ,
627
+ suffix,
628
+ ) ) ;
629
+ path
630
+ } else {
631
+ let out_file = get_arg_flag_value ( "-o" ) . unwrap ( ) ;
632
+ PathBuf :: from ( out_file)
633
+ }
605
634
}
606
635
607
636
let verbose = std:: env:: var_os ( "MIRI_VERBOSE" ) . is_some ( ) ;
608
637
let target_crate = is_target_crate ( ) ;
609
638
let print = get_arg_flag_value ( "--print" ) . is_some ( ) ; // whether this is cargo passing `--print` to get some infos
610
639
611
- let store_json = |info : CrateRunInfo | {
640
+ let store_json = |info : & CrateRunInfo | {
612
641
// Create a stub .d file to stop Cargo from "rebuilding" the crate:
613
642
// https://github.com/rust-lang/miri/issues/1724#issuecomment-787115693
614
643
// As we store a JSON file instead of building the crate here, an empty file is fine.
@@ -636,15 +665,55 @@ fn phase_cargo_rustc(mut args: env::Args) {
636
665
// like we want them.
637
666
// Instead of compiling, we write JSON into the output file with all the relevant command-line flags
638
667
// and environment variables; this is used when cargo calls us again in the CARGO_TARGET_RUNNER phase.
639
- store_json ( CrateRunInfo :: collect ( args) ) ;
668
+ let info = CrateRunInfo :: collect ( args) ;
669
+ store_json ( & info) ;
670
+
671
+ // Rustdoc expects us to exit with an error code if the test is marked as `compile_fail`,
672
+ // just creating the JSON file is not enough: we need to detect syntax errors,
673
+ // so we need to run Miri with `MIRI_BE_RUSTC` for a check-only build.
674
+ if std:: env:: var_os ( "MIRI_CALLED_FROM_RUSTDOC" ) . is_some ( ) {
675
+ let mut cmd = miri ( ) ;
676
+ let env = if let CrateRunInfo :: RunWith ( env) = info {
677
+ env
678
+ } else {
679
+ return ;
680
+ } ;
681
+
682
+ // use our own sysroot
683
+ if !has_arg_flag ( "--sysroot" ) {
684
+ let sysroot = env:: var_os ( "MIRI_SYSROOT" )
685
+ . expect ( "the wrapper should have set MIRI_SYSROOT" ) ;
686
+ cmd. arg ( "--sysroot" ) . arg ( sysroot) ;
687
+ }
688
+
689
+ // ensure --emit argument for a check-only build is present
690
+ if let Some ( i) = env. args . iter ( ) . position ( |arg| arg. starts_with ( "--emit=" ) ) {
691
+ // We need to make sure we're not producing a binary that overwrites the JSON file.
692
+ // rustdoc should only ever pass an --emit=metadata argument for tests marked as `no_run`:
693
+ assert_eq ! ( env. args[ i] , "--emit=metadata" ) ;
694
+ } else {
695
+ cmd. arg ( "--emit=dep-info,metadata" ) ;
696
+ }
697
+
698
+ cmd. args ( env. args ) ;
699
+ cmd. env ( "MIRI_BE_RUSTC" , "1" ) ;
700
+
701
+ if verbose {
702
+ eprintln ! ( "[cargo-miri rustc] captured input:\n {}" , std:: str :: from_utf8( & env. stdin) . unwrap( ) ) ;
703
+ eprintln ! ( "[cargo-miri rustc] {:?}" , cmd) ;
704
+ }
705
+
706
+ exec_with_pipe ( cmd, & env. stdin ) ;
707
+ }
708
+
640
709
return ;
641
710
}
642
711
643
712
if runnable_crate && ArgFlagValueIter :: new ( "--extern" ) . any ( |krate| krate == "proc_macro" ) {
644
713
// This is a "runnable" `proc-macro` crate (unit tests). We do not support
645
714
// interpreting that under Miri now, so we write a JSON file to (display a
646
715
// helpful message and) skip it in the runner phase.
647
- store_json ( CrateRunInfo :: SkipProcMacroTest ) ;
716
+ store_json ( & CrateRunInfo :: SkipProcMacroTest ) ;
648
717
return ;
649
718
}
650
719
@@ -715,6 +784,18 @@ fn phase_cargo_rustc(mut args: env::Args) {
715
784
}
716
785
}
717
786
787
+ fn forward_patched_extern_arg ( args : & mut impl Iterator < Item = String > , cmd : & mut Command ) {
788
+ cmd. arg ( "--extern" ) ; // always forward flag, but adjust filename:
789
+ let path = args. next ( ) . expect ( "`--extern` should be followed by a filename" ) ;
790
+ if let Some ( lib) = path. strip_suffix ( ".rlib" ) {
791
+ // If this is an rlib, make it an rmeta.
792
+ cmd. arg ( format ! ( "{}.rmeta" , lib) ) ;
793
+ } else {
794
+ // Some other extern file (e.g. a `.so`). Forward unchanged.
795
+ cmd. arg ( path) ;
796
+ }
797
+ }
798
+
718
799
fn phase_cargo_runner ( binary : & Path , binary_args : env:: Args ) {
719
800
let verbose = std:: env:: var_os ( "MIRI_VERBOSE" ) . is_some ( ) ;
720
801
@@ -801,6 +882,73 @@ fn phase_cargo_runner(binary: &Path, binary_args: env::Args) {
801
882
if verbose {
802
883
eprintln ! ( "[cargo-miri runner] {:?}" , cmd) ;
803
884
}
885
+
886
+ if std:: env:: var_os ( "MIRI_CALLED_FROM_RUSTDOC" ) . is_some ( ) {
887
+ exec_with_pipe ( cmd, & info. stdin )
888
+ } else {
889
+ exec ( cmd)
890
+ }
891
+ }
892
+
893
+ fn phase_cargo_rustdoc ( fst_arg : & str , mut args : env:: Args ) {
894
+ let verbose = std:: env:: var_os ( "MIRI_VERBOSE" ) . is_some ( ) ;
895
+
896
+ // phase_cargo_miri sets the RUSTDOC env var to ourselves, so we can't use that here;
897
+ // just default to a straight-forward invocation for now:
898
+ let mut cmd = Command :: new ( OsString :: from ( "rustdoc" ) ) ;
899
+
900
+ // Because of the way the main function is structured, we have to take the first argument spearately
901
+ // from the rest; to simplify the following argument patching loop, we'll just skip that one.
902
+ // This is fine for now, because cargo will never pass --extern arguments in the first position,
903
+ // but we should defensively assert that this will work.
904
+ let extern_flag = "--extern" ;
905
+ assert ! ( fst_arg != extern_flag) ;
906
+ cmd. arg ( fst_arg) ;
907
+
908
+ let runtool_flag = "--runtool" ;
909
+ let mut crossmode = fst_arg == runtool_flag;
910
+ while let Some ( arg) = args. next ( ) {
911
+ if arg == extern_flag {
912
+ // Patch --extern arguments to use *.rmeta files, since phase_cargo_rustc only creates stub *.rlib files.
913
+ forward_patched_extern_arg ( & mut args, & mut cmd) ;
914
+ } else if arg == runtool_flag {
915
+ // An existing --runtool flag indicates cargo is running in cross-target mode, which we don't support.
916
+ // Note that this is only passed when cargo is run with the unstable -Zdoctest-xcompile flag;
917
+ // otherwise, we won't be called as rustdoc at all.
918
+ crossmode = true ;
919
+ break ;
920
+ } else {
921
+ cmd. arg ( arg) ;
922
+ }
923
+ }
924
+
925
+ if crossmode {
926
+ eprintln ! ( "Cross-interpreting doc-tests is not currently supported by Miri." ) ;
927
+ return ;
928
+ }
929
+
930
+ // For each doc-test, rustdoc starts two child processes: first the test is compiled,
931
+ // then the produced executable is invoked. We want to reroute both of these to cargo-miri,
932
+ // such that the first time we'll enter phase_cargo_rustc, and phase_cargo_runner second.
933
+ //
934
+ // rustdoc invokes the test-builder by forwarding most of its own arguments, which makes
935
+ // it difficult to determine when phase_cargo_rustc should run instead of phase_cargo_rustdoc.
936
+ // Furthermore, the test code is passed via stdin, rather than a temporary file, so we need
937
+ // to let phase_cargo_rustc know to expect that. We'll use this environment variable as a flag:
938
+ cmd. env ( "MIRI_CALLED_FROM_RUSTDOC" , "1" ) ;
939
+
940
+ // The `--test-builder` and `--runtool` arguments are unstable rustdoc features,
941
+ // which are disabled by default. We first need to enable them explicitly:
942
+ cmd. arg ( "-Z" ) . arg ( "unstable-options" ) ;
943
+
944
+ let cargo_miri_path = std:: env:: current_exe ( ) . expect ( "current executable path invalid" ) ;
945
+ cmd. arg ( "--test-builder" ) . arg ( & cargo_miri_path) ; // invoked by forwarding most arguments
946
+ cmd. arg ( "--runtool" ) . arg ( & cargo_miri_path) ; // invoked with just a single path argument
947
+
948
+ if verbose {
949
+ eprintln ! ( "[cargo-miri rustdoc] {:?}" , cmd) ;
950
+ }
951
+
804
952
exec ( cmd)
805
953
}
806
954
@@ -817,6 +965,30 @@ fn main() {
817
965
return ;
818
966
}
819
967
968
+ // The way rustdoc invokes rustc is indistuingishable from the way cargo invokes rustdoc
969
+ // by the arguments alone, and we can't take from the args iterator in this case.
970
+ // phase_cargo_rustdoc sets this environment variable to let us disambiguate here
971
+ let invoked_by_rustdoc = env:: var_os ( "MIRI_CALLED_FROM_RUSTDOC" ) . is_some ( ) ;
972
+ if invoked_by_rustdoc {
973
+ // ...however, we then also see this variable when rustdoc invokes us as the testrunner!
974
+ // The runner is invoked as `$runtool ($runtool-arg)* output_file`;
975
+ // since we don't specify any runtool-args, and rustdoc supplies multiple arguments to
976
+ // the test-builder unconditionally, we can just check the number of remaining arguments:
977
+ if args. len ( ) == 1 {
978
+ let arg = args. next ( ) . unwrap ( ) ;
979
+ let binary = Path :: new ( & arg) ;
980
+ if binary. exists ( ) {
981
+ phase_cargo_runner ( binary, args) ;
982
+ } else {
983
+ show_error ( format ! ( "`cargo-miri` called with non-existing path argument `{}` in rustdoc mode; please invoke this binary through `cargo miri`" , arg) ) ;
984
+ }
985
+ } else {
986
+ phase_cargo_rustc ( args) ;
987
+ }
988
+
989
+ return ;
990
+ }
991
+
820
992
// Dispatch to `cargo-miri` phase. There are three phases:
821
993
// - When we are called via `cargo miri`, we run as the frontend and invoke the underlying
822
994
// cargo. We set RUSTC_WRAPPER and CARGO_TARGET_RUNNER to ourselves.
@@ -829,16 +1001,15 @@ fn main() {
829
1001
Some ( "miri" ) => phase_cargo_miri ( args) ,
830
1002
Some ( "rustc" ) => phase_cargo_rustc ( args) ,
831
1003
Some ( arg) => {
832
- // We have to distinguish the "runner" and "rustfmt " cases.
1004
+ // We have to distinguish the "runner" and "rustdoc " cases.
833
1005
// 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`).
1006
+ // as rustdoc , the first argument is a flag (`--something`).
835
1007
let binary = Path :: new ( arg) ;
836
1008
if binary. exists ( ) {
837
1009
assert ! ( !arg. starts_with( "--" ) ) ; // not a flag
838
1010
phase_cargo_runner ( binary, args) ;
839
1011
} else if arg. starts_with ( "--" ) {
840
- // We are rustdoc.
841
- eprintln ! ( "Running doctests is not currently supported by Miri." )
1012
+ phase_cargo_rustdoc ( arg, args) ;
842
1013
} else {
843
1014
show_error ( format ! ( "`cargo-miri` called with unexpected first argument `{}`; please only invoke this binary through `cargo miri`" , arg) ) ;
844
1015
}
0 commit comments