From bdb5046913b04ede2b9655acb62c2c2228d21193 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 2 Dec 2025 15:53:46 -0500 Subject: [PATCH 1/6] refactor: move RustDocFingerprint to fingerprint mod --- src/cargo/core/compiler/build_context/mod.rs | 7 +- .../compiler/build_context/target_info.rs | 123 ++---------------- src/cargo/core/compiler/fingerprint/mod.rs | 2 + .../core/compiler/fingerprint/rustdoc.rs | 119 +++++++++++++++++ src/cargo/core/compiler/mod.rs | 9 +- 5 files changed, 140 insertions(+), 120 deletions(-) create mode 100644 src/cargo/core/compiler/fingerprint/rustdoc.rs diff --git a/src/cargo/core/compiler/build_context/mod.rs b/src/cargo/core/compiler/build_context/mod.rs index 90ba0907e4c..355e32c1663 100644 --- a/src/cargo/core/compiler/build_context/mod.rs +++ b/src/cargo/core/compiler/build_context/mod.rs @@ -13,9 +13,10 @@ use crate::util::logger::BuildLogger; use std::collections::{HashMap, HashSet}; mod target_info; -pub use self::target_info::{ - FileFlavor, FileType, RustDocFingerprint, RustcTargetData, TargetInfo, -}; +pub use self::target_info::FileFlavor; +pub use self::target_info::FileType; +pub use self::target_info::RustcTargetData; +pub use self::target_info::TargetInfo; /// The build context, containing complete information needed for a build task /// before it gets started. diff --git a/src/cargo/core/compiler/build_context/target_info.rs b/src/cargo/core/compiler/build_context/target_info.rs index 76e387c579c..a997ef2c38a 100644 --- a/src/cargo/core/compiler/build_context/target_info.rs +++ b/src/cargo/core/compiler/build_context/target_info.rs @@ -7,19 +7,24 @@ //! * [`RustcTargetData::info`] to get a [`TargetInfo`] for an in-depth query. //! * [`TargetInfo::rustc_outputs`] to get a list of supported file types. +use crate::core::compiler::CompileKind; +use crate::core::compiler::CompileMode; +use crate::core::compiler::CompileTarget; +use crate::core::compiler::CrateType; use crate::core::compiler::apply_env_config; -use crate::core::compiler::{BuildRunner, CompileKind, CompileMode, CompileTarget, CrateType}; use crate::core::{Dependency, Package, Target, TargetKind, Workspace}; use crate::util::context::{GlobalContext, StringList, TargetConfig}; use crate::util::interning::InternedString; use crate::util::{CargoResult, Rustc}; + use anyhow::Context as _; use cargo_platform::{Cfg, CfgExpr}; -use cargo_util::{ProcessBuilder, paths}; -use serde::{Deserialize, Serialize}; +use cargo_util::ProcessBuilder; +use serde::Deserialize; + use std::cell::RefCell; use std::collections::hash_map::{Entry, HashMap}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::rc::Rc; use std::str::{self, FromStr}; @@ -1110,113 +1115,3 @@ impl<'gctx> RustcTargetData<'gctx> { &self.requested_kinds } } - -/// Structure used to deal with Rustdoc fingerprinting -#[derive(Debug, Serialize, Deserialize)] -pub struct RustDocFingerprint { - pub rustc_vv: String, -} - -impl RustDocFingerprint { - /// This function 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 `doc/` folder as well as overwriting - /// the rustdoc fingerprint info in order to guarantee that we won't end up with mixed - /// versions of the `js/html/css` files that `rustdoc` autogenerates which do not have - /// any versioning. - pub fn check_rustdoc_fingerprint(build_runner: &BuildRunner<'_, '_>) -> CargoResult<()> { - if build_runner - .bcx - .gctx - .cli_unstable() - .skip_rustdoc_fingerprint - { - return Ok(()); - } - let actual_rustdoc_target_data = RustDocFingerprint { - rustc_vv: build_runner.bcx.rustc().verbose_version.clone(), - }; - - let fingerprint_path = build_runner - .files() - .host_build_root() - .join(".rustdoc_fingerprint.json"); - let write_fingerprint = || -> CargoResult<()> { - paths::write( - &fingerprint_path, - serde_json::to_string(&actual_rustdoc_target_data)?, - ) - }; - 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::(&rustdoc_data) { - Ok(fingerprint) => { - if fingerprint.rustc_vv == actual_rustdoc_target_data.rustc_vv { - return Ok(()); - } else { - tracing::debug!( - "doc fingerprint changed:\noriginal:\n{}\nnew:\n{}", - fingerprint.rustc_vv, - actual_rustdoc_target_data.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 - ); - build_runner - .bcx - .all_kinds - .iter() - .map(|kind| { - build_runner - .files() - .layout(*kind) - .artifact_dir() - .expect("artifact-dir was not locked") - .doc() - }) - .filter(|path| path.exists()) - .try_for_each(|path| clean_doc(path))?; - write_fingerprint()?; - return Ok(()); - - 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(()) - } - } -} diff --git a/src/cargo/core/compiler/fingerprint/mod.rs b/src/cargo/core/compiler/fingerprint/mod.rs index 3cc431e1f93..428743b74cf 100644 --- a/src/cargo/core/compiler/fingerprint/mod.rs +++ b/src/cargo/core/compiler/fingerprint/mod.rs @@ -371,6 +371,7 @@ mod dep_info; mod dirty_reason; +mod rustdoc; use std::collections::hash_map::{Entry, HashMap}; use std::env; @@ -409,6 +410,7 @@ pub use self::dep_info::parse_dep_info; pub use self::dep_info::parse_rustc_dep_info; pub use self::dep_info::translate_dep_info; pub use self::dirty_reason::DirtyReason; +pub use self::rustdoc::RustDocFingerprint; /// Determines if a [`Unit`] is up-to-date, and if not prepares necessary work to /// update the persisted fingerprint. diff --git a/src/cargo/core/compiler/fingerprint/rustdoc.rs b/src/cargo/core/compiler/fingerprint/rustdoc.rs new file mode 100644 index 00000000000..f888c301b49 --- /dev/null +++ b/src/cargo/core/compiler/fingerprint/rustdoc.rs @@ -0,0 +1,119 @@ +use std::path::Path; + +use anyhow::Context as _; +use cargo_util::paths; +use serde::Deserialize; +use serde::Serialize; + +use crate::CargoResult; +use crate::core::compiler::BuildRunner; + +/// Structure used to deal with Rustdoc fingerprinting +#[derive(Debug, Serialize, Deserialize)] +pub struct RustDocFingerprint { + pub rustc_vv: String, +} + +impl RustDocFingerprint { + /// This function 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 `doc/` folder as well as overwriting + /// the rustdoc fingerprint info in order to guarantee that we won't end up with mixed + /// versions of the `js/html/css` files that `rustdoc` autogenerates which do not have + /// any versioning. + pub fn check_rustdoc_fingerprint(build_runner: &BuildRunner<'_, '_>) -> CargoResult<()> { + if build_runner + .bcx + .gctx + .cli_unstable() + .skip_rustdoc_fingerprint + { + return Ok(()); + } + let actual_rustdoc_target_data = RustDocFingerprint { + rustc_vv: build_runner.bcx.rustc().verbose_version.clone(), + }; + + let fingerprint_path = build_runner + .files() + .host_build_root() + .join(".rustdoc_fingerprint.json"); + let write_fingerprint = || -> CargoResult<()> { + paths::write( + &fingerprint_path, + serde_json::to_string(&actual_rustdoc_target_data)?, + ) + }; + 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::(&rustdoc_data) { + Ok(fingerprint) => { + if fingerprint.rustc_vv == actual_rustdoc_target_data.rustc_vv { + return Ok(()); + } else { + tracing::debug!( + "doc fingerprint changed:\noriginal:\n{}\nnew:\n{}", + fingerprint.rustc_vv, + actual_rustdoc_target_data.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 + ); + build_runner + .bcx + .all_kinds + .iter() + .map(|kind| { + build_runner + .files() + .layout(*kind) + .artifact_dir() + .expect("artifact-dir was not locked") + .doc() + }) + .filter(|path| path.exists()) + .try_for_each(|path| clean_doc(path))?; + write_fingerprint()?; + return Ok(()); + + 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(()) + } + } +} diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 3466baafb05..62eb3df4d63 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -72,9 +72,11 @@ use tracing::{debug, instrument, trace}; pub use self::build_config::UserIntent; pub use self::build_config::{BuildConfig, CompileMode, MessageFormat, TimingOutput}; -pub use self::build_context::{ - BuildContext, FileFlavor, FileType, RustDocFingerprint, RustcTargetData, TargetInfo, -}; +pub use self::build_context::BuildContext; +pub use self::build_context::FileFlavor; +pub use self::build_context::FileType; +pub use self::build_context::RustcTargetData; +pub use self::build_context::TargetInfo; pub use self::build_runner::{BuildRunner, Metadata, UnitHash}; pub use self::compilation::{Compilation, Doctest, UnitOutput}; pub use self::compile_kind::{CompileKind, CompileKindFallback, CompileTarget}; @@ -82,6 +84,7 @@ pub use self::crate_type::CrateType; pub use self::custom_build::LinkArgTarget; pub use self::custom_build::{BuildOutput, BuildScriptOutputs, BuildScripts, LibraryPath}; pub(crate) use self::fingerprint::DirtyReason; +pub use self::fingerprint::RustDocFingerprint; pub use self::job_queue::Freshness; use self::job_queue::{Job, JobQueue, JobState, Work}; pub(crate) use self::layout::Layout; From 95b8e3ce660ebffb35ae0d307bf28b834ec6b173 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 2 Dec 2025 16:01:59 -0500 Subject: [PATCH 2/6] docs(fingerprint): reorganize rustdoc fingerprint docs --- src/cargo/core/compiler/build_runner/mod.rs | 11 +++----- .../core/compiler/fingerprint/rustdoc.rs | 25 +++++++++++++------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/cargo/core/compiler/build_runner/mod.rs b/src/cargo/core/compiler/build_runner/mod.rs index d1ae513238e..9d0533f7fce 100644 --- a/src/cargo/core/compiler/build_runner/mod.rs +++ b/src/cargo/core/compiler/build_runner/mod.rs @@ -174,14 +174,9 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { self.check_collisions()?; self.compute_metadata_for_doc_units(); - // We need to make sure that if there were any previous docs - // already compiled, they were compiled with the same Rustc version that we're currently - // using. Otherwise we must remove the `doc/` folder and compile again forcing a rebuild. - // - // This is important because the `.js`/`.html` & `.css` files that are generated by Rustc don't have - // any versioning (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 Rustc version that we're currently using. + // See the function doc comment for more. if self.bcx.build_config.intent.is_doc() { RustDocFingerprint::check_rustdoc_fingerprint(&self)? } diff --git a/src/cargo/core/compiler/fingerprint/rustdoc.rs b/src/cargo/core/compiler/fingerprint/rustdoc.rs index f888c301b49..98730cf9031 100644 --- a/src/cargo/core/compiler/fingerprint/rustdoc.rs +++ b/src/cargo/core/compiler/fingerprint/rustdoc.rs @@ -9,20 +9,31 @@ use crate::CargoResult; use crate::core::compiler::BuildRunner; /// Structure used to deal with Rustdoc fingerprinting +/// +/// This is important because the `.js`/`.html` & `.css` files +/// that are generated by Rustc don't have any versioning yet +/// (see ). +/// 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 Rustc version that we're currently using. +/// Otherwise we must remove the `doc/` folder and compile again forcing a rebuild. #[derive(Debug, Serialize, Deserialize)] pub struct RustDocFingerprint { + /// `rustc -vV` verbose version output. pub rustc_vv: String, } impl RustDocFingerprint { - /// This function 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. + /// 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 `doc/` folder as well as overwriting - /// the rustdoc fingerprint info in order to guarantee that we won't end up with mixed - /// versions of the `js/html/css` files that `rustdoc` autogenerates which do not have - /// any versioning. + /// In case it's not, + /// it takes care of removing the `/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. pub fn check_rustdoc_fingerprint(build_runner: &BuildRunner<'_, '_>) -> CargoResult<()> { if build_runner .bcx From 874d482347fac15a91c3edba4339f08692f0a2f8 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 2 Dec 2025 16:18:52 -0500 Subject: [PATCH 3/6] refactor(fingerprint): move out inner `clean_doc` --- .../core/compiler/fingerprint/rustdoc.rs | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/cargo/core/compiler/fingerprint/rustdoc.rs b/src/cargo/core/compiler/fingerprint/rustdoc.rs index 98730cf9031..965ead794b3 100644 --- a/src/cargo/core/compiler/fingerprint/rustdoc.rs +++ b/src/cargo/core/compiler/fingerprint/rustdoc.rs @@ -100,31 +100,32 @@ impl RustDocFingerprint { .filter(|path| path.exists()) .try_for_each(|path| clean_doc(path))?; write_fingerprint()?; - return Ok(()); - 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(()) + Ok(()) + } +} + +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(()) } From 21ba21352294f3caf6a88bb5a042105df6c082b8 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 2 Dec 2025 18:31:01 -0500 Subject: [PATCH 4/6] test(rustdoc): show only one fingerprint for all targets --- tests/testsuite/doc.rs | 91 ++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 56 deletions(-) diff --git a/tests/testsuite/doc.rs b/tests/testsuite/doc.rs index 88d4f27c0ed..72df61648c5 100644 --- a/tests/testsuite/doc.rs +++ b/tests/testsuite/doc.rs @@ -2548,6 +2548,7 @@ LLVM version: 9.0 #[cargo_test] fn doc_fingerprint_respects_target_paths() { + let host = rustc_host(); // Random rustc verbose version let old_rustc_verbose_version = format!( "\ @@ -2555,88 +2556,66 @@ rustc 1.41.1 (f3e1a954d 2020-02-24) binary: rustc commit-hash: f3e1a954d2ead4e2fc197c7da7d71e6c61bad196 commit-date: 2020-02-24 -host: {} +host: {host} release: 1.41.1 LLVM version: 9.0 ", - rustc_host() ); - // Create the dummy project. - let dummy_project = project() + let p = project() .file( "Cargo.toml", r#" [package] name = "foo" - version = "1.2.4" - edition = "2015" - authors = [] + edition = "2021" "#, ) .file("src/lib.rs", "//! These are the docs!") .build(); - dummy_project.cargo("doc --target").arg(rustc_host()).run(); + // generate `target/doc` and `target//doc + p.cargo("doc --target").arg(host).run(); + p.cargo("doc").run(); - let fingerprint: RustDocFingerprint = - serde_json::from_str(&dummy_project.read_file("target/.rustdoc_fingerprint.json")) + let host_fingerprint_path = p.build_dir().join(".rustdoc_fingerprint.json"); + + let target_fingerprint_path = p.build_dir().join(host).join(".rustdoc_fingerprint.json"); + + let host_fingerprint: RustDocFingerprint = + serde_json::from_str(&fs::read_to_string(&host_fingerprint_path).unwrap()) .expect("JSON Serde fail"); + assert!(!target_fingerprint_path.exists()); + // Check that the fingerprint contains the actual rustc version // which has been used to compile the docs. - let output = std::process::Command::new("rustc") - .arg("-vV") - .output() - .expect("Failed to get actual rustc verbose version"); - assert_eq!( - fingerprint.rustc_vv, - (String::from_utf8_lossy(&output.stdout).as_ref()) - ); - - // As the test shows above. Now we have generated the `doc/` folder and inside - // the rustdoc fingerprint file is located with the correct rustc version. - // So we will remove it and create a new fingerprint with an old rustc version - // inside it. We will also place a bogus file inside of the `doc/` folder to ensure - // it gets removed as we expect on the next doc compilation. - dummy_project.change_file( - "target/.rustdoc_fingerprint.json", - &old_rustc_verbose_version, - ); + let current_rustc_version = String::from_utf8( + std::process::Command::new("rustc") + .arg("-vV") + .output() + .unwrap() + .stdout, + ) + .unwrap(); + assert_eq!(&host_fingerprint.rustc_vv, ¤t_rustc_version); + // Write random `rustc -vV` output and bogus file for host + fs::write(&host_fingerprint_path, &old_rustc_verbose_version).unwrap(); fs::write( - dummy_project - .build_dir() - .join(rustc_host()) - .join("doc/bogus_file"), - String::from("This is a bogus file and should be removed!"), + p.build_dir().join("doc/bogus_file"), + "This is a bogus file and should be removed!", ) - .expect("Error writing test bogus file"); - - // Now if we trigger another compilation, since the fingerprint contains an old version - // of rustc, cargo should remove the entire `/doc` folder (including the fingerprint) - // and generating another one with the actual version. - // It should also remove the bogus file we created above. - dummy_project.cargo("doc --target").arg(rustc_host()).run(); + .unwrap(); - assert!( - !dummy_project - .build_dir() - .join(rustc_host()) - .join("doc/bogus_file") - .exists() - ); + // ...but run only target + p.cargo("doc --target").arg(host).run(); + // host doc dir got cleaned + assert!(!p.build_dir().join("doc/bogus_file").exists()); let fingerprint: RustDocFingerprint = - serde_json::from_str(&dummy_project.read_file("target/.rustdoc_fingerprint.json")) - .expect("JSON Serde fail"); - - // Check that the fingerprint contains the actual rustc version - // which has been used to compile the docs. - assert_eq!( - fingerprint.rustc_vv, - (String::from_utf8_lossy(&output.stdout).as_ref()) - ); + serde_json::from_str(&fs::read_to_string(&host_fingerprint_path).unwrap()).unwrap(); + assert_eq!(&fingerprint.rustc_vv, ¤t_rustc_version); } #[cargo_test] From fac0ff7459bb425e8fcab2e296248b22ecbde357 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 2 Dec 2025 22:41:06 -0500 Subject: [PATCH 5/6] refactor(fingerprint): extract rustdoc fingerprint check This is a preparation for per-target check. Currently it checks only host build-dir for the entire workspace --- .../core/compiler/fingerprint/rustdoc.rs | 116 ++++++++++-------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/src/cargo/core/compiler/fingerprint/rustdoc.rs b/src/cargo/core/compiler/fingerprint/rustdoc.rs index 965ead794b3..9c859d6ad06 100644 --- a/src/cargo/core/compiler/fingerprint/rustdoc.rs +++ b/src/cargo/core/compiler/fingerprint/rustdoc.rs @@ -47,64 +47,74 @@ impl RustDocFingerprint { rustc_vv: build_runner.bcx.rustc().verbose_version.clone(), }; - let fingerprint_path = build_runner - .files() - .host_build_root() - .join(".rustdoc_fingerprint.json"); - let write_fingerprint = || -> CargoResult<()> { - paths::write( - &fingerprint_path, - serde_json::to_string(&actual_rustdoc_target_data)?, - ) - }; - 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::(&rustdoc_data) { - Ok(fingerprint) => { - if fingerprint.rustc_vv == actual_rustdoc_target_data.rustc_vv { - return Ok(()); - } else { - tracing::debug!( - "doc fingerprint changed:\noriginal:\n{}\nnew:\n{}", - fingerprint.rustc_vv, - actual_rustdoc_target_data.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 - ); - build_runner - .bcx - .all_kinds - .iter() - .map(|kind| { - build_runner - .files() - .layout(*kind) - .artifact_dir() - .expect("artifact-dir was not locked") - .doc() - }) - .filter(|path| path.exists()) - .try_for_each(|path| clean_doc(path))?; - write_fingerprint()?; + check_fingerprint(build_runner, &actual_rustdoc_target_data)?; Ok(()) } } +/// Checks rustdoc fingerprint file. +fn check_fingerprint( + build_runner: &BuildRunner<'_, '_>, + actual_rustdoc_target_data: &RustDocFingerprint, +) -> CargoResult<()> { + let fingerprint_path = build_runner + .files() + .host_build_root() + .join(".rustdoc_fingerprint.json"); + let write_fingerprint = || -> CargoResult<()> { + paths::write( + &fingerprint_path, + serde_json::to_string(&actual_rustdoc_target_data)?, + ) + }; + 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::(&rustdoc_data) { + Ok(fingerprint) => { + if fingerprint.rustc_vv == actual_rustdoc_target_data.rustc_vv { + return Ok(()); + } else { + tracing::debug!( + "doc fingerprint changed:\noriginal:\n{}\nnew:\n{}", + fingerprint.rustc_vv, + actual_rustdoc_target_data.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 + ); + build_runner + .bcx + .all_kinds + .iter() + .map(|kind| { + build_runner + .files() + .layout(*kind) + .artifact_dir() + .expect("artifact-dir was not locked") + .doc() + }) + .filter(|path| path.exists()) + .try_for_each(|path| clean_doc(path))?; + write_fingerprint()?; + + Ok(()) +} + fn clean_doc(path: &Path) -> CargoResult<()> { let entries = path .read_dir() From 03abcfba82e6ea09697e9424bb7da00c173c5e86 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 2 Dec 2025 22:43:29 -0500 Subject: [PATCH 6/6] fix(fingerprint): clean doc dirs for only requested targets Previously if rustc version mismatches, this removes all `doc` directories including target platforms that are not part of the build. This makes it `--target` aware and stops excessive cleanup, by putting `.rustdoc_fingerprint.json` in each target directory. --- .../core/compiler/fingerprint/rustdoc.rs | 65 +++++++++++-------- tests/testsuite/doc.rs | 27 ++++++-- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/cargo/core/compiler/fingerprint/rustdoc.rs b/src/cargo/core/compiler/fingerprint/rustdoc.rs index 9c859d6ad06..0bb9a16b636 100644 --- a/src/cargo/core/compiler/fingerprint/rustdoc.rs +++ b/src/cargo/core/compiler/fingerprint/rustdoc.rs @@ -7,6 +7,7 @@ use serde::Serialize; use crate::CargoResult; use crate::core::compiler::BuildRunner; +use crate::core::compiler::CompileKind; /// Structure used to deal with Rustdoc fingerprinting /// @@ -30,10 +31,17 @@ impl RustDocFingerprint { /// 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 `/doc/` folder + /// it takes care of removing the `/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: + /// + /// * `/.rustdoc_fingerprint.json` for host + /// * `/wasm32-wasip1/.rustdoc_fingerprint.json` pub fn check_rustdoc_fingerprint(build_runner: &BuildRunner<'_, '_>) -> CargoResult<()> { if build_runner .bcx @@ -43,31 +51,35 @@ impl RustDocFingerprint { { return Ok(()); } - let actual_rustdoc_target_data = RustDocFingerprint { + let new_fingerprint = RustDocFingerprint { rustc_vv: build_runner.bcx.rustc().verbose_version.clone(), }; - check_fingerprint(build_runner, &actual_rustdoc_target_data)?; + for kind in &build_runner.bcx.build_config.requested_kinds { + check_fingerprint(build_runner, &new_fingerprint, *kind)?; + } Ok(()) } } -/// Checks rustdoc fingerprint file. +/// Checks rustdoc fingerprint file for a given [`CompileKind`]. fn check_fingerprint( build_runner: &BuildRunner<'_, '_>, - actual_rustdoc_target_data: &RustDocFingerprint, + new_fingerprint: &RustDocFingerprint, + kind: CompileKind, ) -> CargoResult<()> { let fingerprint_path = build_runner .files() - .host_build_root() + .layout(kind) + .build_dir() + .root() .join(".rustdoc_fingerprint.json"); + let write_fingerprint = || -> CargoResult<()> { - paths::write( - &fingerprint_path, - serde_json::to_string(&actual_rustdoc_target_data)?, - ) + 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 @@ -75,15 +87,16 @@ fn check_fingerprint( // `cargo doc` in a way that deleting it would break it. return write_fingerprint(); }; + match serde_json::from_str::(&rustdoc_data) { - Ok(fingerprint) => { - if fingerprint.rustc_vv == actual_rustdoc_target_data.rustc_vv { + 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{}", - fingerprint.rustc_vv, - actual_rustdoc_target_data.rustc_vv + on_disk_fingerprint.rustc_vv, + new_fingerprint.rustc_vv ); } } @@ -96,20 +109,16 @@ fn check_fingerprint( "fingerprint {:?} mismatch, clearing doc directories", fingerprint_path ); - build_runner - .bcx - .all_kinds - .iter() - .map(|kind| { - build_runner - .files() - .layout(*kind) - .artifact_dir() - .expect("artifact-dir was not locked") - .doc() - }) - .filter(|path| path.exists()) - .try_for_each(|path| clean_doc(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(()) diff --git a/tests/testsuite/doc.rs b/tests/testsuite/doc.rs index 72df61648c5..871e7be31d4 100644 --- a/tests/testsuite/doc.rs +++ b/tests/testsuite/doc.rs @@ -2586,7 +2586,11 @@ LLVM version: 9.0 serde_json::from_str(&fs::read_to_string(&host_fingerprint_path).unwrap()) .expect("JSON Serde fail"); - assert!(!target_fingerprint_path.exists()); + let target_fingerprint: RustDocFingerprint = + serde_json::from_str(&fs::read_to_string(&target_fingerprint_path).unwrap()) + .expect("JSON Serde fail"); + + assert_eq!(host_fingerprint.rustc_vv, target_fingerprint.rustc_vv); // Check that the fingerprint contains the actual rustc version // which has been used to compile the docs. @@ -2600,8 +2604,14 @@ LLVM version: 9.0 .unwrap(); assert_eq!(&host_fingerprint.rustc_vv, ¤t_rustc_version); - // Write random `rustc -vV` output and bogus file for host + // Write random `rustc -vV` output and bogus file for both host and target fs::write(&host_fingerprint_path, &old_rustc_verbose_version).unwrap(); + fs::write(&target_fingerprint_path, &old_rustc_verbose_version).unwrap(); + fs::write( + p.build_dir().join(host).join("doc/bogus_file"), + "This is a bogus file and should be removed!", + ) + .unwrap(); fs::write( p.build_dir().join("doc/bogus_file"), "This is a bogus file and should be removed!", @@ -2611,10 +2621,17 @@ LLVM version: 9.0 // ...but run only target p.cargo("doc --target").arg(host).run(); - // host doc dir got cleaned - assert!(!p.build_dir().join("doc/bogus_file").exists()); + // host doc dir stays the same, and the fingerprint is still the old random one + assert!(p.build_dir().join("doc/bogus_file").exists()); + assert_eq!( + &fs::read_to_string(&host_fingerprint_path).unwrap(), + &old_rustc_verbose_version, + ); + + // target doc dir got cleaned + assert!(!p.build_dir().join(host).join("doc/bogus_file").exists()); let fingerprint: RustDocFingerprint = - serde_json::from_str(&fs::read_to_string(&host_fingerprint_path).unwrap()).unwrap(); + serde_json::from_str(&fs::read_to_string(&target_fingerprint_path).unwrap()).unwrap(); assert_eq!(&fingerprint.rustc_vv, ¤t_rustc_version); }