Skip to content

Commit 002bb4d

Browse files
committed
Add support for rustdoc mergeable cross-crate info parts
This is an unstable feature that we designed to fix several performance problems with the old system: 1. You couldn't easily build crate docs in hermetic environments. This doesn't matter for Cargo, but it was one of the original reasons to implement the feature. 2. We have to build all the doc resources in their final form at every step, instead of delaying slow parts (mostly the search index) until the end and only doing them once. 3. It requires rustdoc to take a lock at the end. This reduces available concurrency for generating docs.
1 parent 8120df7 commit 002bb4d

File tree

8 files changed

+221
-17
lines changed

8 files changed

+221
-17
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,15 @@ impl RustDocFingerprint {
11841184
.map(|kind| build_runner.files().layout(*kind).artifact_dir().doc())
11851185
.filter(|path| path.exists())
11861186
.try_for_each(|path| clean_doc(path))?;
1187+
if build_runner.bcx.gctx.cli_unstable().rustdoc_mergeable_info {
1188+
build_runner
1189+
.bcx
1190+
.all_kinds
1191+
.iter()
1192+
.map(|kind| build_runner.files().layout(*kind).build_dir().doc_parts())
1193+
.filter(|path| path.exists())
1194+
.try_for_each(|path| clean_doc(&path))?;
1195+
}
11871196
write_fingerprint()?;
11881197
return Ok(());
11891198

src/cargo/core/compiler/build_runner/compilation_files.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,13 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
324324
.build_script(&dir)
325325
}
326326

327+
/// Returns the directory where mergeable cross crate info for docs is stored.
328+
pub fn doc_parts_dir(&self, unit: &Unit) -> PathBuf {
329+
assert!(unit.mode.is_doc());
330+
assert!(self.metas.contains_key(unit));
331+
self.layout(unit.kind).build_dir().doc_parts().to_path_buf()
332+
}
333+
327334
/// Returns the directory for compiled artifacts files.
328335
/// `/path/to/target/{debug,release}/deps/artifact/KIND/PKG-HASH`
329336
fn artifact_dir(&self, unit: &Unit) -> PathBuf {

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! [`BuildRunner`] is the mutable state used during the build process.
22
33
use std::collections::{BTreeSet, HashMap, HashSet};
4+
use std::ffi::OsString;
45
use std::path::{Path, PathBuf};
56
use std::sync::{Arc, Mutex};
67

@@ -310,6 +311,49 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
310311
.insert(dir.clone().into_path_buf());
311312
}
312313
}
314+
315+
if self.bcx.build_config.intent.is_doc()
316+
&& self.bcx.gctx.cli_unstable().rustdoc_mergeable_info
317+
&& let Some(unit) = self
318+
.bcx
319+
.roots
320+
.iter()
321+
.filter(|unit| unit.mode.is_doc())
322+
.next()
323+
{
324+
let mut rustdoc = self.compilation.rustdoc_process(unit, None)?;
325+
let doc_dir = self.files().out_dir(unit);
326+
let mut include_arg = OsString::from("--include-parts-dir=");
327+
include_arg.push(self.files().doc_parts_dir(&unit));
328+
rustdoc
329+
.arg("-o")
330+
.arg(&doc_dir)
331+
.arg("--emit=toolchain-shared-resources")
332+
.arg("-Zunstable-options")
333+
.arg("--merge=finalize")
334+
.arg(include_arg);
335+
exec.exec(
336+
&rustdoc,
337+
unit.pkg.package_id(),
338+
&unit.target,
339+
CompileMode::Doc,
340+
// This is always single-threaded, and always gets run,
341+
// so thread delinterleaving isn't needed and neither is
342+
// the output cache.
343+
&mut |line| {
344+
let mut shell = self.bcx.gctx.shell();
345+
shell.print_ansi_stdout(line.as_bytes())?;
346+
shell.err().write_all(b"\n")?;
347+
Ok(())
348+
},
349+
&mut |line| {
350+
let mut shell = self.bcx.gctx.shell();
351+
shell.print_ansi_stderr(line.as_bytes())?;
352+
shell.err().write_all(b"\n")?;
353+
Ok(())
354+
},
355+
)?;
356+
}
313357
Ok(self.compilation)
314358
}
315359

