about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEtomicBomb <ethan@ethan.ws>2024-08-20 23:34:19 +0000
committerethan <ethan@localhost.localdomain>2024-09-07 18:59:01 -0400
commit2e1cba64157c9bf60f7bc010268ac0bd5ca9438c (patch)
tree559ba4e87e79ca379b827496a0bcc61ef34637df
parent3782251c2c6e6e7fedb806a97664ee38ced1677b (diff)
downloadrust-2e1cba64157c9bf60f7bc010268ac0bd5ca9438c.tar.gz
rust-2e1cba64157c9bf60f7bc010268ac0bd5ca9438c.zip
rfc#3662 changes under unstable flags
* All new functionality is under unstable options
* Adds `--merge=shared|none|finalize` flags
* Adds `--parts-out-dir=<crate specific directory>` for `--merge=none`
to write cross-crate info file for a single crate
* Adds `--include-parts-dir=<previously specified directory>` for
`--merge=finalize` to write cross-crate info files
* update tests/run-make/rustdoc-default-output/rmake.rs golden
-rw-r--r--src/librustdoc/config.rs109
-rw-r--r--src/librustdoc/html/render/context.rs175
-rw-r--r--src/librustdoc/html/render/mod.rs1
-rw-r--r--src/librustdoc/html/render/write_shared.rs178
-rw-r--r--src/librustdoc/html/render/write_shared/tests.rs10
-rw-r--r--src/librustdoc/lib.rs64
-rw-r--r--tests/run-make/rustdoc-default-output/output-default.stdout17
7 files changed, 419 insertions, 135 deletions
diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs
index 7e4e8fd506f..b3c87a72508 100644
--- a/src/librustdoc/config.rs
+++ b/src/librustdoc/config.rs
@@ -53,6 +53,14 @@ impl TryFrom<&str> for OutputFormat {
     }
 }
 
