11//! Get "compiler" args from cargo
22
33use crate :: errors:: * ;
4- use log:: { info, warn} ;
4+ use cargo_manifest:: { Edition , Manifest , MaybeInherited :: Local } ;
5+ use log:: { debug, info} ;
56use std:: fs;
67use std:: fs:: File ;
78use std:: io:: prelude:: * ;
@@ -40,45 +41,73 @@ use std::process::Command;
4041
4142#[ derive( Debug ) ]
4243pub struct ExternArgs {
43- suffix_args : Vec < String > ,
44+ edition : String ,
45+ crate_name : String ,
46+ lib_list : Vec < String > ,
47+ extern_list : Vec < String > ,
4448}
4549
4650impl ExternArgs {
4751 /// simple constructor
4852 pub fn new ( ) -> Self {
4953 ExternArgs {
50- suffix_args : vec ! [ ] ,
54+ edition : String :: default ( ) ,
55+ crate_name : String :: default ( ) ,
56+ lib_list : vec ! [ ] ,
57+ extern_list : vec ! [ ] ,
5158 }
5259 }
5360
5461 /// Run a `cargo build` to see what args Cargo is using for library paths and extern crates.
55- /// Touch a source file to ensure something is compiled and the args will be visible.
56- ///
57- /// >>>Future research: see whether `cargo check` can be used instead. It emits the `--extern`s
58- /// with `.rmeta` instead of `.rlib`, and the compiler can't actually use those
59- /// when compiling a doctest. But perhaps simply changing the file extension would work?
62+ /// Touch a source file in the crate to ensure something is compiled and the args will be visible.
63+
6064 pub fn load ( & mut self , proj_root : & Path ) -> Result < & Self > {
65+ // find Cargo.toml and determine the package name and lib or bin source file.
66+ let cargo_path = proj_root. join ( "Cargo.toml" ) ;
67+ let mut manifest = Manifest :: from_path ( & cargo_path) ?;
68+ manifest. complete_from_path ( proj_root) ?; // try real hard to determine bin or lib
69+ let package = manifest
70+ . package
71+ . expect ( "doctest Cargo.toml must include a [package] section" ) ;
72+
73+ self . crate_name = package. name . replace ( '-' , "_" ) ; // maybe cargo shouldn't allow packages to include non-identifier characters?
74+ // in any case, this won't work when default crate doesn't have package name (which I'm sure cargo allows somehow or another)
75+ self . edition = if let Some ( Local ( edition) ) = package. edition {
76+ my_display_edition ( edition)
77+ } else {
78+ "2015" . to_owned ( ) // and good luck to you, sir!
79+ } ;
80+
81+ debug ! (
82+ "parsed from manifest: name: {}, edition: {}" ,
83+ self . crate_name,
84+ format!( "{:?}" , self . edition)
85+ ) ;
86+
6187 // touch (change) a file in the project to force check to do something
88+ // I haven't figured out how to determine bin or lib source file from cargo, fall back on heuristics here.
6289
63- for fname in [ "lib.rs" , "main.rs" ] {
64- let try_path: PathBuf = [ & proj_root. to_string_lossy ( ) , "src" , fname]
65- . iter ( )
66- . collect ( ) ;
90+ for fname in [ "main.rs" , "lib.rs" ] {
91+ let try_path: PathBuf = proj_root. join ( "src" ) . join ( fname) ;
6792 if try_path. exists ( ) {
6893 touch ( & try_path) ?;
69- self . run_cargo ( proj_root) ?;
94+ self . run_cargo ( proj_root, & cargo_path ) ?;
7095 return Ok ( self ) ;
7196 // file should be closed when f goes out of scope at bottom of this loop
7297 }
7398 }
74- bail ! ( "Couldn't find source target in project {:?}" , proj_root)
99+ bail ! ( "Couldn't find lib or bin source in project {:?}" , proj_root)
75100 }
76101
77- fn run_cargo ( & mut self , proj_root : & Path ) -> Result < & Self > {
102+ fn run_cargo ( & mut self , proj_root : & Path , manifest_path : & Path ) -> Result < & Self > {
78103 let mut cmd = Command :: new ( "cargo" ) ;
79- cmd. current_dir ( & proj_root) . arg ( "build" ) . arg ( "--verbose" ) ;
80-
104+ cmd. current_dir ( & proj_root)
105+ . arg ( "build" )
106+ . arg ( "--verbose" )
107+ . arg ( "--manifest-path" )
108+ . arg ( manifest_path) ;
81109 info ! ( "running {:?}" , cmd) ;
110+
82111 let output = cmd. output ( ) ?;
83112
84113 if !output. status . success ( ) {
@@ -90,63 +119,107 @@ impl ExternArgs {
90119 ) ;
91120 }
92121
122+ //ultimatedebug std::fs::write(proj_root.join("mdbook_cargo_out.txt"), &output.stderr)?;
123+
93124 let cmd_resp: & str = std:: str:: from_utf8 ( & output. stderr ) ?;
94- self . parse_response ( & cmd_resp) ?;
125+ self . parse_response ( & self . crate_name . clone ( ) , & cmd_resp) ?;
95126
96127 Ok ( self )
97128 }
98129
99130 /// Parse response stdout+stderr response from `cargo build`
100- /// into arguments we can use to invoke rustdoc.
101- /// Stop at first line that traces a compiler invocation .
131+ /// into arguments we can use to invoke rustdoc (--edition --extern and -L) .
132+ /// The response may contain multiple builds, scan for the one that corresponds to the doctest crate .
102133 ///
103- /// >>> This parser is broken, doesn't handle arg values with embedded spaces (single quoted).
134+ /// > This parser is broken, doesn't handle arg values with embedded spaces (single quoted).
104135 /// Fortunately, the args we care about (so far) don't have those kinds of values.
105- pub fn parse_response ( & mut self , buf : & str ) -> Result < ( ) > {
136+ pub fn parse_response ( & mut self , my_crate : & str , buf : & str ) -> Result < ( ) > {
137+ let mut builds_ignored = 0 ;
138+
139+ let my_cn_arg = format ! ( " --crate-name {}" , my_crate) ;
106140 for l in buf. lines ( ) {
107141 if let Some ( _i) = l. find ( " Running " ) {
108- let args_seg: & str = l. split ( '`' ) . skip ( 1 ) . take ( 1 ) . collect :: < Vec < _ > > ( ) [ 0 ] ; // sadly, cargo decorates string with backticks
109- let mut arg_iter = args_seg. split_whitespace ( ) ;
110-
111- while let Some ( arg) = arg_iter. next ( ) {
112- match arg {
113- "-L" | "--library-path" => {
114- self . suffix_args . push ( arg. to_owned ( ) ) ;
115- self . suffix_args
116- . push ( arg_iter. next ( ) . unwrap_or ( "" ) . to_owned ( ) ) ;
142+ if let Some ( _cn_pos) = l. find ( & my_cn_arg) {
143+ let args_seg: & str = l. split ( '`' ) . skip ( 1 ) . take ( 1 ) . collect :: < Vec < _ > > ( ) [ 0 ] ; // sadly, cargo decorates string with backticks
144+ let mut arg_iter = args_seg. split_whitespace ( ) ;
145+
146+ while let Some ( arg) = arg_iter. next ( ) {
147+ match arg {
148+ "-L" | "--library-path" => {
149+ self . lib_list
150+ . push ( arg_iter. next ( ) . unwrap_or_default ( ) . to_owned ( ) ) ;
151+ }
152+
153+ "--extern" => {
154+ let mut dep_arg = arg_iter. next ( ) . unwrap_or_default ( ) . to_owned ( ) ;
155+
156+ // sometimes, build references the.rmeta even though our doctests will require .rlib
157+ // so convert the argument and hope for the best.
158+ // if .rlib is not there when the doctest runs, it will complain.
159+ if dep_arg. ends_with ( ".rmeta" ) {
160+ debug ! (
161+ "Build referenced {}, converted to .rlib hoping that actual file will be there in time." ,
162+ dep_arg) ;
163+ dep_arg = dep_arg. replace ( ".rmeta" , ".rlib" ) ;
164+ }
165+ self . extern_list . push ( dep_arg) ;
166+ }
167+
168+ "--crate-name" => {
169+ self . crate_name = arg_iter. next ( ) . unwrap_or_default ( ) . to_owned ( ) ;
170+ }
171+
172+ _ => {
173+ if let Some ( ( kw, val) ) = arg. split_once ( '=' ) {
174+ if kw == "--edition" {
175+ self . edition = val. to_owned ( ) ;
176+ }
177+ }
178+ }
117179 }
118- "--extern" => {
119- // needs a hack to force reference to rlib over rmeta
120- self . suffix_args . push ( arg. to_owned ( ) ) ;
121- self . suffix_args . push (
122- arg_iter
123- . next ( )
124- . unwrap_or ( "" )
125- . replace ( ".rmeta" , ".rlib" )
126- . to_owned ( ) ,
127- ) ;
128- }
129- _ => { }
130180 }
181+ } else {
182+ builds_ignored += 1 ;
131183 }
132-
133- return Ok ( ( ) ) ;
134184 } ;
135185 }
136186
137- if self . suffix_args . len ( ) < 1 {
138- warn ! ( "Couldn't extract --extern args from Cargo, is current directory == cargo project root?" ) ;
187+ if self . extern_list . len ( ) == 0 || self . lib_list . len ( ) == 0 {
188+ bail ! ( "Couldn't extract -L or --extern args from Cargo, is current directory == cargo project root?" ) ;
139189 }
140190
191+ debug ! (
192+ "Ignored {} other builds performed in this run" ,
193+ builds_ignored
194+ ) ;
195+
141196 Ok ( ( ) )
142197 }
143198
144- /// get a list of (- L and --extern) args used to invoke rustdoc .
199+ /// provide the parsed external args used to invoke rustdoc (--edition, - L and --extern).
145200 pub fn get_args ( & self ) -> Vec < String > {
146- self . suffix_args . clone ( )
201+ let mut ret_val: Vec < String > = vec ! [ "--edition" . to_owned( ) , self . edition. clone( ) ] ;
202+ for i in & self . lib_list {
203+ ret_val. push ( "-L" . to_owned ( ) ) ;
204+ ret_val. push ( i. clone ( ) ) ;
205+ }
206+ for j in & self . extern_list {
207+ ret_val. push ( "--extern" . to_owned ( ) ) ;
208+ ret_val. push ( j. clone ( ) ) ;
209+ }
210+ ret_val
147211 }
148212}
149213
214+ fn my_display_edition ( edition : Edition ) -> String {
215+ match edition {
216+ Edition :: E2015 => "2015" ,
217+ Edition :: E2018 => "2018" ,
218+ Edition :: E2021 => "2021" ,
219+ Edition :: E2024 => "2024" ,
220+ }
221+ . to_owned ( )
222+ }
150223// Private "touch" function to update file modification time without changing content.
151224// needed because [std::fs::set_modified] is unstable in rust 1.74,
152225// which is currently the MSRV for mdBook. It is available in rust 1.76 onward.
@@ -196,7 +269,7 @@ mod test {
196269 "### ;
197270
198271 let mut ea = ExternArgs :: new ( ) ;
199- ea. parse_response ( & test_str) ?;
272+ ea. parse_response ( & test_str, "leptos_book" ) ?;
200273
201274 let args = ea. get_args ( ) ;
202275 assert_eq ! ( 18 , args. len( ) ) ;
0 commit comments