cargo/core/compiler/fingerprint/
rustdoc.rs1use std::collections::HashMap;
2use std::path::Path;
3use std::path::PathBuf;
4
5use anyhow::Context as _;
6use cargo_util::paths;
7use serde::Deserialize;
8use serde::Serialize;
9
10use crate::CargoResult;
11use crate::core::compiler::BuildRunner;
12use crate::core::compiler::CompileKind;
13
14#[derive(Debug, Serialize, Deserialize)]
16struct RustdocFingerprintJson {
17 pub rustc_vv: String,
19
20 #[serde(default, skip_serializing_if = "Vec::is_empty")]
22 pub doc_parts: Vec<PathBuf>,
23}
24
25#[derive(Debug)]
37pub struct RustdocFingerprint {
38 path: PathBuf,
40 rustc_vv: String,
42 doc_parts: Vec<PathBuf>,
44 on_disk: Option<RustdocFingerprintJson>,
46}
47
48impl RustdocFingerprint {
49 pub fn check_rustdoc_fingerprint(build_runner: &BuildRunner<'_, '_>) -> CargoResult<()> {
65 if build_runner
66 .bcx
67 .gctx
68 .cli_unstable()
69 .skip_rustdoc_fingerprint
70 {
71 return Ok(());
72 }
73 let new_fingerprint = RustdocFingerprintJson {
74 rustc_vv: build_runner.bcx.rustc().verbose_version.clone(),
75 doc_parts: Vec::new(),
76 };
77
78 for kind in &build_runner.bcx.build_config.requested_kinds {
79 check_fingerprint(build_runner, &new_fingerprint, *kind)?;
80 }
81
82 Ok(())
83 }
84
85 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 pub fn persist<F>(&self, exec: F) -> CargoResult<()>
108 where
109 F: Fn(&[&Path]) -> CargoResult<()>,
111 {
112 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 .map(|p| base.join(p))
129 .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 let doc_parts_dirs: Vec<_> = doc_parts.iter().map(|p| p.parent().unwrap()).collect();
144 exec(&doc_parts_dirs)?;
145
146 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 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 }
189}
190
191fn fingerprint_path(build_runner: &BuildRunner<'_, '_>, kind: CompileKind) -> PathBuf {
193 build_runner
194 .files()
195 .layout(kind)
196 .build_dir()
197 .root()
198 .join(".rustdoc_fingerprint.json")
199}
200
201fn check_fingerprint(
203 build_runner: &BuildRunner<'_, '_>,
204 new_fingerprint: &RustdocFingerprintJson,
205 kind: CompileKind,
206) -> CargoResult<()> {
207 let fingerprint_path = fingerprint_path(build_runner, kind);
208
209 let write_fingerprint = || -> CargoResult<()> {
210 paths::write(&fingerprint_path, serde_json::to_string(new_fingerprint)?)
211 };
212
213 let Ok(rustdoc_data) = paths::read(&fingerprint_path) else {
214 return write_fingerprint();
219 };
220
221 match serde_json::from_str::<RustdocFingerprintJson>(&rustdoc_data) {
222 Ok(on_disk_fingerprint) => {
223 if on_disk_fingerprint.rustc_vv == new_fingerprint.rustc_vv {
224 return Ok(());
225 } else {
226 tracing::debug!(
227 "doc fingerprint changed:\noriginal:\n{}\nnew:\n{}",
228 on_disk_fingerprint.rustc_vv,
229 new_fingerprint.rustc_vv
230 );
231 }
232 }
233 Err(e) => {
234 tracing::debug!("could not deserialize {:?}: {}", fingerprint_path, e);
235 }
236 };
237 tracing::debug!(
239 "fingerprint {:?} mismatch, clearing doc directories",
240 fingerprint_path
241 );
242 let doc_dir = build_runner
243 .files()
244 .layout(kind)
245 .artifact_dir()
246 .expect("artifact-dir was not locked")
247 .doc();
248 if doc_dir.exists() {
249 clean_doc(doc_dir)?;
250 }
251
252 write_fingerprint()?;
253
254 Ok(())
255}
256
257fn 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
276fn clean_doc(path: &Path) -> CargoResult<()> {
277 let entries = path
278 .read_dir()
279 .with_context(|| format!("failed to read directory `{}`", path.display()))?;
280 for entry in entries {
281 let entry = entry?;
282 if entry
285 .file_name()
286 .to_str()
287 .map_or(false, |name| name.starts_with('.'))
288 {
289 continue;
290 }
291 let path = entry.path();
292 if entry.file_type()?.is_dir() {
293 paths::remove_dir_all(path)?;
294 } else {
295 paths::remove_file(path)?;
296 }
297 }
298 Ok(())
299}