Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/cargo/core/compiler/build_context/target_info.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for creating this draft PR! I didn't know about RFC 3662 until now, so may need more time to catch up.
From my quick skimming through that, it seem prettty aligned with the WIP new build-dir layout in #15010 (one unit per independent directory), as well as the fine-grained locking in #4282.

Not sure if we want to pair them together. It may depend on the timeline of the stabilization of each unstable feature.

Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,15 @@ impl RustDocFingerprint {
})
.filter(|path| path.exists())
.try_for_each(|path| clean_doc(path))?;
if build_runner.bcx.gctx.cli_unstable().rustdoc_mergeable_info {
build_runner
.bcx
.all_kinds
.iter()
.map(|kind| build_runner.files().layout(*kind).build_dir().doc_parts())
.filter(|path| path.exists())
.try_for_each(|path| clean_doc(&path))?;
}
write_fingerprint()?;
return Ok(());

Expand Down
7 changes: 7 additions & 0 deletions src/cargo/core/compiler/build_runner/compilation_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,13 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
.build_script(&dir)
}

/// Returns the directory where mergeable cross crate info for docs is stored.
pub fn doc_parts_dir(&self, unit: &Unit) -> PathBuf {
assert!(unit.mode.is_doc());
assert!(self.metas.contains_key(unit));
self.layout(unit.kind).build_dir().doc_parts().to_path_buf()
}

/// Returns the directory for compiled artifacts files.
/// `/path/to/target/{debug,release}/deps/artifact/KIND/PKG-HASH`
fn artifact_dir(&self, unit: &Unit) -> PathBuf {
Expand Down
44 changes: 44 additions & 0 deletions src/cargo/core/compiler/build_runner/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! [`BuildRunner`] is the mutable state used during the build process.

use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

Expand Down Expand Up @@ -302,6 +303,49 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> {
.insert(dir.clone().into_path_buf());
}
}

if self.bcx.build_config.intent.is_doc()
&& self.bcx.gctx.cli_unstable().rustdoc_mergeable_info
&& let Some(unit) = self
.bcx
.roots
.iter()
.filter(|unit| unit.mode.is_doc())
.next()
Comment on lines +309 to +314
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the finial merge step required to be run always?

I was wondering the fingerprint/caching story from here #16291. Perhaps a better model is making this a Unit with dependencies of all rustdoc invocations.

(Happy to help continue Cargo integration if you need more time on rustdoc side 🙂)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the finial merge step required to be run always?

The final merge step has to run if any of the docs have been changed. It can be skipped only if absolutely none of the crates needed documented.

Perhaps a better model is making this a Unit with dependencies of all rustdoc invocations.

I would love to. Unfortunately, the merge step is not associated with any crate in particular, and Unit has a nonOptional package member variable.

(Happy to help continue Cargo integration if you need more time on rustdoc side 🙂)

Thank you!

{
let mut rustdoc = self.compilation.rustdoc_process(unit, None)?;
let doc_dir = self.files().out_dir(unit);
let mut include_arg = OsString::from("--include-parts-dir=");
include_arg.push(self.files().doc_parts_dir(&unit));
rustdoc
.arg("-o")
.arg(&doc_dir)
.arg("--emit=toolchain-shared-resources")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding --emit=toolchain-shared-resources to --merge=finalize will hinder rustdoc from generating search index. I don't know if it is a bug or intentional, or just --emit and --merge haven't yet collaborate with each other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does seem weird. Probably a cut-and-paste error.

.arg("-Zunstable-options")
.arg("--merge=finalize")
.arg(include_arg);
exec.exec(
&rustdoc,
unit.pkg.package_id(),
&unit.target,
CompileMode::Doc,
// This is always single-threaded, and always gets run,
// so thread delinterleaving isn't needed and neither is
// the output cache.
&mut |line| {
let mut shell = self.bcx.gctx.shell();
shell.print_ansi_stdout(line.as_bytes())?;
shell.err().write_all(b"\n")?;
Ok(())
},
&mut |line| {
let mut shell = self.bcx.gctx.shell();
shell.print_ansi_stderr(line.as_bytes())?;
shell.err().write_all(b"\n")?;
Ok(())
},
)?;
}
Ok(self.compilation)
}