src/cargo/core/compiler/layout.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,10 @@ impl BuildDirLayout {
337337
self.build().join(pkg_dir)
338338
}
339339
}
340+
/// Fetch the doc parts path.
341+
pub fn doc_parts(&self) -> PathBuf {
342+
self.build().join("doc.parts")
343+
}
340344
/// Fetch the build script execution path.
341345
pub fn build_script_execution(&self, pkg_dir: &str) -> PathBuf {
342346
if self.is_new_layout {

src/cargo/core/compiler/mod.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -831,8 +831,13 @@ fn prepare_rustdoc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResu
831831
if build_runner.bcx.gctx.cli_unstable().rustdoc_depinfo {
832832
// toolchain-shared-resources is required for keeping the shared styling resources
833833
// invocation-specific is required for keeping the original rustdoc emission
834-
let mut arg =
835-
OsString::from("--emit=toolchain-shared-resources,invocation-specific,dep-info=");
834+
let mut arg = if build_runner.bcx.gctx.cli_unstable().rustdoc_mergeable_info {
835+
// toolchain resources are written at the end, at the same time as merging
836+
OsString::from("--emit=invocation-specific,dep-info=")
837+
} else {
838+
// if not using mergeable CCI, everything is written every time
839+
OsString::from("--emit=toolchain-shared-resources,invocation-specific,dep-info=")
840+
};
836841
arg.push(rustdoc_dep_info_loc(build_runner, unit));
837842
rustdoc.arg(arg);
838843

@@ -841,6 +846,18 @@ fn prepare_rustdoc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResu
841846
}
842847

843848
rustdoc.arg("-Zunstable-options");
849+
} else if build_runner.bcx.gctx.cli_unstable().rustdoc_mergeable_info {
850+
// toolchain resources are written at the end, at the same time as merging
851+
rustdoc.arg("--emit=invocation-specific");
852+
rustdoc.arg("-Zunstable-options");
853+
}
854+
855+
if build_runner.bcx.gctx.cli_unstable().rustdoc_mergeable_info {
856+
// write out mergeable data to be imported
857+
rustdoc.arg("--merge=none");
858+
let mut arg = OsString::from("--parts-out-dir=");
859+
arg.push(build_runner.files().doc_parts_dir(&unit));
860+
rustdoc.arg(arg);
844861
}
845862

846863
if let Some(trim_paths) = unit.profile.trim_paths.as_ref() {

src/cargo/core/features.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,7 @@ unstable_cli_options!(
883883
root_dir: Option<PathBuf> = ("Set the root directory relative to which paths are printed (defaults to workspace root)"),
884884
rustdoc_depinfo: bool = ("Use dep-info files in rustdoc rebuild detection"),
885885
rustdoc_map: bool = ("Allow passing external documentation mappings to rustdoc"),
886+
rustdoc_mergeable_info: bool = ("Use rustdoc mergeable cross-crate-info files"),
886887
rustdoc_scrape_examples: bool = ("Allows Rustdoc to scrape code examples from reverse-dependencies"),
887888
sbom: bool = ("Enable the `sbom` option in build config in .cargo/config.toml file"),
888889
script: bool = ("Enable support for single-file, `.rs` packages"),
@@ -1413,6 +1414,7 @@ impl CliUnstable {
14131414
"root-dir" => self.root_dir = v.map(|v| v.into()),
14141415
"rustdoc-depinfo" => self.rustdoc_depinfo = parse_empty(k, v)?,
14151416
"rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?,
1417+
"rustdoc-mergeable-info" => self.rustdoc_mergeable_info = parse_empty(k, v)?,
14161418
"rustdoc-scrape-examples" => self.rustdoc_scrape_examples = parse_empty(k, v)?,
14171419
"sbom" => self.sbom = parse_empty(k, v)?,
14181420
"section-timings" => self.section_timings = parse_empty(k, v)?,

tests/testsuite/cargo/z_help/stdout.term.svg

Lines changed: 17 additions & 15 deletions
Loading

tests/testsuite/doc.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,125 @@ fn doc_no_deps() {
182182
assert!(!p.root().join("target/doc/bar/index.html").is_file());
183183
}
184184

185+
#[cargo_test]
186+
fn doc_deps_rustdoc_mergeable_info() {
187+
let p = project()
188+
.file(
189+
"Cargo.toml",
190+
r#"
191+
[package]
192+
name = "foo"
193+
version = "0.0.1"
194+
edition = "2015"
195+
authors = []
196+
197+
[dependencies.bar]
198+
path = "bar"
199+
"#,
200+
)
201+
.file("src/lib.rs", "extern crate bar; pub fn foo() {}")
202+
.file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1"))
203+
.file("bar/src/lib.rs", "pub fn bar() {}")
204+
.build();
205+
206+
p.cargo("doc -Zunstable-options -Zrustdoc-mergeable-info")
207+
.masquerade_as_nightly_cargo(&["rustdoc-mergeable-info"])
208+
.with_stderr_data(
209+
str![[r#"
210+
[LOCKING] 1 package to latest compatible version
211+
[DOCUMENTING] bar v0.0.1 ([ROOT]/foo/bar)
212+
[CHECKING] bar v0.0.1 ([ROOT]/foo/bar)
213+
[DOCUMENTING] foo v0.0.1 ([ROOT]/foo)
214+
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
215+
[GENERATED] [ROOT]/foo/target/doc/foo/index.html
216+
217+
"#]]
218+
.unordered(),
219+
)
220+
.run();
221+
222+
assert!(p.root().join("target/doc").is_dir());
223+
assert!(p.root().join("target/doc/foo/index.html").is_file());
224+
assert!(p.root().join("target/doc/bar/index.html").is_file());
225+
226+
// Verify that it only emits rmeta for the dependency.
227+
assert_eq!(p.glob("target/debug/**/*.rlib").count(), 0);
228+
assert_eq!(p.glob("target/debug/deps/libbar-*.rmeta").count(), 1);
229+
230+
// Make sure it doesn't recompile.
231+
p.cargo("doc -Zunstable-options -Zrustdoc-mergeable-info")
232+
.masquerade_as_nightly_cargo(&["rustdoc-mergeable-info"])
233+
.with_stderr_data(str![[r#"
234+
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
235+
[GENERATED] [ROOT]/foo/target/doc/foo/index.html
236+
237+
"#]])
238+
.run();
239+
240+
assert!(p.root().join("target/doc").is_dir());
241+
assert!(p.root().join("target/doc/foo/index.html").is_file());
242+
assert!(p.root().join("target/doc/bar/index.html").is_file());
243+
}
244+
245+
#[cargo_test]
246+
fn doc_no_deps_rustdoc_mergeable_info() {
247+
let p = project()
248+
.file(
249+
"Cargo.toml",
250+
r#"
251+
[package]
252+
name = "foo"
253+
version = "0.0.1"
254+
edition = "2015"
255+
authors = []
256+
257+
[dependencies.bar]
258+
path = "bar"
259+
"#,
260+
)
261+
.file("src/lib.rs", "extern crate bar; pub fn foo() {}")
262+
.file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1"))
263+
.file("bar/src/lib.rs", "pub fn bar() {}")
264+
.build();
265+
266+
p.cargo("doc --no-deps -Zunstable-options -Zrustdoc-mergeable-info")
267+
.masquerade_as_nightly_cargo(&["rustdoc-mergeable-info"])
268+
.with_stderr_data(
269+
str![[r#"
270+
[LOCKING] 1 package to latest compatible version
271+
[CHECKING] bar v0.0.1 ([ROOT]/foo/bar)
272+
[DOCUMENTING] foo v0.0.1 ([ROOT]/foo)
273+
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
274+
[GENERATED] [ROOT]/foo/target/doc/foo/index.html
275+
276+
"#]]
277+
.unordered(),
278+
)
279+
.run();
280+
281+
assert!(p.root().join("target/doc").is_dir());
282+
assert!(p.root().join("target/doc/foo/index.html").is_file());
283+
assert!(!p.root().join("target/doc/bar/index.html").is_file());
284+
285+
// Verify that it only emits rmeta for the dependency.
286+
assert_eq!(p.glob("target/debug/**/*.rlib").count(), 0);
287+
assert_eq!(p.glob("target/debug/deps/libbar-*.rmeta").count(), 1);
288+
289+
// Make sure it doesn't recompile.
290+
p.cargo("doc --no-deps -Zunstable-options -Zrustdoc-mergeable-info")
291+
.masquerade_as_nightly_cargo(&["rustdoc-mergeable-info"])
292+
.with_stderr_data(str![[r#"
293+
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
294+
[GENERATED] [ROOT]/foo/target/doc/foo/index.html
295+
296+
"#]])
297+
.run();
298+
299+
assert!(p.root().join("target/doc").is_dir());
300+
assert!(p.root().join("target/doc/foo/index.html").is_file());
301+
assert!(!p.root().join("target/doc/bar/index.html").is_file());
302+
}
303+
185304
#[cargo_test]
186305
fn doc_only_bin() {
187306
let p = project()

0 commit comments

Comments
 (0)