Skip to content

Commit 8fa0827

Browse files
committed
Auto merge of #8640 - CPerezz:doc_versioning, r=ehuss
Implement Rustdoc versioning checks Before compiling, 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. Otherways we must remove the `doc/` folder and compile again. This is important because as stated in #8461 the .js/.html&.css files that are generated by Rustc don't have any versioning. Therefore, we can fall in weird bugs and behaviours if we mix different compiler versions of these js/.html&.css files. Closes #8461
2 parents b842849 + 61c2332 commit 8fa0827

File tree

5 files changed

+278
-7
lines changed

5 files changed

+278
-7
lines changed

src/cargo/core/compiler/build_context/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use std::collections::{HashMap, HashSet};
1111
use std::path::PathBuf;
1212

1313
mod target_info;
14-
pub use self::target_info::{FileFlavor, FileType, RustcTargetData, TargetInfo};
14+
pub use self::target_info::{
15+
FileFlavor, FileType, RustDocFingerprint, RustcTargetData, TargetInfo,
16+
};
1517

1618
/// The build context, containing all information about a build task.
1719
///

src/cargo/core/compiler/build_context/target_info.rs

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
use crate::core::compiler::{BuildOutput, CompileKind, CompileMode, CompileTarget, CrateType};
1+
use crate::core::compiler::{
2+
BuildOutput, CompileKind, CompileMode, CompileTarget, Context, CrateType,
3+
};
24
use crate::core::{Dependency, Target, TargetKind, Workspace};
35
use crate::util::config::{Config, StringList, TargetConfig};
4-
use crate::util::{CargoResult, CargoResultExt, ProcessBuilder, Rustc};
6+
use crate::util::{paths, CargoResult, CargoResultExt, ProcessBuilder, Rustc};
57
use cargo_platform::{Cfg, CfgExpr};
8+
use serde::{Deserialize, Serialize};
69
use std::cell::RefCell;
710
use std::collections::hash_map::{Entry, HashMap};
811
use std::env;
9-
use std::path::PathBuf;
12+
use std::path::{Path, PathBuf};
1013
use std::str::{self, FromStr};
1114

1215
/// Information about the platform target gleaned from querying rustc.
@@ -754,3 +757,77 @@ impl RustcTargetData {
754757
self.target_config(kind).links_overrides.get(lib_name)
755758
}
756759
}
760+
761+
/// Structure used to deal with Rustdoc fingerprinting
762+
#[derive(Debug, Serialize, Deserialize)]
763+
pub struct RustDocFingerprint {
764+
pub rustc_vv: String,
765+
}
766+
767+
impl RustDocFingerprint {
768+
/// Read the `RustDocFingerprint` info from the fingerprint file.
769+
fn read<'a, 'cfg>(cx: &Context<'a, 'cfg>) -> CargoResult<Self> {
770+
let rustdoc_data = paths::read(&cx.files().host_root().join(".rustdoc_fingerprint.json"))?;
771+
serde_json::from_str(&rustdoc_data).map_err(|e| anyhow::anyhow!("{:?}", e))
772+
}
773+
774+
/// Write the `RustDocFingerprint` info into the fingerprint file.
775+
fn write<'a, 'cfg>(&self, cx: &Context<'a, 'cfg>) -> CargoResult<()> {
776+
paths::write(
777+
&cx.files().host_root().join(".rustdoc_fingerprint.json"),
778+
serde_json::to_string(&self)?.as_bytes(),
779+
)
780+
}
781+
782+
fn remove_doc_dirs(doc_dirs: &Vec<&Path>) -> CargoResult<()> {
783+
doc_dirs
784+
.iter()
785+
.filter(|path| path.exists())
786+
.map(|path| paths::remove_dir_all(&path))
787+
.collect::<CargoResult<()>>()
788+
}
789+
790+
/// This function checks whether the latest version of `Rustc` used to compile this
791+
/// `Workspace`'s docs was the same as the one is currently being used in this `cargo doc`
792+
/// call.
793+
///
794+
/// In case it's not, it takes care of removing the `doc/` folder as well as overwriting
795+
/// the rustdoc fingerprint info in order to guarantee that we won't end up with mixed
796+
/// versions of the `js/html/css` files that `rustdoc` autogenerates which do not have
797+
/// any versioning.
798+
pub fn check_rustdoc_fingerprint<'a, 'cfg>(cx: &Context<'a, 'cfg>) -> CargoResult<()> {
799+
let actual_rustdoc_target_data = RustDocFingerprint {
800+
rustc_vv: cx.bcx.rustc().verbose_version.clone(),
801+
};
802+
803+
// Collect all of the target doc paths for which the docs need to be compiled for.
804+
let doc_dirs: Vec<&Path> = cx
805+
.bcx
806+
.all_kinds
807+
.iter()
808+
.map(|kind| cx.files().layout(*kind).doc())
809+
.collect();
810+
811+
// Check wether `.rustdoc_fingerprint.json` exists
812+
match Self::read(cx) {
813+
Ok(fingerprint) => {
814+
// Check if rustc_version matches the one we just used. Otherways,
815+
// remove the `doc` folder to trigger a re-compilation of the docs.
816+
if fingerprint.rustc_vv != actual_rustdoc_target_data.rustc_vv {
817+
Self::remove_doc_dirs(&doc_dirs)?;
818+
actual_rustdoc_target_data.write(cx)?
819+
}
820+
}
821+
// If the file does not exist, then we cannot assume that the docs were compiled
822+
// with the actual Rustc instance version. Therefore, we try to remove the
823+
// `doc` directory forcing the recompilation of the docs. If the directory doesn't
824+
// exists neither, we simply do nothing and continue.
825+
Err(_) => {
826+
// We don't care if this succeeds as explained above.
827+
let _ = Self::remove_doc_dirs(&doc_dirs);
828+
actual_rustdoc_target_data.write(cx)?
829+
}
830+
}
831+
Ok(())
832+
}
833+
}