+/// Either an input crate, markdown file, or nothing (--merge=finalize).
+pub(crate) enum InputMode {
+    /// The `--merge=finalize` step does not need an input crate to rustdoc.
+    NoInputMergeFinalize,
+    /// A crate or markdown file.
+    HasFile(Input),
+}
+
 /// Configuration options for rustdoc.
 #[derive(Clone)]
 pub(crate) struct Options {
@@ -286,6 +294,12 @@ pub(crate) struct RenderOptions {
     /// 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.
+    pub(crate) include_parts_dir: Vec<PathToParts>,
+    /// Where to write crate-info
+    pub(crate) parts_out_dir: Option<PathToParts>,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -345,7 +359,7 @@ impl Options {
         early_dcx: &mut EarlyDiagCtxt,
         matches: &getopts::Matches,
         args: Vec<String>,
-    ) -> Option<(Input, Options, RenderOptions)> {
+    ) -> Option<(InputMode, Options, RenderOptions)> {
         // Check for unstable options.
         nightly_options::check_nightly_options(early_dcx, matches, &opts());
 
@@ -475,15 +489,17 @@ impl Options {
         let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(early_dcx, matches);
 
         let input = if describe_lints {
-            "" // dummy, this won't be used
+            InputMode::HasFile(make_input(early_dcx, ""))
         } else {
             match matches.free.as_slice() {
+                [] if matches.opt_str("merge").as_deref() == Some("finalize") => {
+                    InputMode::NoInputMergeFinalize
+                }
                 [] => dcx.fatal("missing file operand"),
-                [input] => input,
+                [input] => InputMode::HasFile(make_input(early_dcx, input)),
                 _ => dcx.fatal("too many file operands"),
             }
         };
-        let input = make_input(early_dcx, input);
 
         let externs = parse_externs(early_dcx, matches, &unstable_opts);
         let extern_html_root_urls = match parse_extern_html_roots(matches) {
@@ -491,6 +507,16 @@ impl Options {
             Err(err) => dcx.fatal(err),
         };
 
+        let parts_out_dir =
+            match matches.opt_str("parts-out-dir").map(|p| PathToParts::from_flag(p)).transpose() {
+                Ok(parts_out_dir) => parts_out_dir,
+                Err(e) => dcx.fatal(e),
+            };
+        let include_parts_dir = match parse_include_parts_dir(matches) {
+            Ok(include_parts_dir) => include_parts_dir,
+            Err(e) => dcx.fatal(e),
+        };
+
         let default_settings: Vec<Vec<(String, String)>> = vec![
             matches
                 .opt_str("default-theme")
@@ -732,6 +758,10 @@ impl Options {
         let extern_html_root_takes_precedence =
             matches.opt_present("extern-html-root-takes-precedence");
         let html_no_source = matches.opt_present("html-no-source");
+        let should_merge = match parse_merge(matches) {
+            Ok(result) => result,
+            Err(e) => dcx.fatal(format!("--merge option error: {e}")),
+        };
 
         if generate_link_to_definition && (show_coverage || output_format != OutputFormat::Html) {
             dcx.struct_warn(
@@ -819,6 +849,9 @@ impl Options {
             no_emit_shared: false,
             html_no_source,
             output_to_stdout,
+            should_merge,
+            include_parts_dir,
+            parts_out_dir,
         };
         Some((input, options, render_options))
     }
@@ -894,3 +927,71 @@ fn parse_extern_html_roots(
     }
     Ok(externs)
 }
+
+/// Path directly to crate-info file.
+///
+/// For example, `/home/user/project/target/doc.parts/<crate>/crate-info`.
+#[derive(Clone, Debug)]
+pub(crate) struct PathToParts(pub(crate) PathBuf);
+
+impl PathToParts {
+    fn from_flag(path: String) -> Result<PathToParts, String> {
+        let mut path = PathBuf::from(path);
+        // check here is for diagnostics
+        if path.exists() && !path.is_dir() {
+            Err(format!(
+                "--parts-out-dir and --include-parts-dir expect directories, found: {}",
+                path.display(),
+            ))
+        } else {
+            // if it doesn't exist, we'll create it. worry about that in write_shared
+            path.push("crate-info");
+            Ok(PathToParts(path))
+        }
+    }
+}
+
+/// Reports error if --include-parts-dir / crate-info is not a file
+fn parse_include_parts_dir(m: &getopts::Matches) -> Result<Vec<PathToParts>, String> {
+    let mut ret = Vec::new();
+    for p in m.opt_strs("include-parts-dir") {
+        let p = PathToParts::from_flag(p)?;
+        // this is just for diagnostic
+        if !p.0.is_file() {
+            return Err(format!("--include-parts-dir expected {} to be a file", p.0.display()));
+        }
+        ret.push(p);
+    }
+    Ok(ret)
+}
+
+/// Controls merging of cross-crate information
+#[derive(Debug, Clone)]
+pub(crate) struct ShouldMerge {
+    /// Should we append to existing cci in the doc root
+    pub(crate) read_rendered_cci: bool,
+    /// Should we write cci to the doc root
+    pub(crate) write_rendered_cci: bool,
+}
+
+/// Extracts read_rendered_cci and write_rendered_cci from command line arguments, or
+/// reports an error if an invalid option was provided
+fn parse_merge(m: &getopts::Matches) -> Result<ShouldMerge, &'static str> {
+    match m.opt_str("merge").as_deref() {
+        // default = read-write
+        None => Ok(ShouldMerge { read_rendered_cci: true, write_rendered_cci: true }),
+        Some("none") if m.opt_present("include-parts-dir") => {
+            Err("--include-parts-dir not allowed if --merge=none")
+        }
+        Some("none") => Ok(ShouldMerge { read_rendered_cci: false, write_rendered_cci: false }),
+        Some("shared") if m.opt_present("parts-out-dir") || m.opt_present("include-parts-dir") => {
+            Err("--parts-out-dir and --include-parts-dir not allowed if --merge=shared")
+        }
+        Some("shared") => Ok(ShouldMerge { read_rendered_cci: true, write_rendered_cci: true }),
+        Some("finalize") if m.opt_present("parts-out-dir") => {
+            Err("--parts-out-dir not allowed if --merge=finalize")
+        }
+        Some("finalize") => Ok(ShouldMerge { read_rendered_cci: false, write_rendered_cci: true }),
+        Some(_) => Err("argument to --merge must be `none`, `shared`, or `finalize`"),
+    }
+}
diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs
index 9fe43e428f2..902ccd23e8a 100644
--- a/src/librustdoc/html/render/context.rs
+++ b/src/librustdoc/html/render/context.rs
@@ -16,12 +16,11 @@ use tracing::info;
 
 use super::print_item::{full_path, item_path, print_item};
 use super::sidebar::{print_sidebar, sidebar_module_like, ModuleLike, Sidebar};
-use super::write_shared::write_shared;
 use super::{collect_spans_and_sources, scrape_examples_help, AllTypes, LinkFromSrc, StylePath};
 use crate::clean::types::ExternalLocation;
 use crate::clean::utils::has_doc_flag;
 use crate::clean::{self, ExternalCrate};
-use crate::config::{ModuleSorting, RenderOptions};
+use crate::config::{ModuleSorting, RenderOptions, ShouldMerge};
 use crate::docfs::{DocFS, PathError};
 use crate::error::Error;
 use crate::formats::cache::Cache;
@@ -30,6 +29,7 @@ use crate::formats::FormatRenderer;
 use crate::html::escape::Escape;
 use crate::html::format::{join_with_double_colon, Buffer};
 use crate::html::markdown::{self, plain_text_summary, ErrorCodes, IdMap};
+use crate::html::render::write_shared::write_shared;
 use crate::html::url_parts_builder::UrlPartsBuilder;
 use crate::html::{layout, sources, static_files};
 use crate::scrape_examples::AllCallLocations;
@@ -128,8 +128,10 @@ pub(crate) struct SharedContext<'tcx> {
     pub(crate) span_correspondence_map: FxHashMap<rustc_span::Span, LinkFromSrc>,
     /// The [`Cache`] used during rendering.
     pub(crate) cache: Cache,
-
     pub(crate) call_locations: AllCallLocations,
+    /// Controls whether we read / write to cci files in the doc root. Defaults read=true,
+    /// write=true
+    should_merge: ShouldMerge,
 }
 
 impl SharedContext<'_> {
@@ -551,6 +553,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
             span_correspondence_map: matches,
             cache,
             call_locations,
+            should_merge: options.should_merge,
         };
 
         let dst = output;
@@ -640,92 +643,96 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
         );
         shared.fs.write(final_file, v)?;
 
-        // Generating settings page.
-        page.title = "Settings";
-        page.description = "Settings of Rustdoc";
-        page.root_path = "./";
-        page.rust_logo = true;
+        // if to avoid writing help, settings files to doc root unless we're on the final invocation
+        if shared.should_merge.write_rendered_cci {
+            // Generating settings page.
+            page.title = "Settings";
+            page.description = "Settings of Rustdoc";
+            page.root_path = "./";
+            page.rust_logo = true;
 
-        let sidebar = "<h2 class=\"location\">Settings</h2><div class=\"sidebar-elems\"></div>";
-        let v = layout::render(
-            &shared.layout,
-            &page,
-            sidebar,
-            |buf: &mut Buffer| {
-                write!(
-                    buf,
-                    "<div class=\"main-heading\">\
-                     <h1>Rustdoc settings</h1>\
-                     <span class=\"out-of-band\">\
-                         <a id=\"back\" href=\"javascript:void(0)\" onclick=\"history.back();\">\
-                            Back\
-                        </a>\
-                     </span>\
-                     </div>\
-                     <noscript>\
-                        <section>\
-                            You need to enable JavaScript be able to update your settings.\
-                        </section>\
-                     </noscript>\
-                     <script defer src=\"{static_root_path}{settings_js}\"></script>",
-                    static_root_path = page.get_static_root_path(),
-                    settings_js = static_files::STATIC_FILES.settings_js,
-                );
-                // Pre-load all theme CSS files, so that switching feels seamless.
-                //
-                // When loading settings.html as a popover, the equivalent HTML is
-                // generated in main.js.
-                for file in &shared.style_files {
-                    if let Ok(theme) = file.basename() {
-                        write!(
-                            buf,
-                            "<link rel=\"preload\" href=\"{root_path}{theme}{suffix}.css\" \
-                                as=\"style\">",
-                            root_path = page.static_root_path.unwrap_or(""),
-                            suffix = page.resource_suffix,
-                        );
+            let sidebar = "<h2 class=\"location\">Settings</h2><div class=\"sidebar-elems\"></div>";
+            let v = layout::render(
+                &shared.layout,
+                &page,
+                sidebar,
+                |buf: &mut Buffer| {
+                    write!(
+                        buf,
+                        "<div class=\"main-heading\">\
+                         <h1>Rustdoc settings</h1>\
+                         <span class=\"out-of-band\">\
+                             <a id=\"back\" href=\"javascript:void(0)\" onclick=\"history.back();\">\
+                                Back\
+                            </a>\
+                         </span>\
+                         </div>\
+                         <noscript>\
+                            <section>\
+                                You need to enable JavaScript be able to update your settings.\
+                            </section>\
+                         </noscript>\
+                         <script defer src=\"{static_root_path}{settings_js}\"></script>",
+                        static_root_path = page.get_static_root_path(),
+                        settings_js = static_files::STATIC_FILES.settings_js,
+                    );
+                    // Pre-load all theme CSS files, so that switching feels seamless.
+                    //
+                    // When loading settings.html as a popover, the equivalent HTML is
+                    // generated in main.js.
+                    for file in &shared.style_files {
+                        if let Ok(theme) = file.basename() {
+                            write!(
+                                buf,
+                                "<link rel=\"preload\" href=\"{root_path}{theme}{suffix}.css\" \
+                                    as=\"style\">",
+                                root_path = page.static_root_path.unwrap_or(""),
+                                suffix = page.resource_suffix,
+                            );
+                        }
                     }
-                }
-            },
-            &shared.style_files,
-        );
-        shared.fs.write(settings_file, v)?;
+                },
+                &shared.style_files,
+            );
+            shared.fs.write(settings_file, v)?;
 
-        // Generating help page.
-        page.title = "Help";
-        page.description = "Documentation for Rustdoc";
-        page.root_path = "./";
-        page.rust_logo = true;
+            // Generating help page.
+            page.title = "Help";
+            page.description = "Documentation for Rustdoc";
+            page.root_path = "./";
+            page.rust_logo = true;
 
-        let sidebar = "<h2 class=\"location\">Help</h2><div class=\"sidebar-elems\"></div>";
-        let v = layout::render(
-            &shared.layout,
-            &page,
-            sidebar,
-            |buf: &mut Buffer| {
-                write!(
-                    buf,
-                    "<div class=\"main-heading\">\
-                     <h1>Rustdoc help</h1>\
-                     <span class=\"out-of-band\">\
-                         <a id=\"back\" href=\"javascript:void(0)\" onclick=\"history.back();\">\
-                            Back\
-                        </a>\
-                     </span>\
-                     </div>\
-                     <noscript>\
-                        <section>\
-                            <p>You need to enable JavaScript to use keyboard commands or search.</p>\
-                            <p>For more information, browse the <a href=\"https://doc.rust-lang.org/rustdoc/\">rustdoc handbook</a>.</p>\
-                        </section>\
-                     </noscript>",
-                )
-            },
-            &shared.style_files,
-        );
-        shared.fs.write(help_file, v)?;
+            let sidebar = "<h2 class=\"location\">Help</h2><div class=\"sidebar-elems\"></div>";
+            let v = layout::render(
+                &shared.layout,
+                &page,
+                sidebar,
+                |buf: &mut Buffer| {
+                    write!(
+                        buf,
+                        "<div class=\"main-heading\">\
+                         <h1>Rustdoc help</h1>\
+                         <span class=\"out-of-band\">\
+                             <a id=\"back\" href=\"javascript:void(0)\" onclick=\"history.back();\">\
+                                Back\
+                            </a>\
+                         </span>\
+                         </div>\
+                         <noscript>\
+                            <section>\
+                                <p>You need to enable JavaScript to use keyboard commands or search.</p>\
+                                <p>For more information, browse the <a href=\"https://doc.rust-lang.org/rustdoc/\">rustdoc handbook</a>.</p>\
+                            </section>\
+                         </noscript>",
+                    )
+                },
+                &shared.style_files,
+            );
+            shared.fs.write(help_file, v)?;
+        }
 
-        if shared.layout.scrape_examples_extension {
+        // if to avoid writing files to doc root unless we're on the final invocation
+        if shared.layout.scrape_examples_extension && shared.should_merge.write_rendered_cci {
             page.title = "About scraped examples";
             page.description = "How the scraped examples feature works in Rustdoc";
             let v = layout::render(
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index f67d006116c..245d8055552 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -61,6 +61,7 @@ use tracing::{debug, info};
 
 pub(crate) use self::context::*;
 pub(crate) use self::span_map::{collect_spans_and_sources, LinkFromSrc};
+pub(crate) use self::write_shared::*;
 use crate::clean::{self, ItemId, RenderedLink};
 use crate::error::Error;
 use crate::formats::cache::Cache;
diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs
index b0af8d8c23e..302c454b2a1 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::{collect_paths_for_type, ensure_trailing_slash, Context, RenderMode};
 use crate::clean::{Crate, Item, ItemId, ItemKind};
-use crate::config::{EmitType, RenderOptions};
+use crate::config::{EmitType, PathToParts, RenderOptions, ShouldMerge};
 use crate::docfs::PathError;
 use crate::error::Error;
 use crate::formats::cache::Cache;
@@ -50,12 +50,11 @@ use crate::html::layout;
 use crate::html::render::ordered_json::{EscapedJson, OrderedJson};
 use crate::html::render::search_index::{build_index, SerializedSearchIndex};
 use crate::html::render::sorted_template::{self, FileFormat, SortedTemplate};
-use crate::html::render::{AssocItemLink, ImplRenderingParameters};
+use crate::html::render::{AssocItemLink, ImplRenderingParameters, StylePath};
 use crate::html::static_files::{self, suffix_path};
 use crate::visit::DocVisitor;
 use crate::{try_err, try_none};
 
-/// Write cross-crate information files, static files, invocation-specific files, etc. to disk
 pub(crate) fn write_shared(
     cx: &mut Context<'_>,
     krate: &Crate,
@@ -70,13 +69,14 @@ pub(crate) fn write_shared(
 
     let SerializedSearchIndex { index, desc } =
         build_index(&krate, &mut Rc::get_mut(&mut cx.shared).unwrap().cache, tcx);
-    write_search_desc(cx, &krate, &desc)?; // does not need to be merged; written unconditionally
+    write_search_desc(cx, &krate, &desc)?; // does not need to be merged
 
     let crate_name = krate.name(cx.tcx());
     let crate_name = crate_name.as_str(); // rand
     let crate_name_json = OrderedJson::serialize(crate_name).unwrap(); // "rand"
     let external_crates = hack_get_external_crate_names(&cx.dst, &cx.shared.resource_suffix)?;
     let info = CrateInfo {
+        version: CrateInfoVersion::V1,
         src_files_js: SourcesPart::get(cx, &crate_name_json)?,
         search_index_js: SearchIndexPart::get(index, &cx.shared.resource_suffix)?,
         all_crates: AllCratesPart::get(crate_name_json.clone(), &cx.shared.resource_suffix)?,
@@ -85,47 +85,103 @@ pub(crate) fn write_shared(
         type_impl: TypeAliasPart::get(cx, krate, &crate_name_json)?,
     };
 
-    let crates = vec![info]; // we have info from just one crate. rest will found in out dir
+    if let Some(parts_out_dir) = &opt.parts_out_dir {
+        create_parents(&parts_out_dir.0)?;
+        try_err!(
+            fs::write(&parts_out_dir.0, serde_json::to_string(&info).unwrap()),
+            &parts_out_dir.0
+        );
+    }
 
-    write_static_files(cx, &opt)?;
-    let dst = &cx.dst;
-    if opt.emit.is_empty() || opt.emit.contains(&EmitType::InvocationSpecific) {
-        if cx.include_sources {
-            write_rendered_cci::<SourcesPart, _>(SourcesPart::blank, dst, &crates)?;
-        }
-        write_rendered_cci::<SearchIndexPart, _>(SearchIndexPart::blank, dst, &crates)?;
-        write_rendered_cci::<AllCratesPart, _>(AllCratesPart::blank, dst, &crates)?;
-    }
-    write_rendered_cci::<TraitAliasPart, _>(TraitAliasPart::blank, dst, &crates)?;
-    write_rendered_cci::<TypeAliasPart, _>(TypeAliasPart::blank, dst, &crates)?;
-    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.external_html = cx.shared.layout.external_html.clone();
-            try_err!(
-                crate::markdown::render(&index_page, md_opts, cx.shared.edition()),
-                &index_page
-            );
-        }
-        None if opt.enable_index_page => {
-            write_rendered_cci::<CratesIndexPart, _>(|| CratesIndexPart::blank(cx), dst, &crates)?;
+    let mut crates = CrateInfo::read_many(&opt.include_parts_dir)?;
+    crates.push(info);
+
+    if opt.should_merge.write_rendered_cci {
+        write_not_crate_specific(
+            &crates,
+            &cx.dst,
+            opt,
+            &cx.shared.style_files,
+            cx.shared.layout.css_file_extension.as_deref(),
+            &cx.shared.resource_suffix,
+            cx.include_sources,
+        )?;
+        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.external_html = cx.shared.layout.external_html.clone();
+                try_err!(
+                    crate::markdown::render(&index_page, md_opts, cx.shared.edition()),
+                    &index_page
+                );
+            }
+            None if opt.enable_index_page => {
+                write_rendered_cci::<CratesIndexPart, _>(
+                    || CratesIndexPart::blank(cx),
+                    &cx.dst,
+                    &crates,
+                    &opt.should_merge,
+                )?;
+            }
+            _ => {} // they don't want an index page
         }
-        _ => {} // they don't want an index page
     }
 
     Rc::get_mut(&mut cx.shared).unwrap().fs.set_sync_only(false);
     Ok(())
 }
 
-/// Writes the static files, the style files, and the css extensions
-fn write_static_files(cx: &mut Context<'_>, options: &RenderOptions) -> Result<(), Error> {
-    let static_dir = cx.dst.join("static.files");
+/// Writes files that are written directly to the `--out-dir`, without the prefix from the current
+/// crate. These are the rendered cross-crate files that encode info from multiple crates (e.g.
+/// search index), and the static files.
+pub(crate) fn write_not_crate_specific(
+    crates: &[CrateInfo],
+    dst: &Path,
+    opt: &RenderOptions,
+    style_files: &[StylePath],
+    css_file_extension: Option<&Path>,
+    resource_suffix: &str,
+    include_sources: bool,
+) -> Result<(), Error> {
+    write_rendered_cross_crate_info(crates, dst, opt, include_sources)?;
+    write_static_files(dst, opt, style_files, css_file_extension, resource_suffix)?;
+    Ok(())
+}
 
-    cx.shared.fs.create_dir_all(&static_dir).map_err(|e| PathError::new(e, "static.files"))?;
+fn write_rendered_cross_crate_info(
+    crates: &[CrateInfo],
+    dst: &Path,
+    opt: &RenderOptions,
+    include_sources: bool,
+) -> Result<(), Error> {
+    let m = &opt.should_merge;
+    if opt.emit.is_empty() || opt.emit.contains(&EmitType::InvocationSpecific) {
+        if include_sources {
+            write_rendered_cci::<SourcesPart, _>(SourcesPart::blank, dst, &crates, m)?;
+        }
+        write_rendered_cci::<SearchIndexPart, _>(SearchIndexPart::blank, dst, &crates, m)?;
+        write_rendered_cci::<AllCratesPart, _>(AllCratesPart::blank, dst, &crates, m)?;
+    }
+    write_rendered_cci::<TraitAliasPart, _>(TraitAliasPart::blank, dst, &crates, m)?;
+    write_rendered_cci::<TypeAliasPart, _>(TypeAliasPart::blank, dst, &crates, m)?;
+    Ok(())
+}
+
+/// Writes the static files, the style files, and the css extensions.
+/// Have to be careful about these, because they write to the root out dir.
+fn write_static_files(
+    dst: &Path,
+    opt: &RenderOptions,
+    style_files: &[StylePath],
+    css_file_extension: Option<&Path>,
+    resource_suffix: &str,
+) -> Result<(), Error> {
+    let static_dir = dst.join("static.files");
+    try_err!(fs::create_dir_all(&static_dir), &static_dir);
 
     // Handle added third-party themes
-    for entry in &cx.shared.style_files {
+    for entry in style_files {
         let theme = entry.basename()?;
         let extension =
             try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
@@ -136,22 +192,24 @@ fn write_static_files(cx: &mut Context<'_>, options: &RenderOptions) -> Result<(
         }
 
         let bytes = try_err!(fs::read(&entry.path), &entry.path);
-        let filename = format!("{theme}{suffix}.{extension}", suffix = cx.shared.resource_suffix);
-        cx.shared.fs.write(cx.dst.join(filename), bytes)?;
+        let filename = format!("{theme}{resource_suffix}.{extension}");
+        let dst_filename = dst.join(filename);
+        try_err!(fs::write(&dst_filename, bytes), &dst_filename);
     }
 
     // When the user adds their own CSS files with --extend-css, we write that as an
     // invocation-specific file (that is, with a resource suffix).
-    if let Some(ref css) = cx.shared.layout.css_file_extension {
+    if let Some(css) = css_file_extension {
         let buffer = try_err!(fs::read_to_string(css), css);
-        let path = static_files::suffix_path("theme.css", &cx.shared.resource_suffix);
-        cx.shared.fs.write(cx.dst.join(path), buffer)?;
+        let path = static_files::suffix_path("theme.css", resource_suffix);
+        let dst_path = dst.join(path);
+        try_err!(fs::write(&dst_path, buffer), &dst_path);
     }
 
-    if options.emit.is_empty() || options.emit.contains(&EmitType::Toolchain) {
+    if opt.emit.is_empty() || opt.emit.contains(&EmitType::Toolchain) {
         static_files::for_each(|f: &static_files::StaticFile| {
             let filename = static_dir.join(f.output_filename());
-            cx.shared.fs.write(filename, f.minified())
+            fs::write(&filename, f.minified()).map_err(|e| PathError::new(e, &filename))
         })?;
     }
 
@@ -186,7 +244,8 @@ fn write_search_desc(
 
 /// Contains pre-rendered contents to insert into the CCI template
 #[derive(Serialize, Deserialize, Clone, Debug)]
-struct CrateInfo {
+pub(crate) struct CrateInfo {
+    version: CrateInfoVersion,
     src_files_js: PartsAndLocations<SourcesPart>,
     search_index_js: PartsAndLocations<SearchIndexPart>,
     all_crates: PartsAndLocations<AllCratesPart>,
@@ -195,6 +254,33 @@ struct CrateInfo {
     type_impl: PartsAndLocations<TypeAliasPart>,
 }
 
+impl CrateInfo {
+    /// Read all of the crate info from its location on the filesystem
+    pub(crate) fn read_many(parts_paths: &[PathToParts]) -> Result<Vec<Self>, Error> {
+        parts_paths
+            .iter()
+            .map(|parts_path| {
+                let path = &parts_path.0;
+                let parts = try_err!(fs::read(&path), &path);
+                let parts: CrateInfo = try_err!(serde_json::from_slice(&parts), &path);
+                Ok::<_, Error>(parts)
+            })
+            .collect::<Result<Vec<CrateInfo>, Error>>()
+    }
+}
+
+/// Version for the format of the crate-info file.
+///
+/// This enum should only ever have one variant, representing the current version.
+/// Gives pretty good error message about expecting the current version on deserialize.
+///
+/// Must be incremented (V2, V3, etc.) upon any changes to the search index or CrateInfo,
+/// to provide better diagnostics about including an invalid file.
+#[derive(Serialize, Deserialize, Clone, Debug)]
+enum CrateInfoVersion {
+    V1,
+}
+
 /// Paths (relative to the doc root) and their pre-merge contents
 #[derive(Serialize, Deserialize, Debug, Clone)]
 #[serde(transparent)]
@@ -900,10 +986,14 @@ fn create_parents(path: &Path) -> Result<(), Error> {
 fn read_template_or_blank<F, T: FileFormat>(
     mut make_blank: F,
     path: &Path,
+    should_merge: &ShouldMerge,
 ) -> Result<SortedTemplate<T>, Error>
 where
     F: FnMut() -> SortedTemplate<T>,
 {
+    if !should_merge.read_rendered_cci {
+        return Ok(make_blank());
+    }
     match fs::read_to_string(&path) {
         Ok(template) => Ok(try_err!(SortedTemplate::from_str(&template), &path)),
         Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(make_blank()),
@@ -916,6 +1006,7 @@ fn write_rendered_cci<T: CciPart, F>(
     mut make_blank: F,
     dst: &Path,
     crates_info: &[CrateInfo],
+    should_merge: &ShouldMerge,
 ) -> Result<(), Error>
 where
     F: FnMut() -> SortedTemplate<T::FileFormat>,
@@ -924,7 +1015,8 @@ where
     for (path, parts) in get_path_parts::<T>(dst, crates_info) {
         create_parents(&path)?;
         // read previous rendered cci from storage, append to them
-        let mut template = read_template_or_blank::<_, T::FileFormat>(&mut make_blank, &path)?;
+        let mut template =
+            read_template_or_blank::<_, T::FileFormat>(&mut make_blank, &path, should_merge)?;
         for part in parts {
             template.append(part);
         }
diff --git a/src/librustdoc/html/render/write_shared/tests.rs b/src/librustdoc/html/render/write_shared/tests.rs
index e282cd99e43..a235f1d3724 100644
--- a/src/librustdoc/html/render/write_shared/tests.rs
+++ b/src/librustdoc/html/render/write_shared/tests.rs
@@ -1,3 +1,4 @@
+use crate::config::ShouldMerge;
 use crate::html::render::ordered_json::{EscapedJson, OrderedJson};
 use crate::html::render::sorted_template::{Html, SortedTemplate};
 use crate::html::render::write_shared::*;
@@ -192,16 +193,17 @@ fn read_template_test() {
     let path = path.path().join("file.html");
     let make_blank = || SortedTemplate::<Html>::from_before_after("<div>", "</div>");
 
-    let template = read_template_or_blank(make_blank, &path).unwrap();
+    let should_merge = ShouldMerge { read_rendered_cci: true, write_rendered_cci: true };
+    let template = read_template_or_blank(make_blank, &path, &should_merge).unwrap();
     assert_eq!(but_last_line(&template.to_string()), "<div></div>");
     fs::write(&path, template.to_string()).unwrap();
-    let mut template = read_template_or_blank(make_blank, &path).unwrap();
+    let mut template = read_template_or_blank(make_blank, &path, &should_merge).unwrap();
     template.append("<img/>".to_string());
     fs::write(&path, template.to_string()).unwrap();
-    let mut template = read_template_or_blank(make_blank, &path).unwrap();
+    let mut template = read_template_or_blank(make_blank, &path, &should_merge).unwrap();
     template.append("<br/>".to_string());
     fs::write(&path, template.to_string()).unwrap();
-    let template = read_template_or_blank(make_blank, &path).unwrap();
+    let template = read_template_or_blank(make_blank, &path, &should_merge).unwrap();
 
     assert_eq!(but_last_line(&template.to_string()), "<div><br/><img/></div>");
 }
diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs
index e470aef3643..6649e1721a4 100644
--- a/src/librustdoc/lib.rs
+++ b/src/librustdoc/lib.rs
@@ -603,6 +603,33 @@ fn opts() -> Vec<RustcOptGroup> {
                 "path to function call information (for displaying examples in the documentation)",
             )
         }),
+        unstable("merge", |o| {
+            o.optopt(
+                "",
+                "merge",
+                "Controls how rustdoc handles files from previously documented crates in the doc root
+                      none = Do not write cross-crate information to the --out-dir
+                      shared = Append current crate's info to files found in the --out-dir
+                      finalize = Write current crate's info and --include-parts-dir info to the --out-dir, overwriting conflicting files",
+                "none|shared|finalize",
+            )
+        }),
+        unstable("parts-out-dir", |o| {
+            o.optopt(
+                "",
+                "parts-out-dir",
+                "Writes trait implementations and other info for the current crate to provided path. Only use with --merge=none",
+                "path/to/doc.parts/<crate-name>",
+            )
+        }),
+        unstable("include-parts-dir", |o| {
+            o.optmulti(
+                "",
+                "include-parts-dir",
+                "Includes trait implementations and other crate info from provided path. Only use with --merge=finalize",
+                "path/to/doc.parts/<crate-name>",
+            )
+        }),
         // deprecated / removed options
         unstable("disable-minification", |o| o.optflagmulti("", "disable-minification", "removed")),
         stable("plugin-path", |o| {
@@ -697,6 +724,32 @@ fn run_renderer<'tcx, T: formats::FormatRenderer<'tcx>>(
     }
 }
 
+/// Renders and writes cross-crate info files, like the search index. This function exists so that
+/// we can run rustdoc without a crate root in the `--merge=finalize` mode. Cross-crate info files
+/// discovered via `--include-parts-dir` are combined and written to the doc root.
+fn run_merge_finalize(opt: config::RenderOptions) -> Result<(), error::Error> {
+    assert!(
+        opt.should_merge.write_rendered_cci,
+        "config.rs only allows us to return InputMode::NoInputMergeFinalize if --merge=finalize"
+    );
+    assert!(
+        !opt.should_merge.read_rendered_cci,
+        "config.rs only allows us to return InputMode::NoInputMergeFinalize if --merge=finalize"
+    );
+    let crates = html::render::CrateInfo::read_many(&opt.include_parts_dir)?;
+    let include_sources = !opt.html_no_source;
+    html::render::write_not_crate_specific(
+        &crates,
+        &opt.output,
+        &opt,
+        &opt.themes,
+        opt.extension_css.as_deref(),
+        &opt.resource_suffix,
+        include_sources,
+    )?;
+    Ok(())
+}
+
 fn main_args(
     early_dcx: &mut EarlyDiagCtxt,
     at_args: &[String],
@@ -737,6 +790,17 @@ fn main_args(
         core::new_dcx(options.error_format, None, options.diagnostic_width, &options.unstable_opts);
     let dcx = dcx.handle();
 
+    let input = match input {
+        config::InputMode::HasFile(input) => input,
+        config::InputMode::NoInputMergeFinalize => {
+            return wrap_return(
+                dcx,
+                run_merge_finalize(render_options)
+                    .map_err(|e| format!("could not write merged cross-crate info: {e}")),
+            );
+        }
+    };
+
     match (options.should_test, config::markdown_input(&input)) {
         (true, Some(_)) => return wrap_return(dcx, doctest::test_markdown(&input, options)),
         (true, None) => return doctest::run(dcx, input, options),
diff --git a/tests/run-make/rustdoc-default-output/output-default.stdout b/tests/run-make/rustdoc-default-output/output-default.stdout
index bc38d9e9dc6..125443ce619 100644
--- a/tests/run-make/rustdoc-default-output/output-default.stdout
+++ b/tests/run-make/rustdoc-default-output/output-default.stdout
@@ -172,6 +172,23 @@ Options:
         --scrape-tests  Include test code when scraping examples
         --with-examples path to function call information (for displaying examples in the documentation)
                         
+        --merge none|shared|finalize
+                        Controls how rustdoc handles files from previously
+                        documented crates in the doc root
+                        none = Do not write cross-crate information to the
+                        --out-dir
+                        shared = Append current crate's info to files found in
+                        the --out-dir
+                        finalize = Write current crate's info and
+                        --include-parts-dir info to the --out-dir, overwriting
+                        conflicting files
+        --parts-out-dir path/to/doc.parts/<crate-name>
+                        Writes trait implementations and other info for the
+                        current crate to provided path. Only use with
+                        --merge=none
+        --include-parts-dir path/to/doc.parts/<crate-name>
+                        Includes trait implementations and other crate info
+                        from provided path. Only use with --merge=finalize
         --disable-minification 
                         removed
         --plugin-path DIR