| use crate::util::data_structures::HashMap; |
| use std::path::Path; |
| use std::path::PathBuf; |
| |
| use anyhow::Context as _; |
| use cargo_util::paths; |
| use serde::Deserialize; |
| use serde::Serialize; |
| |
| use crate::CargoResult; |
| use crate::compiler::BuildRunner; |
| use crate::compiler::CompileKind; |
| |
| /// JSON Schema of the [`RustdocFingerprint`] file. |
| #[derive(Debug, Serialize, Deserialize)] |
| struct RustdocFingerprintJson { |
| /// `rustc -vV` verbose version output. |
| pub rustc_vv: String, |
| |
| /// Relative paths to cross crate info JSON files from previous `cargo doc` invocations. |
| #[serde(default, skip_serializing_if = "Vec::is_empty")] |
| pub doc_parts: Vec<PathBuf>, |
| } |
| |
| /// Structure used to deal with Rustdoc fingerprinting |
| /// |
| /// This is important because the `.js`/`.html` & `.css` files |
| /// that are generated by Rustdoc don't have any versioning yet |
| /// (see <https://github.com/rust-lang/cargo/issues/8461>). |
| /// Therefore, we can end up with weird bugs and behaviours |
| /// if we mix different versions of these files. |
| /// |
| /// We need to make sure that if there were any previous docs already compiled, |
| /// they were compiled with the same Rustdoc version that we're currently using. |
| /// Otherwise we must remove the `doc/` folder and compile again forcing a rebuild. |
| #[derive(Debug)] |
| pub struct RustdocFingerprint { |
| /// Path to the fingerprint file. |
| path: PathBuf, |
| /// `rustc -vV` verbose version output for the current session. |
| rustc_vv: String, |
| /// Absolute paths to new cross crate info JSON files generated in the current session. |
| doc_parts: Vec<PathBuf>, |
| /// The fingerprint file on disk. |
| on_disk: Option<RustdocFingerprintJson>, |
| } |
| |
| impl RustdocFingerprint { |
| /// Checks whether the latest version of rustc used to compile this workspace's docs |
| /// was the same as the one is currently being used in this `cargo doc` call. |
| /// |
| /// In case it's not, |
| /// it takes care of removing the `<build-dir>/doc/` folder |
| /// as well as overwriting the rustdoc fingerprint info. |
| /// This is to guarantee that we won't end up with mixed versions of the `js/html/css` files |
| /// which `rustdoc` autogenerates without any versioning. |
| /// |
| /// Each requested target platform maintains its own fingerprint file. |
| /// That is, if you run `cargo doc` and then `cargo doc --target wasm32-wasip1`, |
| /// you will have two separate fingerprint files: |
| /// |
| /// * `<build-dir>/.rustdoc_fingerprint.json` for host |
| /// * `<build-dir>/wasm32-wasip1/.rustdoc_fingerprint.json` |
| pub fn check_rustdoc_fingerprint(build_runner: &BuildRunner<'_, '_>) -> CargoResult<()> { |
| if build_runner |
| .bcx |
| .gctx |
| .cli_unstable() |
| .skip_rustdoc_fingerprint |
| { |
| return Ok(()); |
| } |
| let new_fingerprint = RustdocFingerprintJson { |
| rustc_vv: build_runner.bcx.rustc().verbose_version.clone(), |
| doc_parts: Vec::new(), |
| }; |
| |
| for kind in &build_runner.bcx.build_config.requested_kinds { |
| check_fingerprint(build_runner, &new_fingerprint, *kind)?; |
| } |
| |
| Ok(()) |
| } |
| |
| /// Creates a new fingerprint with given doc parts paths. |
| pub fn new( |
| build_runner: &BuildRunner<'_, '_>, |
| kind: CompileKind, |
| doc_parts: Vec<PathBuf>, |
| ) -> Self { |
| let path = fingerprint_path(build_runner, kind); |
| let rustc_vv = build_runner.bcx.rustc().verbose_version.clone(); |
| let on_disk = load_on_disk(&path); |
| Self { |
| path, |
| rustc_vv, |
| doc_parts, |
| on_disk, |
| } |
| } |
| |
| /// Persists the fingerprint. |
| /// |
| /// The closure will run before persisting the fingerprint, |
| /// and will be given a list of doc parts directories for passing to |
| /// `rustdoc --read-doc-meta-dir`. |
| pub fn persist<F>(&self, exec: F) -> CargoResult<()> |
| where |
| // 1. paths for `--read-doc-meta-dir` |
| F: Fn(&[&Path]) -> CargoResult<()>, |
| { |
| // Dedupe crate with the same name by file stem (which is effectively crate name), |
| // since rustdoc doesn't distinguish different crate versions. |
| // |
| // Rules applied here: |
| // |
| // * If name collides, favor the one selected via CLI over cached ones |
| // (done by the insertion order) |
| let base = self.path.parent().unwrap(); |
| let on_disk_doc_parts: Vec<_> = self |
| .on_disk |
| .iter() |
| .flat_map(|on_disk| { |
| on_disk |
| .doc_parts |
| .iter() |
| // Make absolute so that we can pass to rustdoc |
| .map(|p| base.join(p)) |
| // Doc parts may be selectively cleaned by `cargo clean -p <doc>`. |
| // We should stop caching those no-exist. |
| .filter(|p| p.exists()) |
| }) |
| .collect(); |
| let dedup_map = on_disk_doc_parts |
| .iter() |
| .chain(self.doc_parts.iter()) |
| .map(|p| (p.file_stem(), p)) |
| .collect::<HashMap<_, _>>(); |
| let mut doc_parts: Vec<_> = dedup_map.into_values().collect(); |
| doc_parts.sort_unstable(); |
| |
| // Prepare args for `rustdoc --read-doc-meta-dir` |
| let doc_parts_dirs: Vec<_> = doc_parts.iter().map(|p| p.parent().unwrap()).collect(); |
| exec(&doc_parts_dirs)?; |
| |
| // Persist with relative paths to the directory where fingerprint file is at. |
| let json = RustdocFingerprintJson { |
| rustc_vv: self.rustc_vv.clone(), |
| doc_parts: doc_parts |
| .iter() |
| .map(|p| p.strip_prefix(base).unwrap_or(p).to_owned()) |
| .collect(), |
| }; |
| paths::write(&self.path, serde_json::to_string(&json)?)?; |
| |
| Ok(()) |
| } |
| |
| /// Checks if the fingerprint is outdated comparing against given doc parts file paths. |
| pub fn is_dirty(&self) -> bool { |
| let Some(on_disk) = self.on_disk.as_ref() else { |
| return true; |
| }; |
| |
| let Some(fingerprint_mtime) = paths::mtime(&self.path).ok() else { |
| return true; |
| }; |
| |
| if self.rustc_vv != on_disk.rustc_vv { |
| return true; |
| } |
| |
| for path in &self.doc_parts { |
| let parts_mtime = match paths::mtime(&path) { |
| Ok(mtime) => mtime, |
| Err(e) => { |
| tracing::debug!("failed to read mtime of {}: {e}", path.display()); |
| return true; |
| } |
| }; |
| |
| if parts_mtime > fingerprint_mtime { |
| return true; |
| } |
| } |
| |
| false |
| } |
| } |
| |
| /// Returns the path to rustdoc fingerprint file for a given [`CompileKind`]. |
| fn fingerprint_path(build_runner: &BuildRunner<'_, '_>, kind: CompileKind) -> PathBuf { |
| build_runner |
| .files() |
| .layout(kind) |
| .build_dir() |
| .root() |
| .join(".rustdoc_fingerprint.json") |
| } |
| |
| /// Checks rustdoc fingerprint file for a given [`CompileKind`]. |
| fn check_fingerprint( |
| build_runner: &BuildRunner<'_, '_>, |
| new_fingerprint: &RustdocFingerprintJson, |
| kind: CompileKind, |
| ) -> CargoResult<()> { |
| let fingerprint_path = fingerprint_path(build_runner, kind); |
| |
| let write_fingerprint = || -> CargoResult<()> { |
| paths::write(&fingerprint_path, serde_json::to_string(new_fingerprint)?) |
| }; |
| |
| let Ok(rustdoc_data) = paths::read(&fingerprint_path) else { |
| // If the fingerprint does not exist, do not clear out the doc |
| // directories. Otherwise this ran into problems where projects |
| // like bootstrap were creating the doc directory before running |
| // `cargo doc` in a way that deleting it would break it. |
| return write_fingerprint(); |
| }; |
| |
| match serde_json::from_str::<RustdocFingerprintJson>(&rustdoc_data) { |
| Ok(on_disk_fingerprint) => { |
| if on_disk_fingerprint.rustc_vv == new_fingerprint.rustc_vv { |
| return Ok(()); |
| } else { |
| tracing::debug!( |
| "doc fingerprint changed:\noriginal:\n{}\nnew:\n{}", |
| on_disk_fingerprint.rustc_vv, |
| new_fingerprint.rustc_vv |
| ); |
| } |
| } |
| Err(e) => { |
| tracing::debug!("could not deserialize {:?}: {}", fingerprint_path, e); |
| } |
| }; |
| // Fingerprint does not match, delete the doc directories and write a new fingerprint. |
| tracing::debug!( |
| "fingerprint {:?} mismatch, clearing doc directories", |
| fingerprint_path |
| ); |
| let doc_dir = build_runner |
| .files() |
| .layout(kind) |
| .artifact_dir() |
| .expect("artifact-dir was not locked") |
| .doc(); |
| if doc_dir.exists() { |
| clean_doc(doc_dir)?; |
| } |
| |
| write_fingerprint()?; |
| |
| Ok(()) |
| } |
| |
| /// Loads an on-disk fingerprint JSON file. |
| fn load_on_disk(path: &Path) -> Option<RustdocFingerprintJson> { |
| let on_disk = match paths::read(path) { |
| Ok(data) => data, |
| Err(e) => { |
| tracing::debug!("failed to read rustdoc fingerprint at {path:?}: {e}"); |
| return None; |
| } |
| }; |
| |
| match serde_json::from_str::<RustdocFingerprintJson>(&on_disk) { |
| Ok(on_disk) => Some(on_disk), |
| Err(e) => { |
| tracing::debug!("could not deserialize {path:?}: {e}"); |
| None |
| } |
| } |
| } |
| |
| fn clean_doc(path: &Path) -> CargoResult<()> { |
| let entries = path |
| .read_dir() |
| .with_context(|| format!("failed to read directory `{}`", path.display()))?; |
| for entry in entries { |
| let entry = entry?; |
| // Don't remove hidden files. Rustdoc does not create them, |
| // but the user might have. |
| if entry |
| .file_name() |
| .to_str() |
| .map_or(false, |name| name.starts_with('.')) |
| { |
| continue; |
| } |
| let path = entry.path(); |
| if entry.file_type()?.is_dir() { |
| paths::remove_dir_all(path)?; |
| } else { |
| paths::remove_file(path)?; |
| } |
| } |
| Ok(()) |
| } |