diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs index 6d3df478f5a..8e978e4c321 100644 --- a/crates/cargo-test-support/src/compare.rs +++ b/crates/cargo-test-support/src/compare.rs @@ -338,6 +338,7 @@ static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[ ("[BLOCKING]", " Blocking"), ("[GENERATED]", " Generated"), ("[OPENING]", " Opening"), + ("[MERGING]", " Merging"), ]; /// Checks that the given string contains the given contiguous lines diff --git a/src/cargo/core/compiler/build_context/target_info.rs b/src/cargo/core/compiler/build_context/target_info.rs index a997ef2c38a..10671f7d50f 100644 --- a/src/cargo/core/compiler/build_context/target_info.rs +++ b/src/cargo/core/compiler/build_context/target_info.rs @@ -79,6 +79,8 @@ pub enum FileFlavor { DebugInfo, /// SBOM (Software Bill of Materials pre-cursor) file (e.g. cargo-sbon.json). Sbom, + /// Cross-crate info JSON files generated by rustdoc. + DocParts, } /// Type of each file generated by a Unit. diff --git a/src/cargo/core/compiler/build_runner/compilation_files.rs b/src/cargo/core/compiler/build_runner/compilation_files.rs index f09e591bd33..cdadbb6129f 100644 --- a/src/cargo/core/compiler/build_runner/compilation_files.rs +++ b/src/cargo/core/compiler/build_runner/compilation_files.rs @@ -275,6 +275,15 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> { self.layout(unit.kind).build_dir().deps(&dir) } + /// Returns the directories where Rust crate dependencies are found for the + /// specified unit. (new layout) + /// + /// New features should consider using this so we can avoid their migrations. + pub fn deps_dir_new_layout(&self, unit: &Unit) -> PathBuf { + let dir = self.pkg_dir(unit); + self.layout(unit.kind).build_dir().deps_new_layout(&dir) + } + /// Directory where the fingerprint for the given unit should go. pub fn fingerprint_dir(&self, unit: &Unit) -> PathBuf { let dir = self.pkg_dir(unit); @@ -495,12 +504,27 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> { .join("index.html") }; - vec![OutputFile { + let mut outputs = vec![OutputFile { path, hardlink: None, export_path: None, flavor: FileFlavor::Normal, - }] + }]; + + if bcx.gctx.cli_unstable().rustdoc_mergeable_info { + // `-Zrustdoc-mergeable-info` always uses the new layout. + outputs.push(OutputFile { + path: self + .deps_dir_new_layout(unit) + .join(unit.target.crate_name()) + .with_extension("json"), + hardlink: None, + export_path: None, + flavor: FileFlavor::DocParts, + }) + } + + outputs } CompileMode::RunCustomBuild => { // At this time, this code path does not handle build script diff --git a/src/cargo/core/compiler/build_runner/mod.rs b/src/cargo/core/compiler/build_runner/mod.rs index 9d0533f7fce..1768dccaf35 100644 --- a/src/cargo/core/compiler/build_runner/mod.rs +++ b/src/cargo/core/compiler/build_runner/mod.rs @@ -16,15 +16,14 @@ use filetime::FileTime; use itertools::Itertools; use jobserver::Client; +use super::RustdocFingerprint; use super::custom_build::{self, BuildDeps, BuildScriptOutputs, BuildScripts}; use super::fingerprint::{Checksum, Fingerprint}; use super::job_queue::JobQueue; use super::layout::Layout; use super::lto::Lto; use super::unit_graph::UnitDep; -use super::{ - BuildContext, Compilation, CompileKind, CompileMode, Executor, FileFlavor, RustDocFingerprint, -}; +use super::{BuildContext, Compilation, CompileKind, CompileMode, Executor, FileFlavor}; mod compilation_files; use self::compilation_files::CompilationFiles; @@ -178,7 +177,7 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { // 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)? + RustdocFingerprint::check_rustdoc_fingerprint(&self)? } for unit in &self.bcx.roots { @@ -225,6 +224,8 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { } } + self.collect_doc_merge_info()?; + // Collect the result of the build into `self.compilation`. for unit in &self.bcx.roots { self.collect_tests_and_executables(unit)?; @@ -330,6 +331,58 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { Ok(()) } + fn collect_doc_merge_info(&mut self) -> CargoResult<()> { + if !self.bcx.gctx.cli_unstable().rustdoc_mergeable_info { + return Ok(()); + } + + if !self.bcx.build_config.intent.is_doc() { + return Ok(()); + } + + if self.bcx.build_config.intent.wants_doc_json_output() { + // rustdoc JSON output doesn't support merge (yet?) + return Ok(()); + } + + let mut doc_parts_map: HashMap<_, Vec<_>> = HashMap::new(); + + let unit_iter = if self.bcx.build_config.intent.wants_deps_docs() { + itertools::Either::Left(self.bcx.unit_graph.keys()) + } else { + itertools::Either::Right(self.bcx.roots.iter()) + }; + + for unit in unit_iter { + if !unit.mode.is_doc() { + continue; + } + // Assumption: one `rustdoc` call generates only one cross-crate info JSON. + let outputs = self.outputs(unit)?; + + let Some(doc_parts) = outputs + .iter() + .find(|o| matches!(o.flavor, FileFlavor::DocParts)) + else { + continue; + }; + + doc_parts_map + .entry(unit.kind) + .or_default() + .push(doc_parts.path.to_owned()); + } + + self.compilation.rustdoc_fingerprints = Some( + doc_parts_map + .into_iter() + .map(|(kind, doc_parts)| (kind, RustdocFingerprint::new(self, kind, doc_parts))) + .collect(), + ); + + Ok(()) + } + /// Returns the executable for the specified unit (if any). pub fn get_executable(&mut self, unit: &Unit) -> CargoResult> { let is_binary = unit.target.is_executable(); diff --git a/src/cargo/core/compiler/compilation.rs b/src/cargo/core/compiler/compilation.rs index 66948f1e252..dff2d00ba80 100644 --- a/src/cargo/core/compiler/compilation.rs +++ b/src/cargo/core/compiler/compilation.rs @@ -9,6 +9,7 @@ use cargo_util::{ProcessBuilder, paths}; use crate::core::Package; use crate::core::compiler::BuildContext; +use crate::core::compiler::RustdocFingerprint; use crate::core::compiler::apply_env_config; use crate::core::compiler::{CompileKind, Unit, UnitHash}; use crate::util::{CargoResult, GlobalContext, context}; @@ -106,6 +107,11 @@ pub struct Compilation<'gctx> { /// Libraries to test with rustdoc. pub to_doc_test: Vec, + /// Rustdoc fingerprint files to determine whether we need to run `rustdoc --merge=finalize`. + /// + /// See `-Zrustdoc-mergeable-info` for more. + pub rustdoc_fingerprints: Option>, + /// The target host triple. pub host: String, @@ -143,6 +149,7 @@ impl<'gctx> Compilation<'gctx> { root_crate_names: Vec::new(), extra_env: HashMap::new(), to_doc_test: Vec::new(), + rustdoc_fingerprints: None, gctx: bcx.gctx, host: bcx.host_triple().to_string(), rustc_process, diff --git a/src/cargo/core/compiler/fingerprint/mod.rs b/src/cargo/core/compiler/fingerprint/mod.rs index 428743b74cf..cd48d4df82a 100644 --- a/src/cargo/core/compiler/fingerprint/mod.rs +++ b/src/cargo/core/compiler/fingerprint/mod.rs @@ -410,7 +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; +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 index 0bb9a16b636..3b1b0dfbcbc 100644 --- a/src/cargo/core/compiler/fingerprint/rustdoc.rs +++ b/src/cargo/core/compiler/fingerprint/rustdoc.rs @@ -1,4 +1,6 @@ +use std::collections::HashMap; use std::path::Path; +use std::path::PathBuf; use anyhow::Context as _; use cargo_util::paths; @@ -9,6 +11,17 @@ use crate::CargoResult; use crate::core::compiler::BuildRunner; use crate::core::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, +} + /// Structure used to deal with Rustdoc fingerprinting /// /// This is important because the `.js`/`.html` & `.css` files @@ -20,13 +33,19 @@ use crate::core::compiler::CompileKind; /// 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, +#[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, + /// The fingerprint file on disk. + on_disk: Option, } -impl RustDocFingerprint { +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. /// @@ -51,8 +70,9 @@ impl RustDocFingerprint { { return Ok(()); } - let new_fingerprint = RustDocFingerprint { + 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 { @@ -61,20 +81,130 @@ impl RustDocFingerprint { Ok(()) } + + /// Creates a new fingerprint with given doc parts paths. + pub fn new( + build_runner: &BuildRunner<'_, '_>, + kind: CompileKind, + doc_parts: Vec, + ) -> 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 --include-parts-dir`. + pub fn persist(&self, exec: F) -> CargoResult<()> + where + // 1. paths for `--include-parts-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 `. + // 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::>(); + let mut doc_parts: Vec<_> = dedup_map.into_values().collect(); + doc_parts.sort_unstable(); + + // Prepare args for `rustdoc --include-parts-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: &RustDocFingerprint, + new_fingerprint: &RustdocFingerprintJson, kind: CompileKind, ) -> CargoResult<()> { - let fingerprint_path = build_runner - .files() - .layout(kind) - .build_dir() - .root() - .join(".rustdoc_fingerprint.json"); + let fingerprint_path = fingerprint_path(build_runner, kind); let write_fingerprint = || -> CargoResult<()> { paths::write(&fingerprint_path, serde_json::to_string(new_fingerprint)?) @@ -88,7 +218,7 @@ fn check_fingerprint( return write_fingerprint(); }; - match serde_json::from_str::(&rustdoc_data) { + match serde_json::from_str::(&rustdoc_data) { Ok(on_disk_fingerprint) => { if on_disk_fingerprint.rustc_vv == new_fingerprint.rustc_vv { return Ok(()); @@ -124,6 +254,25 @@ fn check_fingerprint( Ok(()) } +/// Loads an on-disk fingerprint JSON file. +fn load_on_disk(path: &Path) -> Option { + 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::(&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() diff --git a/src/cargo/core/compiler/layout.rs b/src/cargo/core/compiler/layout.rs index 8824350b8a5..272e8198e27 100644 --- a/src/cargo/core/compiler/layout.rs +++ b/src/cargo/core/compiler/layout.rs @@ -309,11 +309,17 @@ impl BuildDirLayout { /// Fetch the deps path. pub fn deps(&self, pkg_dir: &str) -> PathBuf { if self.is_new_layout { - self.build_unit(pkg_dir).join("deps") + self.deps_new_layout(pkg_dir) } else { self.legacy_deps().to_path_buf() } } + /// Fetch the deps path. (new layout) + /// + /// New features should consider using this so we can avoid their migrations. + pub fn deps_new_layout(&self, pkg_dir: &str) -> PathBuf { + self.build_unit(pkg_dir).join("deps") + } /// Fetch the deps path. (old layout) pub fn legacy_deps(&self) -> &Path { &self.deps diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 62eb3df4d63..974c58abc7f 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -84,7 +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::fingerprint::RustdocFingerprint; pub use self::job_queue::Freshness; use self::job_queue::{Job, JobQueue, JobState, Work}; pub(crate) use self::layout::Layout; @@ -833,8 +833,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=") + }; arg.push(rustdoc_dep_info_loc(build_runner, unit)); rustdoc.arg(arg); @@ -843,6 +848,19 @@ 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="); + // `-Zrustdoc-mergeable-info` always uses the new layout. + arg.push(build_runner.files().deps_dir_new_layout(unit)); + rustdoc.arg(arg); } if let Some(trim_paths) = unit.profile.trim_paths.as_ref() { diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index f9f13dc2951..36bb8414296 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -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"), @@ -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)?, diff --git a/src/cargo/ops/cargo_doc.rs b/src/cargo/ops/cargo_doc.rs index fa350275529..36c34e94aab 100644 --- a/src/cargo/ops/cargo_doc.rs +++ b/src/cargo/ops/cargo_doc.rs @@ -1,9 +1,14 @@ use crate::core::compiler::{Compilation, CompileKind}; use crate::core::{Shell, Workspace, shell::Verbosity}; use crate::ops; +use crate::util; use crate::util::CargoResult; use crate::util::context::{GlobalContext, PathAndArgs}; + use anyhow::{Error, bail}; +use cargo_util::ProcessBuilder; + +use std::ffi::OsString; use std::path::Path; use std::path::PathBuf; use std::process::Command; @@ -55,6 +60,10 @@ pub struct DocOptions { pub fn doc(ws: &Workspace<'_>, options: &DocOptions) -> CargoResult<()> { let compilation = ops::compile(ws, &options.compile_opts)?; + if ws.gctx().cli_unstable().rustdoc_mergeable_info { + merge_cross_crate_info(ws, &compilation)?; + } + if options.open_result { let name = &compilation.root_crate_names.get(0).ok_or_else(|| { anyhow::anyhow!( @@ -117,6 +126,79 @@ pub fn doc(ws: &Workspace<'_>, options: &DocOptions) -> CargoResult<()> { Ok(()) } +fn merge_cross_crate_info(ws: &Workspace<'_>, compilation: &Compilation<'_>) -> CargoResult<()> { + let Some(fingerprints) = compilation.rustdoc_fingerprints.as_ref() else { + return Ok(()); + }; + + let now = std::time::Instant::now(); + for (kind, fingerprint) in fingerprints.iter() { + let (target_name, build_dir, artifact_dir) = match kind { + CompileKind::Host => ("host", ws.build_dir(), ws.target_dir()), + CompileKind::Target(t) => { + let name = t.short_name(); + let build_dir = ws.build_dir().join(name); + let artifact_dir = ws.target_dir().join(name); + (name, build_dir, artifact_dir) + } + }; + + // rustdoc needs to read doc parts files from build dir + build_dir.open_ro_shared_create(".cargo-lock", ws.gctx(), "build directory")?; + // rustdoc will write to `/doc/` + artifact_dir.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "artifact directory")?; + // We're leaking the layout implementation detail here. + // This detail should be hidden when doc merge becomes a Unit of work inside the build. + let rustdoc_artifact_dir = artifact_dir.join("doc"); + + if !fingerprint.is_dirty() { + ws.gctx().shell().verbose(|shell| { + shell.status("Fresh", format_args!("doc-merge for {target_name}")) + })?; + continue; + } + + fingerprint.persist(|doc_parts_dirs| { + let mut cmd = ProcessBuilder::new(ws.gctx().rustdoc()?); + if ws.gctx().extra_verbose() { + cmd.display_env_vars(); + } + cmd.retry_with_argfile(true); + cmd.arg("-o") + .arg(rustdoc_artifact_dir.as_path_unlocked()) + .arg("-Zunstable-options") + .arg("--merge=finalize"); + for parts_dir in doc_parts_dirs { + let mut include_arg = OsString::from("--include-parts-dir="); + include_arg.push(parts_dir); + cmd.arg(include_arg); + } + + let num_crates = doc_parts_dirs.len(); + let plural = if num_crates == 1 { "" } else { "s" }; + + ws.gctx().shell().status( + "Merging", + format_args!("{num_crates} doc{plural} for {target_name}"), + )?; + ws.gctx() + .shell() + .verbose(|shell| shell.status("Running", cmd.to_string()))?; + cmd.exec()?; + + Ok(()) + })?; + } + + let time_elapsed = util::elapsed(now.elapsed()); + ws.gctx().shell().status( + "Finished", + format_args!("documentation merge in {time_elapsed}"), + )?; + + Ok(()) +} + fn path_by_output_format( compilation: &Compilation<'_>, kind: &CompileKind, diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index fcab1f6e134..9e684dc1d9d 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -103,6 +103,7 @@ Each new feature described below should explain how to use it. * [scrape-examples](#scrape-examples) --- Shows examples within documentation. * [output-format](#output-format-for-rustdoc) --- Allows documentation to also be emitted in the experimental [JSON format](https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc_json_types/). * [rustdoc-depinfo](#rustdoc-depinfo) --- Use dep-info files in rustdoc rebuild detection. + * [rustdoc-mergeable-info](#rustdoc-mergeable-info) --- Use rustdoc mergeable cross-crate-info files. * `Cargo.toml` extensions * [Profile `rustflags` option](#profile-rustflags-option) --- Passed directly to rustc. * [Profile `hint-mostly-unused` option](#profile-hint-mostly-unused-option) --- Hint that a dependency is mostly unused, to optimize compilation time. @@ -2059,6 +2060,17 @@ cargo +nightly check --compile-time-deps --all-targets -Z unstable-options Enable `rustc`'s unicode error format in Cargo's error messages +## rustdoc mergeable info + +* Original Pull Request: [#16309](https://github.com/rust-lang/cargo/pull/16309) +* Tracking issue: [#16306](https://github.com/rust-lang/cargo/issues/16306) +* Tracking rustc issue: [rust-lang/rust#130676](https://github.com/rust-lang/rust/issues/130676) + +The `-Z rustdoc-mergeable-info` leverage rustdoc's mergeable crate info, +so that `cargo doc` can merge cross-crate information +(like the search index, source files index, etc.) +from separate output directories, +and run `rustdoc` in parallel. # Stabilized and removed features diff --git a/tests/testsuite/cargo/z_help/stdout.term.svg b/tests/testsuite/cargo/z_help/stdout.term.svg index f3354ac179b..e1dddbc6cec 100644 --- a/tests/testsuite/cargo/z_help/stdout.term.svg +++ b/tests/testsuite/cargo/z_help/stdout.term.svg @@ -1,4 +1,4 @@ - +