blob: 11336b55ee6feb983cbddd18b6ac6d50aac8f5a6 [file] [edit]
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(())
}