From 7889c25f8b49cf66bc935f3ec176d6dd14b61507 Mon Sep 17 00:00:00 2001 From: Alona Enraght-Moony Date: Wed, 26 Nov 2025 23:23:24 +0000 Subject: [PATCH 1/2] rustdoc-json: Make `-o`/`--output` control file, not directory Historically in rustdoc, `-o`, `--out-dir` and `--output` all controlled the output directory, as rustdoc produced many HTML files, so it didn't make sense to give the path to an individual one. This doesn't make sense for rustdoc-json, where only a single file is produced. This commit makes `-o`/`--output` give a path to a file, rather than a directory. `--out-dir` is uneffected. This commit doesn't effect the HTML behaviour. Works towards https://www.github.com/rust-lang/rust/issues/142370 and https://www.github.com/rust-lang/cargo/issues/16291. Zulip Discussion: https://rust-lang.zulipchat.com/#narrow/channel/246057-t-cargo/topic/rustdoc-json.20output.20filename.20design/with/560468799 --- src/librustdoc/config.rs | 41 ++++++++++++----- src/librustdoc/formats/cache.rs | 2 +- src/librustdoc/html/render/context.rs | 6 +-- src/librustdoc/html/render/write_shared.rs | 4 +- src/librustdoc/json/mod.rs | 53 ++++++++++++++-------- src/librustdoc/lib.rs | 6 +-- src/librustdoc/markdown.rs | 6 +-- src/tools/compiletest/src/runtest.rs | 9 +--- 8 files changed, 77 insertions(+), 50 deletions(-) diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 5d16dff24c69a..a54f797c4dd36 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -222,7 +222,7 @@ impl fmt::Debug for Options { #[derive(Clone, Debug)] pub(crate) struct RenderOptions { /// Output directory to generate docs into. Defaults to `doc`. - pub(crate) output: PathBuf, + pub(crate) output: Output, /// External files to insert into generated pages. pub(crate) external_html: ExternalHtml, /// A pre-populated `IdMap` with the default headings and any headings added by Markdown files @@ -288,9 +288,6 @@ pub(crate) struct RenderOptions { pub(crate) no_emit_shared: bool, /// If `true`, HTML source code pages won't be generated. pub(crate) html_no_source: bool, - /// This field is only used for the JSON output. If it's set to true, no file will be created - /// and content will be displayed in stdout directly. - pub(crate) output_to_stdout: bool, /// Whether we should read or write rendered cross-crate info in the doc root. pub(crate) should_merge: ShouldMerge, /// Path to crate-info for external crates. @@ -303,6 +300,27 @@ pub(crate) struct RenderOptions { pub(crate) generate_macro_expansion: bool, } +#[derive(Clone, Debug)] +pub(crate) enum Output { + /// `-o` / `--output` + Output(PathBuf), + /// `--out-dir` + OutDir(PathBuf), +} + +impl Output { + /// The output directory, assuming we're in the HTML backend. + /// + /// This should *not* be called from the JSON backend, as that treets + /// `--output` and `--output-dir` differently + pub fn output_dir_html(&self) -> &Path { + match self { + Output::Output(o) => Path::new(o), + Output::OutDir(o) => Path::new(o), + } + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum ModuleSorting { DeclarationOrder, @@ -346,6 +364,11 @@ impl RenderOptions { } None } + + /// See [`Output::output_dir_html`]. + pub(crate) fn output_dir_html(&self) -> &Path { + self.output.output_dir_html() + } } /// Create the input (string or file path) @@ -632,18 +655,15 @@ impl Options { dcx.fatal("the `--test` flag must be passed to enable `--no-run`"); } - let mut output_to_stdout = false; let test_builder_wrappers = matches.opt_strs("test-builder-wrapper").iter().map(PathBuf::from).collect(); let output = match (matches.opt_str("out-dir"), matches.opt_str("output")) { (Some(_), Some(_)) => { dcx.fatal("cannot use both 'out-dir' and 'output' at once"); } - (Some(out_dir), None) | (None, Some(out_dir)) => { - output_to_stdout = out_dir == "-"; - PathBuf::from(out_dir) - } - (None, None) => PathBuf::from("doc"), + (Some(out_dir), None) => Output::OutDir(PathBuf::from(out_dir)), + (None, Some(output)) => Output::Output(PathBuf::from(output)), + (None, None) => Output::OutDir(PathBuf::from("doc")), }; let cfgs = matches.opt_strs("cfg"); @@ -897,7 +917,6 @@ impl Options { call_locations, no_emit_shared: false, html_no_source, - output_to_stdout, should_merge, include_parts_dir, parts_out_dir, diff --git a/src/librustdoc/formats/cache.rs b/src/librustdoc/formats/cache.rs index 56f10b03d62de..dfd3995abf863 100644 --- a/src/librustdoc/formats/cache.rs +++ b/src/librustdoc/formats/cache.rs @@ -170,7 +170,7 @@ impl Cache { cx.cache.traits = mem::take(&mut krate.external_traits); let extern_url_takes_precedence = render_options.extern_html_root_takes_precedence; - let dst = &render_options.output; + let dst = render_options.output_dir_html(); // FIXME: This is sus for json // Make `--extern-html-root-url` support the same names as `--extern` whenever possible let cstore = CStore::from_tcx(tcx); diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index e42997d5b4a14..bcc9367cc603f 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -580,12 +580,12 @@ impl<'tcx> Context<'tcx> { expanded_codes, }; - let dst = output; - scx.ensure_dir(&dst)?; + let dst = output.output_dir_html(); + scx.ensure_dir(dst)?; let mut cx = Context { current: Vec::new(), - dst, + dst: dst.to_path_buf(), id_map: RefCell::new(id_map), deref_id_map: Default::default(), shared: scx, diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index 9a8df53931394..a46805758e838 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -39,7 +39,7 @@ use serde::{Deserialize, Serialize, Serializer}; use super::{Context, RenderMode, collect_paths_for_type, ensure_trailing_slash}; use crate::clean::{Crate, Item, ItemId, ItemKind}; -use crate::config::{EmitType, PathToParts, RenderOptions, ShouldMerge}; +use crate::config::{self, EmitType, PathToParts, RenderOptions, ShouldMerge}; use crate::docfs::PathError; use crate::error::Error; use crate::formats::Impl; @@ -109,7 +109,7 @@ pub(crate) fn write_shared( match &opt.index_page { Some(index_page) if opt.enable_index_page => { let mut md_opts = opt.clone(); - md_opts.output = cx.dst.clone(); + md_opts.output = config::Output::OutDir(cx.dst.clone()); md_opts.external_html = cx.shared.layout.external_html.clone(); try_err!( crate::markdown::render_and_write(index_page, md_opts, cx.shared.edition()), diff --git a/src/librustdoc/json/mod.rs b/src/librustdoc/json/mod.rs index 37d456ae796bd..d1add9b0f6cc6 100644 --- a/src/librustdoc/json/mod.rs +++ b/src/librustdoc/json/mod.rs @@ -27,7 +27,7 @@ use tracing::{debug, trace}; use crate::clean::ItemKind; use crate::clean::types::{ExternalCrate, ExternalLocation}; -use crate::config::RenderOptions; +use crate::config::{self, RenderOptions}; use crate::docfs::PathError; use crate::error::Error; use crate::formats::FormatRenderer; @@ -40,10 +40,7 @@ pub(crate) struct JsonRenderer<'tcx> { /// A mapping of IDs that contains all local items for this crate which gets output as a top /// level field of the JSON blob. index: FxHashMap, - /// The directory where the JSON blob should be written to. - /// - /// If this is `None`, the blob will be printed to `stdout` instead. - out_dir: Option, + output: config::Output, cache: Rc, imported_items: DefIdSet, id_interner: RefCell, @@ -137,7 +134,7 @@ impl<'tcx> JsonRenderer<'tcx> { JsonRenderer { tcx, index: FxHashMap::default(), - out_dir: if options.output_to_stdout { None } else { Some(options.output) }, + output: options.output, cache: Rc::new(cache), imported_items, id_interner: Default::default(), @@ -316,20 +313,36 @@ impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> { target, format_version: types::FORMAT_VERSION, }; - if let Some(ref out_dir) = self.out_dir { - try_err!(create_dir_all(out_dir), out_dir); - - let mut p = out_dir.clone(); - p.push(output_crate.index.get(&output_crate.root).unwrap().name.clone().unwrap()); - p.set_extension("json"); - - self.serialize_and_write( - output_crate, - try_err!(File::create_buffered(&p), p), - &p.display().to_string(), - ) - } else { - self.serialize_and_write(output_crate, BufWriter::new(stdout().lock()), "") + + match &self.output { + config::Output::OutDir(out_dir) => { + try_err!(create_dir_all(out_dir), out_dir); + + let mut p = PathBuf::from(out_dir); + p.push(output_crate.index.get(&output_crate.root).unwrap().name.clone().unwrap()); + p.set_extension("json"); + + self.serialize_and_write( + output_crate, + try_err!(File::create_buffered(&p), p), + &p.display().to_string(), + ) + } + config::Output::Output(p) => { + if p == "-" { + self.serialize_and_write( + output_crate, + BufWriter::new(stdout().lock()), + "", + ) + } else { + self.serialize_and_write( + output_crate, + try_err!(File::create_buffered(p), p), + &p.display().to_string(), + ) + } + } } } } diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index e4601bfb20d7d..b3dfab4fd7079 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -215,12 +215,12 @@ fn opts() -> Vec { opt( Stable, Opt, - "", + "o", "output", "Which directory to place the output. This option is deprecated, use --out-dir instead.", "PATH", ), - opt(Stable, Opt, "o", "out-dir", "which directory to place the output", "PATH"), + opt(Stable, Opt, "", "out-dir", "which directory to place the output", "PATH"), opt(Stable, Opt, "", "crate-name", "specify the name of this crate", "NAME"), make_crate_type_option(), opt(Stable, Multi, "L", "library-path", "directory to add to crate search path", "DIR"), @@ -757,7 +757,7 @@ fn run_merge_finalize(opt: config::RenderOptions) -> Result<(), error::Error> { let include_sources = !opt.html_no_source; html::render::write_not_crate_specific( &crates, - &opt.output, + &opt.output_dir_html(), &opt, &opt.themes, opt.extension_css.as_deref(), diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs index 4ca2c104888bb..7e3e12337cd95 100644 --- a/src/librustdoc/markdown.rs +++ b/src/librustdoc/markdown.rs @@ -48,12 +48,12 @@ pub(crate) fn render_and_write>( options: RenderOptions, edition: Edition, ) -> Result<(), String> { - if let Err(e) = create_dir_all(&options.output) { - return Err(format!("{output}: {e}", output = options.output.display())); + let mut output = options.output_dir_html().to_path_buf(); + if let Err(e) = create_dir_all(&output) { + return Err(format!("{output}: {e}", output = output.display())); } let input = input.as_ref(); - let mut output = options.output; output.push(input.file_name().unwrap()); output.set_extension("html"); diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs index ddcda91c13d01..fa80f948b570b 100644 --- a/src/tools/compiletest/src/runtest.rs +++ b/src/tools/compiletest/src/runtest.rs @@ -1063,7 +1063,7 @@ impl<'test> TestCx<'test> { .arg(self.config.run_lib_path.as_path()) .arg("-L") .arg(aux_dir) - .arg("-o") + .arg("--out-dir") .arg(out_dir.as_ref()) .arg("--deny") .arg("warnings") @@ -1806,12 +1806,7 @@ impl<'test> TestCx<'test> { rustc.arg("-o").arg(path); } TargetLocation::ThisDirectory(path) => { - if is_rustdoc { - // `rustdoc` uses `-o` for the output directory. - rustc.arg("-o").arg(path); - } else { - rustc.arg("--out-dir").arg(path); - } + rustc.arg("--out-dir").arg(path); } } From 33bd1b6ebe9db13d48bec6e87b56e7c63748add7 Mon Sep 17 00:00:00 2001 From: Alona Enraght-Moony Date: Thu, 27 Nov 2025 00:45:55 +0000 Subject: [PATCH 2/2] [squashme] test fixes --- .../output-default.stdout | 4 ++-- tests/run-make/rustdoc-output-stdout/rmake.rs | 22 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/run-make/rustdoc-default-output/output-default.stdout b/tests/run-make/rustdoc-default-output/output-default.stdout index 49eaf7e2e1e0e..b1fd70a3f8791 100644 --- a/tests/run-make/rustdoc-default-output/output-default.stdout +++ b/tests/run-make/rustdoc-default-output/output-default.stdout @@ -6,9 +6,9 @@ Options: -v, --verbose use verbose output -w, --output-format [html] the output type to write - --output PATH Which directory to place the output. This option is + -o, --output PATH Which directory to place the output. This option is deprecated, use --out-dir instead. - -o, --out-dir PATH which directory to place the output + --out-dir PATH which directory to place the output --crate-name NAME specify the name of this crate --crate-type diff --git a/tests/run-make/rustdoc-output-stdout/rmake.rs b/tests/run-make/rustdoc-output-stdout/rmake.rs index e6c007978bdca..291330b476545 100644 --- a/tests/run-make/rustdoc-output-stdout/rmake.rs +++ b/tests/run-make/rustdoc-output-stdout/rmake.rs @@ -7,9 +7,22 @@ use run_make_support::path_helpers::{cwd, has_extension, read_dir_entries_recurs use run_make_support::{rustdoc, serde_json}; fn main() { + check("-o"); + check("--output"); + + // Then we check it didn't generate any JSON file. + read_dir_entries_recursive(cwd(), |path| { + if path.is_file() && has_extension(path, "json") { + panic!("Found a JSON file {path:?}"); + } + }); +} + +fn check(out_arg: &str) { let json_string = rustdoc() .input("foo.rs") - .out_dir("-") + .arg(out_arg) + .arg("-") .arg("-Zunstable-options") .output_format("json") .run() @@ -26,11 +39,4 @@ fn main() { .as_i64() .expect("json output should contain format_version field"); assert!(format_version > 30); - - // Then we check it didn't generate any JSON file. - read_dir_entries_recursive(cwd(), |path| { - if path.is_file() && has_extension(path, "json") { - panic!("Found a JSON file {path:?}"); - } - }); }