src/cargo/core/compiler/context/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use super::job_queue::JobQueue;
1818
use super::layout::Layout;
1919
use super::lto::Lto;
2020
use super::unit_graph::UnitDep;
21-
use super::{BuildContext, Compilation, CompileKind, CompileMode, Executor, FileFlavor};
21+
use super::{
22+
BuildContext, Compilation, CompileKind, CompileMode, Executor, FileFlavor, RustDocFingerprint,
23+
};
2224

2325
mod compilation_files;
2426
use self::compilation_files::CompilationFiles;
@@ -133,6 +135,18 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
133135
custom_build::build_map(&mut self)?;
134136
self.check_collisions()?;
135137

138+
// We need to make sure that if there were any previous docs
139+
// already compiled, they were compiled with the same Rustc version that we're currently
140+
// using. Otherways we must remove the `doc/` folder and compile again forcing a rebuild.
141+
//
142+
// This is important because the `.js`/`.html` & `.css` files that are generated by Rustc don't have
143+
// any versioning (See https://github.com/rust-lang/cargo/issues/8461).
144+
// Therefore, we can end up with weird bugs and behaviours if we mix different
145+
// versions of these files.
146+
if self.bcx.build_config.mode.is_doc() {
147+
RustDocFingerprint::check_rustdoc_fingerprint(&self)?
148+
}
149+
136150
for unit in &self.bcx.roots {
137151
// Build up a list of pending jobs, each of which represent
138152
// compiling a particular package. No actual work is executed as

src/cargo/core/compiler/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ use lazycell::LazyCell;
3232
use log::debug;
3333

3434
pub use self::build_config::{BuildConfig, CompileMode, MessageFormat};
35-
pub use self::build_context::{BuildContext, FileFlavor, FileType, RustcTargetData, TargetInfo};
35+
pub use self::build_context::{
36+
BuildContext, FileFlavor, FileType, RustDocFingerprint, RustcTargetData, TargetInfo,
37+
};
3638
use self::build_plan::BuildPlan;
3739
pub use self::compilation::{Compilation, Doctest, UnitOutput};
3840
pub use self::compile_kind::{CompileKind, CompileTarget};
@@ -586,7 +588,6 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
586588
if let CompileKind::Target(target) = unit.kind {
587589
rustdoc.arg("--target").arg(target.rustc_target());
588590
}
589-
590591
let doc_dir = cx.files().out_dir(unit);
591592

592593
// Create the documentation directory ahead of time as rustdoc currently has

tests/testsuite/doc.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Tests for the `cargo doc` command.
22
3+
use cargo::core::compiler::RustDocFingerprint;
34
use cargo_test_support::paths::CargoPathExt;
45
use cargo_test_support::registry::Package;
56
use cargo_test_support::{basic_lib_manifest, basic_manifest, git, project};
@@ -1638,3 +1639,179 @@ fn crate_versions_flag_is_overridden() {
16381639
p.cargo("rustdoc -- --crate-version 2.0.3").run();
16391640
asserts(output_documentation());
16401641
}
1642+
1643+
#[cargo_test]
1644+
fn doc_fingerprint_is_versioning_consistent() {
1645+
// Random rustc verbose version
1646+
let old_rustc_verbose_version = format!(
1647+
"\
1648+
rustc 1.41.1 (f3e1a954d 2020-02-24)
1649+
binary: rustc
1650+
commit-hash: f3e1a954d2ead4e2fc197c7da7d71e6c61bad196
1651+
commit-date: 2020-02-24
1652+
host: {}
1653+
release: 1.41.1
1654+
LLVM version: 9.0
1655+
",
1656+
rustc_host()
1657+
);
1658+
1659+
// Create the dummy project.
1660+
let dummy_project = project()
1661+
.file(
1662+
"Cargo.toml",
1663+
r#"
1664+
[package]
1665+
name = "foo"
1666+
version = "1.2.4"
1667+
authors = []
1668+
"#,
1669+
)
1670+
.file("src/lib.rs", "//! These are the docs!")
1671+
.build();
1672+
1673+
dummy_project.cargo("doc").run();
1674+
1675+
let fingerprint: RustDocFingerprint =
1676+
serde_json::from_str(&dummy_project.read_file("target/.rustdoc_fingerprint.json"))
1677+
.expect("JSON Serde fail");
1678+
1679+
// Check that the fingerprint contains the actual rustc version
1680+
// which has been used to compile the docs.
1681+
let output = std::process::Command::new("rustc")
1682+
.arg("-vV")
1683+
.output()
1684+
.expect("Failed to get actual rustc verbose version");
1685+
assert_eq!(
1686+
fingerprint.rustc_vv,
1687+
(String::from_utf8_lossy(&output.stdout).as_ref())
1688+
);
1689+
1690+
// As the test shows above. Now we have generated the `doc/` folder and inside
1691+
// the rustdoc fingerprint file is located with the correct rustc version.
1692+
// So we will remove it and create a new fingerprint with an old rustc version
1693+
// inside it. We will also place a bogus file inside of the `doc/` folder to ensure
1694+
// it gets removed as we expect on the next doc compilation.
1695+
dummy_project.change_file(
1696+
"target/.rustdoc_fingerprint.json",
1697+
&old_rustc_verbose_version,
1698+
);
1699+
1700+
fs::write(
1701+
dummy_project.build_dir().join("doc/bogus_file"),
1702+
String::from("This is a bogus file and should be removed!"),
1703+
)
1704+
.expect("Error writing test bogus file");
1705+
1706+
// Now if we trigger another compilation, since the fingerprint contains an old version
1707+
// of rustc, cargo should remove the entire `/doc` folder (including the fingerprint)
1708+
// and generating another one with the actual version.
1709+
// It should also remove the bogus file we created above.
1710+
dummy_project.cargo("doc").run();
1711+
1712+
assert!(!dummy_project.build_dir().join("doc/bogus_file").exists());
1713+
1714+
let fingerprint: RustDocFingerprint =
1715+
serde_json::from_str(&dummy_project.read_file("target/.rustdoc_fingerprint.json"))
1716+
.expect("JSON Serde fail");
1717+
1718+
// Check that the fingerprint contains the actual rustc version
1719+
// which has been used to compile the docs.
1720+
assert_eq!(
1721+
fingerprint.rustc_vv,
1722+
(String::from_utf8_lossy(&output.stdout).as_ref())
1723+
);
1724+
}
1725+
1726+
#[cfg(target_os = "linux")]
1727+
#[cargo_test]
1728+
fn doc_fingerprint_respects_target_paths() {
1729+
// Random rustc verbose version
1730+
let old_rustc_verbose_version = format!(
1731+
"\
1732+
rustc 1.41.1 (f3e1a954d 2020-02-24)
1733+
binary: rustc
1734+
commit-hash: f3e1a954d2ead4e2fc197c7da7d71e6c61bad196
1735+
commit-date: 2020-02-24
1736+
host: {}
1737+
release: 1.41.1
1738+
LLVM version: 9.0
1739+
",
1740+
rustc_host()
1741+
);
1742+
1743+
// Create the dummy project.
1744+
let dummy_project = project()
1745+
.file(
1746+
"Cargo.toml",
1747+
r#"
1748+
[package]
1749+
name = "foo"
1750+
version = "1.2.4"
1751+
authors = []
1752+
"#,
1753+
)
1754+
.file("src/lib.rs", "//! These are the docs!")
1755+
.build();
1756+
1757+
dummy_project
1758+
.cargo("doc --target x86_64-unknown-linux-gnu")
1759+
.run();
1760+
1761+
let fingerprint: RustDocFingerprint =
1762+
serde_json::from_str(&dummy_project.read_file("target/.rustdoc_fingerprint.json"))
1763+
.expect("JSON Serde fail");
1764+
1765+
// Check that the fingerprint contains the actual rustc version
1766+
// which has been used to compile the docs.
1767+
let output = std::process::Command::new("rustc")
1768+
.arg("-vV")
1769+
.output()
1770+
.expect("Failed to get actual rustc verbose version");
1771+
assert_eq!(
1772+
fingerprint.rustc_vv,
1773+
(String::from_utf8_lossy(&output.stdout).as_ref())
1774+
);
1775+
1776+
// As the test shows above. Now we have generated the `doc/` folder and inside
1777+
// the rustdoc fingerprint file is located with the correct rustc version.
1778+
// So we will remove it and create a new fingerprint with an old rustc version
1779+
// inside it. We will also place a bogus file inside of the `doc/` folder to ensure
1780+
// it gets removed as we expect on the next doc compilation.
1781+
dummy_project.change_file(
1782+
"target/.rustdoc_fingerprint.json",
1783+
&old_rustc_verbose_version,
1784+
);
1785+
1786+
fs::write(
1787+
dummy_project
1788+
.build_dir()
1789+
.join("x86_64-unknown-linux-gnu/doc/bogus_file"),
1790+
String::from("This is a bogus file and should be removed!"),
1791+
)
1792+
.expect("Error writing test bogus file");
1793+
1794+
// Now if we trigger another compilation, since the fingerprint contains an old version
1795+
// of rustc, cargo should remove the entire `/doc` folder (including the fingerprint)
1796+
// and generating another one with the actual version.
1797+
// It should also remove the bogus file we created above.
1798+
dummy_project
1799+
.cargo("doc --target x86_64-unknown-linux-gnu")
1800+
.run();
1801+
1802+
assert!(!dummy_project
1803+
.build_dir()
1804+
.join("x86_64-unknown-linux-gnu/doc/bogus_file")
1805+
.exists());
1806+
1807+
let fingerprint: RustDocFingerprint =
1808+
serde_json::from_str(&dummy_project.read_file("target/.rustdoc_fingerprint.json"))
1809+
.expect("JSON Serde fail");
1810+
1811+
// Check that the fingerprint contains the actual rustc version
1812+
// which has been used to compile the docs.
1813+
assert_eq!(
1814+
fingerprint.rustc_vv,
1815+
(String::from_utf8_lossy(&output.stdout).as_ref())
1816+
);
1817+
}

0 commit comments

Comments
 (0)