1+ use std:: collections:: HashMap ;
12use std:: path:: Path ;
23use std:: path:: PathBuf ;
34
@@ -15,6 +16,10 @@ use crate::core::compiler::CompileKind;
1516struct RustdocFingerprintJson {
1617 /// `rustc -vV` verbose version output.
1718 pub rustc_vv : String ,
19+
20+ /// Relative paths to cross crate info JSON files from previous `cargo doc` invocations.
21+ #[ serde( default , skip_serializing_if = "Vec::is_empty" ) ]
22+ pub doc_parts : Vec < PathBuf > ,
1823}
1924
2025/// Structure used to deal with Rustdoc fingerprinting
@@ -29,7 +34,16 @@ struct RustdocFingerprintJson {
2934/// they were compiled with the same Rustc version that we're currently using.
3035/// Otherwise we must remove the `doc/` folder and compile again forcing a rebuild.
3136#[ derive( Debug ) ]
32- pub struct RustdocFingerprint { }
37+ pub struct RustdocFingerprint {
38+ /// Path to the fingerprint file.
39+ path : PathBuf ,
40+ /// `rustc -vV` verbose version output for the current session.
41+ rustc_vv : String ,
42+ /// Absolute paths to new cross crate info JSON files generated in the current session.
43+ doc_parts : Vec < PathBuf > ,
44+ /// The fingerprint file on disk.
45+ on_disk : Option < RustdocFingerprintJson > ,
46+ }
3347
3448impl RustdocFingerprint {
3549 /// Checks whether the latest version of rustc used to compile this workspace's docs
@@ -58,6 +72,7 @@ impl RustdocFingerprint {
5872 }
5973 let new_fingerprint = RustdocFingerprintJson {
6074 rustc_vv : build_runner. bcx . rustc ( ) . verbose_version . clone ( ) ,
75+ doc_parts : Vec :: new ( ) ,
6176 } ;
6277
6378 for kind in & build_runner. bcx . build_config . requested_kinds {
@@ -66,6 +81,111 @@ impl RustdocFingerprint {
6681
6782 Ok ( ( ) )
6883 }
84+
85+ /// Creates a new fingerprint with given doc parts paths.
86+ pub fn new (
87+ build_runner : & BuildRunner < ' _ , ' _ > ,
88+ kind : CompileKind ,
89+ doc_parts : Vec < PathBuf > ,
90+ ) -> Self {
91+ let path = fingerprint_path ( build_runner, kind) ;
92+ let rustc_vv = build_runner. bcx . rustc ( ) . verbose_version . clone ( ) ;
93+ let on_disk = load_on_disk ( & path) ;
94+ Self {
95+ path,
96+ rustc_vv,
97+ doc_parts,
98+ on_disk,
99+ }
100+ }
101+
102+ /// Persists the fingerprint.
103+ ///
104+ /// The closure will run before persisting the fingerprint,
105+ /// and will be given a list of doc parts directories for passing to
106+ /// `rustdoc --include-parts-dir`.
107+ pub fn persist < F > ( & self , exec : F ) -> CargoResult < ( ) >
108+ where
109+ // 1. paths for `--include-parts-dir`
110+ F : Fn ( & [ & Path ] ) -> CargoResult < ( ) > ,
111+ {
112+ // Dedupe crate with the same name by file stem (which is effectively crate name),
113+ // since rustdoc doesn't distinguish different crate versions.
114+ //
115+ // Rules applied here:
116+ //
117+ // * If name collides, favor the one selected via CLI over cached ones
118+ // (done by the insertion order)
119+ let base = self . path . parent ( ) . unwrap ( ) ;
120+ let on_disk_doc_parts: Vec < _ > = self
121+ . on_disk
122+ . iter ( )
123+ . flat_map ( |on_disk| {
124+ on_disk
125+ . doc_parts
126+ . iter ( )
127+ // Make absolute so that we can pass to rustdoc
128+ . map ( |p| base. join ( p) )
129+ // Doc parts may be selectively cleaned by `cargo clean -p <doc>`.
130+ // We should stop caching those no-exist.
131+ . filter ( |p| p. exists ( ) )
132+ } )
133+ . collect ( ) ;
134+ let dedup_map = on_disk_doc_parts
135+ . iter ( )
136+ . chain ( self . doc_parts . iter ( ) )
137+ . map ( |p| ( p. file_stem ( ) , p) )
138+ . collect :: < HashMap < _ , _ > > ( ) ;
139+ let mut doc_parts: Vec < _ > = dedup_map. into_values ( ) . collect ( ) ;
140+ doc_parts. sort_unstable ( ) ;
141+
142+ // Prepare args for `rustdoc --include-parts-dir`
143+ let doc_parts_dirs: Vec < _ > = doc_parts. iter ( ) . map ( |p| p. parent ( ) . unwrap ( ) ) . collect ( ) ;
144+ exec ( & doc_parts_dirs) ?;
145+
146+ // Persist with relative paths to the directory where fingerprint file is at.
147+ let json = RustdocFingerprintJson {
148+ rustc_vv : self . rustc_vv . clone ( ) ,
149+ doc_parts : doc_parts
150+ . iter ( )
151+ . map ( |p| p. strip_prefix ( base) . unwrap_or ( p) . to_owned ( ) )
152+ . collect ( ) ,
153+ } ;
154+ paths:: write ( & self . path , serde_json:: to_string ( & json) ?) ?;
155+
156+ Ok ( ( ) )
157+ }
158+
159+ /// Checks if the fingerprint is outdated comparing against given doc parts file paths.
160+ pub fn is_dirty ( & self ) -> bool {
161+ let Some ( on_disk) = self . on_disk . as_ref ( ) else {
162+ return true ;
163+ } ;
164+
165+ let Some ( fingerprint_mtime) = paths:: mtime ( & self . path ) . ok ( ) else {
166+ return true ;
167+ } ;
168+
169+ if self . rustc_vv != on_disk. rustc_vv {
170+ return true ;
171+ }
172+
173+ for path in & self . doc_parts {
174+ let parts_mtime = match paths:: mtime ( & path) {
175+ Ok ( mtime) => mtime,
176+ Err ( e) => {
177+ tracing:: debug!( "failed to read mtime of {}: {e}" , path. display( ) ) ;
178+ return true ;
179+ }
180+ } ;
181+
182+ if parts_mtime > fingerprint_mtime {
183+ return true ;
184+ }
185+ }
186+
187+ false
188+ }
69189}
70190
71191/// Returns the path to rustdoc fingerprint file for a given [`CompileKind`].
@@ -134,6 +254,25 @@ fn check_fingerprint(
134254 Ok ( ( ) )
135255}
136256
257+ /// Loads an on-disk fingerprint JSON file.
258+ fn load_on_disk ( path : & Path ) -> Option < RustdocFingerprintJson > {
259+ let on_disk = match paths:: read ( path) {
260+ Ok ( data) => data,
261+ Err ( e) => {
262+ tracing:: debug!( "failed to read rustdoc fingerprint at {path:?}: {e}" ) ;
263+ return None ;
264+ }
265+ } ;
266+
267+ match serde_json:: from_str :: < RustdocFingerprintJson > ( & on_disk) {
268+ Ok ( on_disk) => Some ( on_disk) ,
269+ Err ( e) => {
270+ tracing:: debug!( "could not deserialize {path:?}: {e}" ) ;
271+ None
272+ }
273+ }
274+ }
275+
137276fn clean_doc ( path : & Path ) -> CargoResult < ( ) > {
138277 let entries = path
139278 . read_dir ( )
0 commit comments