Expand Down
4 changes: 4 additions & 0 deletions src/cargo/core/compiler/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,10 @@ impl BuildDirLayout {
self.build().join(pkg_dir)
}
}
/// Fetch the doc parts path.
pub fn doc_parts(&self) -> PathBuf {
self.build().join("doc.parts")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The output crate-info file is per unit, right? This location doesn't seem like a per unit one: target/debug/build/doc.parts/crate-info. We probably should have a unit hash suffix for that directory
  • The legacy fn build() path is for build script compilation and execution. We might need to find a directory layout for doc parts specifically

I guess we can probably put them under something liketarget/<profile>/doc-parts/<name>-<hash>.crate-info.json? Not sure if the crate-info file name can be changed though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the crate-info file is per-unit.

The crate-info file name can't really be changed. It is named ${crate_name}.json. In particular, the file name is aligned with the crate name as found in our URLs (so, if the crate-info file is named foobar.json, then the crate site is found at foobar/index.html in the docs folder).

}
/// Fetch the build script execution path.
pub fn build_script_execution(&self, pkg_dir: &str) -> PathBuf {
if self.is_new_layout {
Expand Down
21 changes: 19 additions & 2 deletions src/cargo/core/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -830,8 +830,13 @@ fn prepare_rustdoc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResu
if build_runner.bcx.gctx.cli_unstable().rustdoc_depinfo {
// toolchain-shared-resources is required for keeping the shared styling resources
// invocation-specific is required for keeping the original rustdoc emission
let mut arg =
OsString::from("--emit=toolchain-shared-resources,invocation-specific,dep-info=");
let mut arg = if build_runner.bcx.gctx.cli_unstable().rustdoc_mergeable_info {
// toolchain resources are written at the end, at the same time as merging
OsString::from("--emit=invocation-specific,dep-info=")
} else {
// if not using mergeable CCI, everything is written every time
OsString::from("--emit=toolchain-shared-resources,invocation-specific,dep-info=")
Comment on lines +833 to +838
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another reason we need to stabilize rustdoc's --emit flag 😬

Copy link
Contributor Author

@notriddle notriddle Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I know. I've been working on that, and will try to get it stabilized before trying to get this stabilized.

rust-lang/rust#148180

};
arg.push(rustdoc_dep_info_loc(build_runner, unit));
rustdoc.arg(arg);

Expand All @@ -840,6 +845,18 @@ fn prepare_rustdoc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResu
}

rustdoc.arg("-Zunstable-options");
} else if build_runner.bcx.gctx.cli_unstable().rustdoc_mergeable_info {
// toolchain resources are written at the end, at the same time as merging
rustdoc.arg("--emit=invocation-specific");
rustdoc.arg("-Zunstable-options");
}

if build_runner.bcx.gctx.cli_unstable().rustdoc_mergeable_info {
// write out mergeable data to be imported
rustdoc.arg("--merge=none");
let mut arg = OsString::from("--parts-out-dir=");
arg.push(build_runner.files().doc_parts_dir(&unit));
rustdoc.arg(arg);
}

if let Some(trim_paths) = unit.profile.trim_paths.as_ref() {
Expand Down
2 changes: 2 additions & 0 deletions src/cargo/core/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,7 @@ unstable_cli_options!(
rustc_unicode: bool = ("Enable `rustc`'s unicode error format in Cargo's error messages"),
rustdoc_depinfo: bool = ("Use dep-info files in rustdoc rebuild detection"),
rustdoc_map: bool = ("Allow passing external documentation mappings to rustdoc"),
rustdoc_mergeable_info: bool = ("Use rustdoc mergeable cross-crate-info files"),
rustdoc_scrape_examples: bool = ("Allows Rustdoc to scrape code examples from reverse-dependencies"),
sbom: bool = ("Enable the `sbom` option in build config in .cargo/config.toml file"),
script: bool = ("Enable support for single-file, `.rs` packages"),
Expand Down Expand Up @@ -1415,6 +1416,7 @@ impl CliUnstable {
"rustc-unicode" => self.rustc_unicode = parse_empty(k, v)?,
"rustdoc-depinfo" => self.rustdoc_depinfo = parse_empty(k, v)?,
"rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?,
"rustdoc-mergeable-info" => self.rustdoc_mergeable_info = parse_empty(k, v)?,
"rustdoc-scrape-examples" => self.rustdoc_scrape_examples = parse_empty(k, v)?,
"sbom" => self.sbom = parse_empty(k, v)?,
"section-timings" => self.section_timings = parse_empty(k, v)?,
Expand Down
30 changes: 16 additions & 14 deletions tests/testsuite/cargo/z_help/stdout.term.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
119 changes: 119 additions & 0 deletions tests/testsuite/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,125 @@ fn doc_no_deps() {
assert!(!p.root().join("target/doc/bar/index.html").is_file());
}

#[cargo_test(nightly, reason = "rustdoc mergeable crate info is unstable")]
fn doc_deps_rustdoc_mergeable_info() {
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.0.1"
edition = "2015"
authors = []

[dependencies.bar]
path = "bar"
"#,
)
.file("src/lib.rs", "extern crate bar; pub fn foo() {}")
.file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1"))
.file("bar/src/lib.rs", "pub fn bar() {}")
.build();

p.cargo("doc -Zunstable-options -Zrustdoc-mergeable-info")
.masquerade_as_nightly_cargo(&["rustdoc-mergeable-info"])
.with_stderr_data(
str![[r#"
[LOCKING] 1 package to latest compatible version
[DOCUMENTING] bar v0.0.1 ([ROOT]/foo/bar)
[CHECKING] bar v0.0.1 ([ROOT]/foo/bar)
[DOCUMENTING] foo v0.0.1 ([ROOT]/foo)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
[GENERATED] [ROOT]/foo/target/doc/foo/index.html

"#]]
.unordered(),
)
.run();

assert!(p.root().join("target/doc").is_dir());
assert!(p.root().join("target/doc/foo/index.html").is_file());
assert!(p.root().join("target/doc/bar/index.html").is_file());

// Verify that it only emits rmeta for the dependency.
assert_eq!(p.glob("target/debug/**/*.rlib").count(), 0);
assert_eq!(p.glob("target/debug/deps/libbar-*.rmeta").count(), 1);

// Make sure it doesn't recompile.
p.cargo("doc -Zunstable-options -Zrustdoc-mergeable-info")
.masquerade_as_nightly_cargo(&["rustdoc-mergeable-info"])
.with_stderr_data(str![[r#"
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
[GENERATED] [ROOT]/foo/target/doc/foo/index.html

"#]])
.run();

assert!(p.root().join("target/doc").is_dir());
assert!(p.root().join("target/doc/foo/index.html").is_file());
assert!(p.root().join("target/doc/bar/index.html").is_file());
}

#[cargo_test(nightly, reason = "rustdoc mergeable crate info is unstable")]
fn doc_no_deps_rustdoc_mergeable_info() {
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.0.1"
edition = "2015"
authors = []

[dependencies.bar]
path = "bar"
"#,
)
.file("src/lib.rs", "extern crate bar; pub fn foo() {}")
.file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1"))
.file("bar/src/lib.rs", "pub fn bar() {}")
.build();

p.cargo("doc --no-deps -Zunstable-options -Zrustdoc-mergeable-info")
.masquerade_as_nightly_cargo(&["rustdoc-mergeable-info"])
.with_stderr_data(
str![[r#"
[LOCKING] 1 package to latest compatible version
[CHECKING] bar v0.0.1 ([ROOT]/foo/bar)
[DOCUMENTING] foo v0.0.1 ([ROOT]/foo)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
[GENERATED] [ROOT]/foo/target/doc/foo/index.html

"#]]
.unordered(),
)
.run();

assert!(p.root().join("target/doc").is_dir());
assert!(p.root().join("target/doc/foo/index.html").is_file());
assert!(!p.root().join("target/doc/bar/index.html").is_file());

// Verify that it only emits rmeta for the dependency.
assert_eq!(p.glob("target/debug/**/*.rlib").count(), 0);
assert_eq!(p.glob("target/debug/deps/libbar-*.rmeta").count(), 1);

// Make sure it doesn't recompile.
p.cargo("doc --no-deps -Zunstable-options -Zrustdoc-mergeable-info")
.masquerade_as_nightly_cargo(&["rustdoc-mergeable-info"])
.with_stderr_data(str![[r#"
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
[GENERATED] [ROOT]/foo/target/doc/foo/index.html

"#]])
.run();

assert!(p.root().join("target/doc").is_dir());
assert!(p.root().join("target/doc/foo/index.html").is_file());
assert!(!p.root().join("target/doc/bar/index.html").is_file());
}

#[cargo_test]
fn doc_only_bin() {
let p = project()
Expand Down