From 998be7de3ee585ea02259772c1ff261fdcbd5394 Mon Sep 17 00:00:00 2001 From: motorailgun <28751910+motorailgun@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:45:07 +0000 Subject: [PATCH 1/3] chore: add tests for rustdoc json output rebuild --- tests/testsuite/build_dir.rs | 38 +++++++++ tests/testsuite/build_dir_legacy.rs | 38 +++++++++ tests/testsuite/rustdoc.rs | 115 ++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+) diff --git a/tests/testsuite/build_dir.rs b/tests/testsuite/build_dir.rs index 0d4b88084b0..2075b3c9edf 100644 --- a/tests/testsuite/build_dir.rs +++ b/tests/testsuite/build_dir.rs @@ -501,6 +501,44 @@ fn cargo_doc_should_output_to_target_dir() { assert_exists(&docs_dir.join("foo/index.html")); } +#[cargo_test(nightly, reason = "--output-format is unstable")] +fn cargo_rustdoc_json_should_output_to_target_dir() { + let p = project() + .file("src/lib.rs", "") + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("-Zbuild-dir-new-layout rustdoc -Zunstable-options --output-format json") + .masquerade_as_nightly_cargo(&["new build-dir layout", "rustdoc-output-format"]) + .enable_mac_dsym() + .run(); + + let docs_dir = p.root().join("target-dir/doc"); + + assert_exists(&docs_dir); + assert_exists(&docs_dir.join("foo.json")); + + p.root().join("build-dir").assert_build_dir_layout(str![ + r#" +[ROOT]/foo/build-dir/.rustc_info.json +[ROOT]/foo/build-dir/.rustdoc_fingerprint.json +[ROOT]/foo/build-dir/CACHEDIR.TAG +[ROOT]/foo/build-dir/debug/.cargo-build-lock +[ROOT]/foo/build-dir/debug/.fingerprint/foo-[HASH]/doc-lib-foo +[ROOT]/foo/build-dir/debug/.fingerprint/foo-[HASH]/doc-lib-foo.json +[ROOT]/foo/build-dir/debug/.fingerprint/foo-[HASH]/invoked.timestamp + +"# + ]); +} + #[cargo_test] fn cargo_package_should_build_in_build_dir_and_output_to_target_dir() { let p = project() diff --git a/tests/testsuite/build_dir_legacy.rs b/tests/testsuite/build_dir_legacy.rs index ff708525fef..576ed5d6a74 100644 --- a/tests/testsuite/build_dir_legacy.rs +++ b/tests/testsuite/build_dir_legacy.rs @@ -459,6 +459,44 @@ fn cargo_doc_should_output_to_target_dir() { assert_exists(&docs_dir.join("foo/index.html")); } +#[cargo_test(nightly, reason = "--output-format is unstable")] +fn cargo_rustdoc_json_should_output_to_target_dir() { + let p = project() + .file("src/lib.rs", "") + .file( + ".cargo/config.toml", + r#" + [build] + target-dir = "target-dir" + build-dir = "build-dir" + "#, + ) + .build(); + + p.cargo("rustdoc -Zunstable-options --output-format json") + .masquerade_as_nightly_cargo(&["rustdoc-output-format"]) + .enable_mac_dsym() + .run(); + + let docs_dir = p.root().join("target-dir/doc"); + + assert_exists(&docs_dir); + assert_exists(&docs_dir.join("foo.json")); + + p.root().join("build-dir").assert_build_dir_layout(str![ + r#" +[ROOT]/foo/build-dir/.rustc_info.json +[ROOT]/foo/build-dir/.rustdoc_fingerprint.json +[ROOT]/foo/build-dir/CACHEDIR.TAG +[ROOT]/foo/build-dir/debug/.cargo-build-lock +[ROOT]/foo/build-dir/debug/.fingerprint/foo-[HASH]/doc-lib-foo +[ROOT]/foo/build-dir/debug/.fingerprint/foo-[HASH]/doc-lib-foo.json +[ROOT]/foo/build-dir/debug/.fingerprint/foo-[HASH]/invoked.timestamp + +"# + ]); +} + #[cargo_test] fn cargo_package_should_build_in_build_dir_and_output_to_target_dir() { let p = project() diff --git a/tests/testsuite/rustdoc.rs b/tests/testsuite/rustdoc.rs index d86a995ec76..1a36350c38d 100644 --- a/tests/testsuite/rustdoc.rs +++ b/tests/testsuite/rustdoc.rs @@ -1,5 +1,7 @@ //! Tests for the `cargo rustdoc` command. +use std::fs; + use crate::prelude::*; use cargo_test_support::str; use cargo_test_support::{basic_manifest, cross_compile, project}; @@ -53,6 +55,23 @@ fn rustdoc_simple_json() { assert!(p.root().join("target/doc/foo.json").is_file()); } +#[cargo_test(nightly, reason = "--output-format is unstable")] +fn rustdoc_json_with_new_layout() { + let p = project().file("src/lib.rs", "").build(); + + p.cargo("rustdoc -Z unstable-options -Z build-dir-new-layout --output-format json -v") + .masquerade_as_nightly_cargo(&["rustdoc-output-format"]) + .with_stderr_data(str![[r#" +[DOCUMENTING] foo v0.0.1 ([ROOT]/foo) +[RUNNING] `rustdoc [..] --crate-name foo [..]-o [ROOT]/foo/target/doc [..] --output-format=json[..] +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[GENERATED] [ROOT]/foo/target/doc/foo.json + +"#]]) + .run(); + assert!(p.root().join("target/doc/foo.json").is_file()); +} + #[cargo_test] fn rustdoc_invalid_output_format() { let p = project().file("src/lib.rs", "").build(); @@ -322,3 +341,99 @@ fn fail_with_glob() { "#]]) .run(); } + +#[cargo_test(nightly, reason = "--output-format is unstable")] +fn rustdoc_json_same_crate_different_version() { + let entry = project() + .file( + "Cargo.toml", + r#" + [package] + name = "entry" + version = "0.1.0" + edition = "2021" + + [dependencies] + dep_v1 = { path = "../dep_v1", package = "dep" } + dep_v2 = { path = "../dep_v2", package = "dep" } + "#, + ) + .file("src/lib.rs", "pub fn entry() {}") + .build(); + + let _dep_v1 = project() + .at("dep_v1") + .file( + "Cargo.toml", + r#" + [package] + name = "dep" + version = "1.0.0" + edition = "2021" + "#, + ) + .file("src/lib.rs", "pub fn dep_v1_fn() {}") + .build(); + + let _dep_v2 = project() + .at("dep_v2") + .file( + "Cargo.toml", + r#" + [package] + name = "dep" + version = "2.0.0" + edition = "2021" + "#, + ) + .file("src/lib.rs", "pub fn dep_v2_fn() {}") + .build(); + + entry + .cargo("rustdoc -v -Z unstable-options --output-format json -p dep@1.0.0") + .masquerade_as_nightly_cargo(&["rustdoc-output-format"]) + .with_stderr_data(str![[r#" +[LOCKING] 2 packages to latest compatible versions +[DOCUMENTING] dep v1.0.0 ([ROOT]/dep_v1) +[RUNNING] `rustdoc [..] --crate-name dep [ROOT]/dep_v1/src/lib.rs [..] --output-format=json[..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[GENERATED] [ROOT]/foo/target/doc/dep.json + +"#]]) + .run(); + + let dep_json = fs::read_to_string(entry.root().join("target/doc/dep.json")).unwrap(); + assert!(dep_json.contains("dep_v1_fn")); + assert!(!dep_json.contains("dep_v2_fn")); + + entry + .cargo("rustdoc -v -Z unstable-options --output-format json -p dep@2.0.0") + .masquerade_as_nightly_cargo(&["rustdoc-output-format"]) + .with_stderr_data(str![[r#" +[DOCUMENTING] dep v2.0.0 ([ROOT]/dep_v2) +[RUNNING] `rustdoc [..] --crate-name dep [ROOT]/dep_v2/src/lib.rs [..] --output-format=json[..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[GENERATED] [ROOT]/foo/target/doc/dep.json + +"#]]) + .run(); + + let dep_json = fs::read_to_string(entry.root().join("target/doc/dep.json")).unwrap(); + assert!(!dep_json.contains("dep_v1_fn")); + assert!(dep_json.contains("dep_v2_fn")); + + entry + .cargo("rustdoc -v -Z unstable-options --output-format json -p dep@1.0.0") + .masquerade_as_nightly_cargo(&["rustdoc-output-format"]) + .with_stderr_data(str![[r#" +[FRESH] dep v1.0.0 ([ROOT]/dep_v1) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +[GENERATED] [ROOT]/foo/target/doc/dep.json + +"#]]) + .run(); + + let dep_json = fs::read_to_string(entry.root().join("target/doc/dep.json")).unwrap(); + assert!(!dep_json.contains("dep_v1_fn")); + assert!(dep_json.contains("dep_v2_fn")); +} From 90695f167f75af2ef301df997fc0d7ea88e08c70 Mon Sep 17 00:00:00 2001 From: motorailgun <28751910+motorailgun@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:27:41 +0000 Subject: [PATCH 2/3] refactor: remove redundant unstable options check in add_output_format() --- src/cargo/core/compiler/rustdoc.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/cargo/core/compiler/rustdoc.rs b/src/cargo/core/compiler/rustdoc.rs index 6f5d0037e48..3b0be01a21c 100644 --- a/src/cargo/core/compiler/rustdoc.rs +++ b/src/cargo/core/compiler/rustdoc.rs @@ -250,12 +250,6 @@ pub fn add_output_format( build_runner: &BuildRunner<'_, '_>, rustdoc: &mut ProcessBuilder, ) -> CargoResult<()> { - let gctx = build_runner.bcx.gctx; - if !gctx.cli_unstable().unstable_options { - tracing::debug!("`unstable-options` is ignored, required -Zunstable-options flag"); - return Ok(()); - } - if build_runner.bcx.build_config.intent.wants_doc_json_output() { rustdoc.arg("-Zunstable-options"); rustdoc.arg("--output-format=json"); From c753ff41d5c70c27428d2cff21d12cbce7c82e9d Mon Sep 17 00:00:00 2001 From: motorailgun <28751910+motorailgun@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:28:48 +0000 Subject: [PATCH 3/3] fix: rebuild rustdoc json for different versions of a same crate --- .../build_runner/compilation_files.rs | 23 ++++++++++++--- src/cargo/core/compiler/mod.rs | 28 +++++++++++++------ tests/testsuite/build_dir.rs | 1 + tests/testsuite/build_dir_legacy.rs | 1 + tests/testsuite/rustdoc.rs | 8 +++--- 5 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/cargo/core/compiler/build_runner/compilation_files.rs b/src/cargo/core/compiler/build_runner/compilation_files.rs index 8cb56d22adb..c8431dfb88d 100644 --- a/src/cargo/core/compiler/build_runner/compilation_files.rs +++ b/src/cargo/core/compiler/build_runner/compilation_files.rs @@ -523,18 +523,33 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> { vec![] } CompileMode::Doc => { - let path = if bcx.build_config.intent.wants_doc_json_output() { - self.output_dir(unit) - .join(format!("{}.json", unit.target.crate_name())) + let wants_json_doc = bcx.build_config.intent.wants_doc_json_output(); + + let path = if wants_json_doc { + // Always use 'new' layout for '--output-format=json'. + let crate_name = unit.target.crate_name(); + self.out_dir_new_layout(unit) + .join(format!("{crate_name}.json")) } else { self.output_dir(unit) .join(unit.target.crate_name()) .join("index.html") }; + // Uplift if output is json, from 'new' layout location for backward compatibility + // See #16773. + let hardlink = if wants_json_doc { + Some( + self.output_dir(unit) + .join(format!("{}.json", unit.target.crate_name())), + ) + } else { + None + }; + let mut outputs = vec![OutputFile { path, - hardlink: None, + hardlink, export_path: None, flavor: FileFlavor::Normal, }]; diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index c67a2085ff1..ab26b27d9a3 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -868,7 +868,16 @@ fn prepare_rustdoc(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> CargoResu add_cap_lints(bcx, unit, &mut rustdoc); unit.kind.add_target_arg(&mut rustdoc); - let doc_dir = build_runner.files().output_dir(unit); + + let doc_dir = if build_runner.bcx.build_config.intent.wants_doc_json_output() { + // Always use new layout for '--output-format=json'. + // In fix for https://github.com/rust-lang/cargo/issues/16291 + + build_runner.files().out_dir_new_layout(unit) + } else { + build_runner.files().output_dir(unit) + }; + rustdoc.arg("-o").arg(&doc_dir); rustdoc.args(&features_args(unit)); rustdoc.args(&check_cfg_args(unit)); @@ -972,6 +981,7 @@ fn rustdoc(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> CargoResult, unit: &Unit) -> CargoResult