about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-09-11 08:05:00 +0000
committerbors <bors@rust-lang.org>2024-09-11 08:05:00 +0000
commit59835ae7594246736ead0e1e50264e0ec6b35fc2 (patch)
tree4e12a81b1412673739f09c93265ab140894f7fbb /src
parent79d4cc9d7ce20cfe2300e33388bc1649bb3e7270 (diff)
parent7e6ce60ffbbe58cbc38181ad281c99196613bb8b (diff)
downloadrust-59835ae7594246736ead0e1e50264e0ec6b35fc2.tar.gz
rust-59835ae7594246736ead0e1e50264e0ec6b35fc2.zip
Auto merge of #3878 - rust-lang:rustup-2024-09-11, r=RalfJung
Automatic Rustup
Diffstat (limited to 'src')
-rw-r--r--src/bootstrap/defaults/config.compiler.toml3
-rw-r--r--src/bootstrap/download-ci-llvm-stamp2
-rw-r--r--src/bootstrap/mk/Makefile.in8
-rw-r--r--src/bootstrap/src/core/config/config.rs3
-rw-r--r--src/bootstrap/src/utils/change_tracker.rs5
-rw-r--r--src/librustdoc/config.rs125
-rw-r--r--src/librustdoc/core.rs4
-rw-r--r--src/librustdoc/doctest.rs10
-rw-r--r--src/librustdoc/doctest/markdown.rs11
-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.rs90
-rw-r--r--src/librustdoc/passes/lint.rs33
-rw-r--r--src/librustdoc/passes/lint/bare_urls.rs68
-rw-r--r--src/librustdoc/passes/lint/check_code_block_syntax.rs6
-rw-r--r--src/librustdoc/passes/lint/html_tags.rs248
-rw-r--r--src/librustdoc/passes/lint/redundant_explicit_links.rs7
-rw-r--r--src/librustdoc/passes/lint/unescaped_backticks.rs12
-rw-r--r--src/librustdoc/passes/lint/unportable_markdown.rs12
-rw-r--r--src/tools/miri/rust-version2
-rw-r--r--src/tools/miri/src/shims/unix/mem.rs4
-rw-r--r--src/tools/miri/tests/fail/intrinsics/simd-div-by-zero.rs6
-rw-r--r--src/tools/miri/tests/fail/intrinsics/simd-div-overflow.rs6
-rw-r--r--src/tools/miri/tests/fail/intrinsics/simd-reduce-invalid-bool.rs4
-rw-r--r--src/tools/miri/tests/fail/intrinsics/simd-rem-by-zero.rs6
-rw-r--r--src/tools/miri/tests/fail/intrinsics/simd-select-bitmask-invalid.rs4
-rw-r--r--src/tools/miri/tests/fail/intrinsics/simd-select-invalid-bool.rs4
-rw-r--r--src/tools/miri/tests/fail/intrinsics/simd-shl-too-far.rs6
-rw-r--r--src/tools/miri/tests/fail/intrinsics/simd-shr-too-far.rs6
-rw-r--r--src/tools/miri/tests/pass/simd-intrinsic-generic-elements.rs18
-rw-r--r--src/tools/run-make-support/src/external_deps/cargo.rs7
-rw-r--r--src/tools/run-make-support/src/external_deps/mod.rs1
-rw-r--r--src/tools/run-make-support/src/external_deps/rustc.rs7
-rw-r--r--src/tools/run-make-support/src/lib.rs4
36 files changed, 684 insertions, 412 deletions
diff --git a/src/bootstrap/defaults/config.compiler.toml b/src/bootstrap/defaults/config.compiler.toml
index 789586b58f7..147939d2047 100644
--- a/src/bootstrap/defaults/config.compiler.toml
+++ b/src/bootstrap/defaults/config.compiler.toml
@@ -27,4 +27,5 @@ assertions = false
 # Enable warnings during the LLVM compilation (when LLVM is changed, causing a compilation)
 enable-warnings = true
 # Will download LLVM from CI if available on your platform.
-download-ci-llvm = "if-unchanged"
+# If you intend to modify `src/llvm-project`, use `"if-unchanged"` or `false` instead.
+download-ci-llvm = true
diff --git a/src/bootstrap/download-ci-llvm-stamp b/src/bootstrap/download-ci-llvm-stamp
index 90901530501..42cecbf5df9 100644
--- a/src/bootstrap/download-ci-llvm-stamp
+++ b/src/bootstrap/download-ci-llvm-stamp
@@ -1,4 +1,4 @@
 Change this file to make users of the `download-ci-llvm` configuration download
 a new version of LLVM from CI, even if the LLVM submodule hasn’t changed.
 
-Last change is for: https://github.com/rust-lang/rust/pull/129116
+Last change is for: https://github.com/rust-lang/rust/pull/129788
diff --git a/src/bootstrap/mk/Makefile.in b/src/bootstrap/mk/Makefile.in
index 9acd85cddde..3fa2b3c2292 100644
--- a/src/bootstrap/mk/Makefile.in
+++ b/src/bootstrap/mk/Makefile.in
@@ -54,30 +54,34 @@ check-aux:
 		src/etc/test-float-parse \
 		$(BOOTSTRAP_ARGS)
 	# Run standard library tests in Miri.
-	$(Q)BOOTSTRAP_SKIP_TARGET_SANITY=1 \
-		$(BOOTSTRAP) miri --stage 2 \
+	$(Q)$(BOOTSTRAP) miri --stage 2 \
 		library/core \
 		library/alloc \
+		$(BOOTSTRAP_ARGS) \
 		--no-doc
 	# Some doctests use file system operations to demonstrate dealing with `Result`.
 	$(Q)MIRIFLAGS="-Zmiri-disable-isolation" \
 		$(BOOTSTRAP) miri --stage 2 \
 		library/core \
 		library/alloc \
+		$(BOOTSTRAP_ARGS) \
 		--doc
 	# In `std` we cannot test everything, so we skip some modules.
 	$(Q)MIRIFLAGS="-Zmiri-disable-isolation" \
 		$(BOOTSTRAP) miri --stage 2 library/std \
+		$(BOOTSTRAP_ARGS) \
 		--no-doc -- \
 		--skip fs:: --skip net:: --skip process:: --skip sys::pal::
 	$(Q)MIRIFLAGS="-Zmiri-disable-isolation" \
 		$(BOOTSTRAP) miri --stage 2 library/std \
+		$(BOOTSTRAP_ARGS) \
 		--doc -- \
 		--skip fs:: --skip net:: --skip process:: --skip sys::pal::
 	# Also test some very target-specific modules on other targets
 	# (making sure to cover an i686 target as well).
 	$(Q)MIRIFLAGS="-Zmiri-disable-isolation" BOOTSTRAP_SKIP_TARGET_SANITY=1 \
 		$(BOOTSTRAP) miri --stage 2 library/std \
+		$(BOOTSTRAP_ARGS) \
 		--target aarch64-apple-darwin,i686-pc-windows-msvc \
 		--no-doc -- \
 		time:: sync:: thread:: env::
diff --git a/src/bootstrap/src/core/config/config.rs b/src/bootstrap/src/core/config/config.rs
index de861c42c4c..f509712730d 100644
--- a/src/bootstrap/src/core/config/config.rs
+++ b/src/bootstrap/src/core/config/config.rs
@@ -2766,7 +2766,8 @@ impl Config {
                     );
                 }
 
-                b
+                // If download-ci-llvm=true we also want to check that CI llvm is available
+                b && llvm::is_ci_llvm_available(self, asserts)
             }
             Some(StringOrBool::String(s)) if s == "if-unchanged" => if_unchanged(),
             Some(StringOrBool::String(other)) => {
diff --git a/src/bootstrap/src/utils/change_tracker.rs b/src/bootstrap/src/utils/change_tracker.rs
index 80ab09881fe..99bcc6e0787 100644
--- a/src/bootstrap/src/utils/change_tracker.rs
+++ b/src/bootstrap/src/utils/change_tracker.rs
@@ -250,4 +250,9 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
         severity: ChangeSeverity::Info,
         summary: "New option `llvm.enzyme` to control whether the llvm based autodiff tool (Enzyme) is built.",
     },
+    ChangeInfo {
+        change_id: 129473,
+        severity: ChangeSeverity::Warning,
+        summary: "`download-ci-llvm = true` now checks if CI llvm is available and has become the default for the compiler profile",
+    },
 ];
diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs
index 9e7b69ec45f..b3c87a72508 100644
--- a/src/librustdoc/config.rs
+++ b/src/librustdoc/config.rs
@@ -53,12 +53,18 @@ 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 {
     // Basic options / Options passed directly to rustc
-    /// The crate root or Markdown file to load.
-    pub(crate) input: Input,
     /// The name of the crate being documented.
     pub(crate) crate_name: Option<String>,
     /// Whether or not this is a bin crate
@@ -179,7 +185,6 @@ impl fmt::Debug for Options {
         }
 
         f.debug_struct("Options")
-            .field("input", &self.input.source_name())
             .field("crate_name", &self.crate_name)
             .field("bin_crate", &self.bin_crate)
             .field("proc_macro_crate", &self.proc_macro_crate)
@@ -289,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)]
@@ -348,7 +359,7 @@ impl Options {
         early_dcx: &mut EarlyDiagCtxt,
         matches: &getopts::Matches,
         args: Vec<String>,
-    ) -> Option<(Options, RenderOptions)> {
+    ) -> Option<(InputMode, Options, RenderOptions)> {
         // Check for unstable options.
         nightly_options::check_nightly_options(early_dcx, matches, &opts());
 
@@ -478,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) {
@@ -494,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")
@@ -735,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(
@@ -751,7 +778,6 @@ impl Options {
         let unstable_features =
             rustc_feature::UnstableFeatures::from_environment(crate_name.as_deref());
         let options = Options {
-            input,
             bin_crate,
             proc_macro_crate,
             error_format,
@@ -823,16 +849,17 @@ impl Options {
             no_emit_shared: false,
             html_no_source,
             output_to_stdout,
+            should_merge,
+            include_parts_dir,
+            parts_out_dir,
         };
-        Some((options, render_options))
+        Some((input, options, render_options))
     }
+}
 
-    /// Returns `true` if the file given as `self.input` is a Markdown file.
-    pub(crate) fn markdown_input(&self) -> Option<&Path> {
-        self.input
-            .opt_path()
-            .filter(|p| matches!(p.extension(), Some(e) if e == "md" || e == "markdown"))
-    }
+/// Returns `true` if the file given as `self.input` is a Markdown file.
+pub(crate) fn markdown_input(input: &Input) -> Option<&Path> {
+    input.opt_path().filter(|p| matches!(p.extension(), Some(e) if e == "md" || e == "markdown"))
 }
 
 fn parse_remap_path_prefix(
@@ -900,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/core.rs b/src/librustdoc/core.rs
index 2cde0ac5c53..4fafef65792 100644
--- a/src/librustdoc/core.rs
+++ b/src/librustdoc/core.rs
@@ -20,7 +20,7 @@ use rustc_interface::interface;
 use rustc_lint::{late_lint_mod, MissingDoc};
 use rustc_middle::hir::nested_filter;
 use rustc_middle::ty::{ParamEnv, Ty, TyCtxt};
-use rustc_session::config::{self, CrateType, ErrorOutputType, ResolveDocLinks};
+use rustc_session::config::{self, CrateType, ErrorOutputType, Input, ResolveDocLinks};
 pub(crate) use rustc_session::config::{Options, UnstableOptions};
 use rustc_session::{lint, Session};
 use rustc_span::symbol::sym;
@@ -177,8 +177,8 @@ pub(crate) fn new_dcx(
 
 /// Parse, resolve, and typecheck the given crate.
 pub(crate) fn create_config(
+    input: Input,
     RustdocOptions {
-        input,
         crate_name,
         proc_macro_crate,
         error_format,
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs
index 8b6588ea75c..05ef7289201 100644
--- a/src/librustdoc/doctest.rs
+++ b/src/librustdoc/doctest.rs
@@ -19,7 +19,7 @@ use rustc_errors::{ColorConfig, DiagCtxtHandle, ErrorGuaranteed, FatalError};
 use rustc_hir::def_id::LOCAL_CRATE;
 use rustc_hir::CRATE_HIR_ID;
 use rustc_interface::interface;
-use rustc_session::config::{self, CrateType, ErrorOutputType};
+use rustc_session::config::{self, CrateType, ErrorOutputType, Input};
 use rustc_session::lint;
 use rustc_span::edition::Edition;
 use rustc_span::symbol::sym;
@@ -88,7 +88,11 @@ fn get_doctest_dir() -> io::Result<TempDir> {
     TempFileBuilder::new().prefix("rustdoctest").tempdir()
 }
 
-pub(crate) fn run(dcx: DiagCtxtHandle<'_>, options: RustdocOptions) -> Result<(), ErrorGuaranteed> {
+pub(crate) fn run(
+    dcx: DiagCtxtHandle<'_>,
+    input: Input,
+    options: RustdocOptions,
+) -> Result<(), ErrorGuaranteed> {
     let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name;
 
     // See core::create_config for what's going on here.
@@ -135,7 +139,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, options: RustdocOptions) -> Result<()
         opts: sessopts,
         crate_cfg: cfgs,
         crate_check_cfg: options.check_cfgs.clone(),
-        input: options.input.clone(),
+        input: input.clone(),
         output_file: None,
         output_dir: None,
         file_loader: None,
diff --git a/src/librustdoc/doctest/markdown.rs b/src/librustdoc/doctest/markdown.rs
index 9a237f8684d..4f83bd5e882 100644
--- a/src/librustdoc/doctest/markdown.rs
+++ b/src/librustdoc/doctest/markdown.rs
@@ -3,6 +3,7 @@
 use std::fs::read_to_string;
 use std::sync::{Arc, Mutex};
 
+use rustc_session::config::Input;
 use rustc_span::FileName;
 use tempfile::tempdir;
 
@@ -69,9 +70,8 @@ impl DocTestVisitor for MdCollector {
 }
 
 /// Runs any tests/code examples in the markdown file `options.input`.
-pub(crate) fn test(options: Options) -> Result<(), String> {
-    use rustc_session::config::Input;
-    let input_str = match &options.input {
+pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> {
+    let input_str = match input {
         Input::File(path) => {
             read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?
         }
@@ -79,7 +79,7 @@ pub(crate) fn test(options: Options) -> Result<(), String> {
     };
 
     // Obviously not a real crate name, but close enough for purposes of doctests.
-    let crate_name = options.input.filestem().to_string();
+    let crate_name = input.filestem().to_string();
     let temp_dir =
         tempdir().map_err(|error| format!("failed to create temporary directory: {error:?}"))?;
     let args_file = temp_dir.path().join("rustdoc-cfgs");
@@ -96,8 +96,7 @@ pub(crate) fn test(options: Options) -> Result<(), String> {
     let mut md_collector = MdCollector {
         tests: vec![],
         cur_path: vec![],
-        filename: options
-            .input
+        filename: input
             .opt_path()
             .map(ToOwned::to_owned)
             .map(FileName::from)
diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs
index 58a228b74fc..a9b9377c0b9 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 3b8eda08372..a402d799504 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 e8d12320f82..dc1a8cca6bc 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 f25acbe080a..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],
@@ -727,22 +780,35 @@ fn main_args(
 
     // Note that we discard any distinction between different non-zero exit
     // codes from `from_matches` here.
-    let (options, render_options) = match config::Options::from_matches(early_dcx, &matches, args) {
-        Some(opts) => opts,
-        None => return Ok(()),
-    };
+    let (input, options, render_options) =
+        match config::Options::from_matches(early_dcx, &matches, args) {
+            Some(opts) => opts,
+            None => return Ok(()),
+        };
 
     let dcx =
         core::new_dcx(options.error_format, None, options.diagnostic_width, &options.unstable_opts);
     let dcx = dcx.handle();
 
-    match (options.should_test, options.markdown_input()) {
-        (true, Some(_)) => return wrap_return(dcx, doctest::test_markdown(options)),
-        (true, None) => return doctest::run(dcx, options),
-        (false, Some(input)) => {
-            let input = input.to_owned();
+    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),
+        (false, Some(md_input)) => {
+            let md_input = md_input.to_owned();
             let edition = options.edition;
-            let config = core::create_config(options, &render_options, using_internal_features);
+            let config =
+                core::create_config(input, options, &render_options, using_internal_features);
 
             // `markdown::render` can invoke `doctest::make_test`, which
             // requires session globals and a thread pool, so we use
@@ -750,7 +816,7 @@ fn main_args(
             return wrap_return(
                 dcx,
                 interface::run_compiler(config, |_compiler| {
-                    markdown::render(&input, render_options, edition)
+                    markdown::render(&md_input, render_options, edition)
                 }),
             );
         }
@@ -775,7 +841,7 @@ fn main_args(
     let scrape_examples_options = options.scrape_examples_options.clone();
     let bin_crate = options.bin_crate;
 
-    let config = core::create_config(options, &render_options, using_internal_features);
+    let config = core::create_config(input, options, &render_options, using_internal_features);
 
     interface::run_compiler(config, |compiler| {
         let sess = &compiler.sess;
diff --git a/src/librustdoc/passes/lint.rs b/src/librustdoc/passes/lint.rs
index bc804a340bf..4da5d8f0e06 100644
--- a/src/librustdoc/passes/lint.rs
+++ b/src/librustdoc/passes/lint.rs
@@ -27,12 +27,33 @@ pub(crate) fn run_lints(krate: Crate, cx: &mut DocContext<'_>) -> Crate {
 
 impl<'a, 'tcx> DocVisitor for Linter<'a, 'tcx> {
     fn visit_item(&mut self, item: &Item) {
-        bare_urls::visit_item(self.cx, item);
-        check_code_block_syntax::visit_item(self.cx, item);
-        html_tags::visit_item(self.cx, item);
-        unescaped_backticks::visit_item(self.cx, item);
-        redundant_explicit_links::visit_item(self.cx, item);
-        unportable_markdown::visit_item(self.cx, item);
+        let Some(hir_id) = DocContext::as_local_hir_id(self.cx.tcx, item.item_id) else {
+            // If non-local, no need to check anything.
+            return;
+        };
+        let dox = item.doc_value();
+        if !dox.is_empty() {
+            let may_have_link = dox.contains(&[':', '['][..]);
+            let may_have_block_comment_or_html = dox.contains(&['<', '>']);
+            // ~~~rust
+            // // This is a real, supported commonmark syntax for block code
+            // ~~~
+            let may_have_code = dox.contains(&['~', '`', '\t'][..]) || dox.contains("    ");
+            if may_have_link {
+                bare_urls::visit_item(self.cx, item, hir_id, &dox);
+                redundant_explicit_links::visit_item(self.cx, item, hir_id);
+            }
+            if may_have_code {
+                check_code_block_syntax::visit_item(self.cx, item, &dox);
+                unescaped_backticks::visit_item(self.cx, item, hir_id, &dox);
+            }
+            if may_have_block_comment_or_html {
+                html_tags::visit_item(self.cx, item, hir_id, &dox);
+                unportable_markdown::visit_item(self.cx, item, hir_id, &dox);
+            } else if may_have_link {
+                unportable_markdown::visit_item(self.cx, item, hir_id, &dox);
+            }
+        }
 
         self.visit_item_recur(item)
     }
diff --git a/src/librustdoc/passes/lint/bare_urls.rs b/src/librustdoc/passes/lint/bare_urls.rs
index 22051dd954d..bac0e07f1c1 100644
--- a/src/librustdoc/passes/lint/bare_urls.rs
+++ b/src/librustdoc/passes/lint/bare_urls.rs
@@ -8,6 +8,7 @@ use std::sync::LazyLock;
 use pulldown_cmark::{Event, Parser, Tag};
 use regex::Regex;
 use rustc_errors::Applicability;
+use rustc_hir::HirId;
 use rustc_resolve::rustdoc::source_span_for_markdown_range;
 use tracing::trace;
 
@@ -15,50 +16,43 @@ use crate::clean::*;
 use crate::core::DocContext;
 use crate::html::markdown::main_body_opts;
 
-pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item) {
-    let Some(hir_id) = DocContext::as_local_hir_id(cx.tcx, item.item_id) else {
-        // If non-local, no need to check anything.
-        return;
+pub(super) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
+    let report_diag = |cx: &DocContext<'_>, msg: &'static str, range: Range<usize>| {
+        let sp = source_span_for_markdown_range(cx.tcx, &dox, &range, &item.attrs.doc_strings)
+            .unwrap_or_else(|| item.attr_span(cx.tcx));
+        cx.tcx.node_span_lint(crate::lint::BARE_URLS, hir_id, sp, |lint| {
+            lint.primary_message(msg)
+                .note("bare URLs are not automatically turned into clickable links")
+                .multipart_suggestion(
+                    "use an automatic link instead",
+                    vec![
+                        (sp.shrink_to_lo(), "<".to_string()),
+                        (sp.shrink_to_hi(), ">".to_string()),
+                    ],
+                    Applicability::MachineApplicable,
+                );
+        });
     };
-    let dox = item.doc_value();
-    if !dox.is_empty() {
-        let report_diag = |cx: &DocContext<'_>, msg: &'static str, range: Range<usize>| {
-            let sp = source_span_for_markdown_range(cx.tcx, &dox, &range, &item.attrs.doc_strings)
-                .unwrap_or_else(|| item.attr_span(cx.tcx));
-            cx.tcx.node_span_lint(crate::lint::BARE_URLS, hir_id, sp, |lint| {
-                lint.primary_message(msg)
-                    .note("bare URLs are not automatically turned into clickable links")
-                    .multipart_suggestion(
-                        "use an automatic link instead",
-                        vec![
-                            (sp.shrink_to_lo(), "<".to_string()),
-                            (sp.shrink_to_hi(), ">".to_string()),
-                        ],
-                        Applicability::MachineApplicable,
-                    );
-            });
-        };
 
-        let mut p = Parser::new_ext(&dox, main_body_opts()).into_offset_iter();
+    let mut p = Parser::new_ext(&dox, main_body_opts()).into_offset_iter();
 
-        while let Some((event, range)) = p.next() {
-            match event {
-                Event::Text(s) => find_raw_urls(cx, &s, range, &report_diag),
-                // We don't want to check the text inside code blocks or links.
-                Event::Start(tag @ (Tag::CodeBlock(_) | Tag::Link { .. })) => {
-                    while let Some((event, _)) = p.next() {
-                        match event {
-                            Event::End(end)
-                                if mem::discriminant(&end) == mem::discriminant(&tag.to_end()) =>
-                            {
-                                break;
-                            }
-                            _ => {}
+    while let Some((event, range)) = p.next() {
+        match event {
+            Event::Text(s) => find_raw_urls(cx, &s, range, &report_diag),
+            // We don't want to check the text inside code blocks or links.
+            Event::Start(tag @ (Tag::CodeBlock(_) | Tag::Link { .. })) => {
+                while let Some((event, _)) = p.next() {
+                    match event {
+                        Event::End(end)
+                            if mem::discriminant(&end) == mem::discriminant(&tag.to_end()) =>
+                        {
+                            break;
                         }
+                        _ => {}
                     }
                 }
-                _ => {}
             }
+            _ => {}
         }
     }
 }
diff --git a/src/librustdoc/passes/lint/check_code_block_syntax.rs b/src/librustdoc/passes/lint/check_code_block_syntax.rs
index 977c0953336..1b2431a629b 100644
--- a/src/librustdoc/passes/lint/check_code_block_syntax.rs
+++ b/src/librustdoc/passes/lint/check_code_block_syntax.rs
@@ -15,10 +15,8 @@ use crate::clean;
 use crate::core::DocContext;
 use crate::html::markdown::{self, RustCodeBlock};
 
-pub(crate) fn visit_item(cx: &DocContext<'_>, item: &clean::Item) {
-    if let Some(def_id) = item.item_id.as_local_def_id()
-        && let Some(dox) = &item.opt_doc_value()
-    {
+pub(crate) fn visit_item(cx: &DocContext<'_>, item: &clean::Item, dox: &str) {
+    if let Some(def_id) = item.item_id.as_local_def_id() {
         let sp = item.attr_span(cx.tcx);
         let extra = crate::html::markdown::ExtraInfo::new(cx.tcx, def_id, sp);
         for code_block in markdown::rust_code_blocks(dox, &extra) {
diff --git a/src/librustdoc/passes/lint/html_tags.rs b/src/librustdoc/passes/lint/html_tags.rs
index 6f9e9d36a5c..223174838ad 100644
--- a/src/librustdoc/passes/lint/html_tags.rs
+++ b/src/librustdoc/passes/lint/html_tags.rs
@@ -5,159 +5,149 @@ use std::ops::Range;
 use std::str::CharIndices;
 
 use pulldown_cmark::{BrokenLink, Event, LinkType, Parser, Tag, TagEnd};
+use rustc_hir::HirId;
 use rustc_resolve::rustdoc::source_span_for_markdown_range;
 
 use crate::clean::*;
 use crate::core::DocContext;
 use crate::html::markdown::main_body_opts;
 
-pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
+pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
     let tcx = cx.tcx;
-    let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id)
-    // If non-local, no need to check anything.
-    else {
-        return;
-    };
-    let dox = item.doc_value();
-    if !dox.is_empty() {
-        let report_diag = |msg: String, range: &Range<usize>, is_open_tag: bool| {
-            let sp = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs.doc_strings)
-            {
-                Some(sp) => sp,
-                None => item.attr_span(tcx),
-            };
-            tcx.node_span_lint(crate::lint::INVALID_HTML_TAGS, hir_id, sp, |lint| {
-                use rustc_lint_defs::Applicability;
+    let report_diag = |msg: String, range: &Range<usize>, is_open_tag: bool| {
+        let sp = match source_span_for_markdown_range(tcx, &dox, range, &item.attrs.doc_strings) {
+            Some(sp) => sp,
+            None => item.attr_span(tcx),
+        };
+        tcx.node_span_lint(crate::lint::INVALID_HTML_TAGS, hir_id, sp, |lint| {
+            use rustc_lint_defs::Applicability;
 
-                lint.primary_message(msg);
+            lint.primary_message(msg);
 
-                // If a tag looks like `<this>`, it might actually be a generic.
-                // We don't try to detect stuff `<like, this>` because that's not valid HTML,
-                // and we don't try to detect stuff `<like this>` because that's not valid Rust.
-                let mut generics_end = range.end;
-                if let Some(Some(mut generics_start)) = (is_open_tag
-                    && dox[..generics_end].ends_with('>'))
-                .then(|| extract_path_backwards(&dox, range.start))
+            // If a tag looks like `<this>`, it might actually be a generic.
+            // We don't try to detect stuff `<like, this>` because that's not valid HTML,
+            // and we don't try to detect stuff `<like this>` because that's not valid Rust.
+            let mut generics_end = range.end;
+            if let Some(Some(mut generics_start)) = (is_open_tag
+                && dox[..generics_end].ends_with('>'))
+            .then(|| extract_path_backwards(&dox, range.start))
+            {
+                while generics_start != 0
+                    && generics_end < dox.len()
+                    && dox.as_bytes()[generics_start - 1] == b'<'
+                    && dox.as_bytes()[generics_end] == b'>'
                 {
-                    while generics_start != 0
-                        && generics_end < dox.len()
-                        && dox.as_bytes()[generics_start - 1] == b'<'
-                        && dox.as_bytes()[generics_end] == b'>'
-                    {
-                        generics_end += 1;
-                        generics_start -= 1;
-                        if let Some(new_start) = extract_path_backwards(&dox, generics_start) {
-                            generics_start = new_start;
-                        }
-                        if let Some(new_end) = extract_path_forward(&dox, generics_end) {
-                            generics_end = new_end;
-                        }
+                    generics_end += 1;
+                    generics_start -= 1;
+                    if let Some(new_start) = extract_path_backwards(&dox, generics_start) {
+                        generics_start = new_start;
                     }
                     if let Some(new_end) = extract_path_forward(&dox, generics_end) {
                         generics_end = new_end;
                     }
-                    let generics_sp = match source_span_for_markdown_range(
-                        tcx,
-                        &dox,
-                        &(generics_start..generics_end),
-                        &item.attrs.doc_strings,
-                    ) {
-                        Some(sp) => sp,
-                        None => item.attr_span(tcx),
-                    };
-                    // Sometimes, we only extract part of a path. For example, consider this:
-                    //
-                    //     <[u32] as IntoIter<u32>>::Item
-                    //                       ^^^^^ unclosed HTML tag `u32`
-                    //
-                    // We don't have any code for parsing fully-qualified trait paths.
-                    // In theory, we could add it, but doing it correctly would require
-                    // parsing the entire path grammar, which is problematic because of
-                    // overlap between the path grammar and Markdown.
-                    //
-                    // The example above shows that ambiguity. Is `[u32]` intended to be an
-                    // intra-doc link to the u32 primitive, or is it intended to be a slice?
-                    //
-                    // If the below conditional were removed, we would suggest this, which is
-                    // not what the user probably wants.
-                    //
-                    //     <[u32] as `IntoIter<u32>`>::Item
-                    //
-                    // We know that the user actually wants to wrap the whole thing in a code
-                    // block, but the only reason we know that is because `u32` does not, in
-                    // fact, implement IntoIter. If the example looks like this:
-                    //
-                    //     <[Vec<i32>] as IntoIter<i32>::Item
-                    //
-                    // The ideal fix would be significantly different.
-                    if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<')
-                        || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>')
-                    {
-                        return;
-                    }
-                    // multipart form is chosen here because ``Vec<i32>`` would be confusing.
-                    lint.multipart_suggestion(
-                        "try marking as source code",
-                        vec![
-                            (generics_sp.shrink_to_lo(), String::from("`")),
-                            (generics_sp.shrink_to_hi(), String::from("`")),
-                        ],
-                        Applicability::MaybeIncorrect,
-                    );
                 }
-            });
-        };
+                if let Some(new_end) = extract_path_forward(&dox, generics_end) {
+                    generics_end = new_end;
+                }
+                let generics_sp = match source_span_for_markdown_range(
+                    tcx,
+                    &dox,
+                    &(generics_start..generics_end),
+                    &item.attrs.doc_strings,
+                ) {
+                    Some(sp) => sp,
+                    None => item.attr_span(tcx),
+                };
+                // Sometimes, we only extract part of a path. For example, consider this:
+                //
+                //     <[u32] as IntoIter<u32>>::Item
+                //                       ^^^^^ unclosed HTML tag `u32`
+                //
+                // We don't have any code for parsing fully-qualified trait paths.
+                // In theory, we could add it, but doing it correctly would require
+                // parsing the entire path grammar, which is problematic because of
+                // overlap between the path grammar and Markdown.
+                //
+                // The example above shows that ambiguity. Is `[u32]` intended to be an
+                // intra-doc link to the u32 primitive, or is it intended to be a slice?
+                //
+                // If the below conditional were removed, we would suggest this, which is
+                // not what the user probably wants.
+                //
+                //     <[u32] as `IntoIter<u32>`>::Item
+                //
+                // We know that the user actually wants to wrap the whole thing in a code
+                // block, but the only reason we know that is because `u32` does not, in
+                // fact, implement IntoIter. If the example looks like this:
+                //
+                //     <[Vec<i32>] as IntoIter<i32>::Item
+                //
+                // The ideal fix would be significantly different.
+                if (generics_start > 0 && dox.as_bytes()[generics_start - 1] == b'<')
+                    || (generics_end < dox.len() && dox.as_bytes()[generics_end] == b'>')
+                {
+                    return;
+                }
+                // multipart form is chosen here because ``Vec<i32>`` would be confusing.
+                lint.multipart_suggestion(
+                    "try marking as source code",
+                    vec![
+                        (generics_sp.shrink_to_lo(), String::from("`")),
+                        (generics_sp.shrink_to_hi(), String::from("`")),
+                    ],
+                    Applicability::MaybeIncorrect,
+                );
+            }
+        });
+    };
 
-        let mut tags = Vec::new();
-        let mut is_in_comment = None;
-        let mut in_code_block = false;
+    let mut tags = Vec::new();
+    let mut is_in_comment = None;
+    let mut in_code_block = false;
 
-        let link_names = item.link_names(&cx.cache);
+    let link_names = item.link_names(&cx.cache);
 
-        let mut replacer = |broken_link: BrokenLink<'_>| {
-            if let Some(link) =
-                link_names.iter().find(|link| *link.original_text == *broken_link.reference)
-            {
-                Some((link.href.as_str().into(), link.new_text.to_string().into()))
-            } else if matches!(
-                &broken_link.link_type,
-                LinkType::Reference | LinkType::ReferenceUnknown
-            ) {
-                // If the link is shaped [like][this], suppress any broken HTML in the [this] part.
-                // The `broken_intra_doc_links` will report typos in there anyway.
-                Some((
-                    broken_link.reference.to_string().into(),
-                    broken_link.reference.to_string().into(),
-                ))
-            } else {
-                None
-            }
-        };
+    let mut replacer = |broken_link: BrokenLink<'_>| {
+        if let Some(link) =
+            link_names.iter().find(|link| *link.original_text == *broken_link.reference)
+        {
+            Some((link.href.as_str().into(), link.new_text.to_string().into()))
+        } else if matches!(&broken_link.link_type, LinkType::Reference | LinkType::ReferenceUnknown)
+        {
+            // If the link is shaped [like][this], suppress any broken HTML in the [this] part.
+            // The `broken_intra_doc_links` will report typos in there anyway.
+            Some((
+                broken_link.reference.to_string().into(),
+                broken_link.reference.to_string().into(),
+            ))
+        } else {
+            None
+        }
+    };
 
-        let p = Parser::new_with_broken_link_callback(&dox, main_body_opts(), Some(&mut replacer))
-            .into_offset_iter();
+    let p = Parser::new_with_broken_link_callback(&dox, main_body_opts(), Some(&mut replacer))
+        .into_offset_iter();
 
-        for (event, range) in p {
-            match event {
-                Event::Start(Tag::CodeBlock(_)) => in_code_block = true,
-                Event::Html(text) | Event::InlineHtml(text) if !in_code_block => {
-                    extract_tags(&mut tags, &text, range, &mut is_in_comment, &report_diag)
-                }
-                Event::End(TagEnd::CodeBlock) => in_code_block = false,
-                _ => {}
+    for (event, range) in p {
+        match event {
+            Event::Start(Tag::CodeBlock(_)) => in_code_block = true,
+            Event::Html(text) | Event::InlineHtml(text) if !in_code_block => {
+                extract_tags(&mut tags, &text, range, &mut is_in_comment, &report_diag)
             }
+            Event::End(TagEnd::CodeBlock) => in_code_block = false,
+            _ => {}
         }
+    }
 
-        for (tag, range) in tags.iter().filter(|(t, _)| {
-            let t = t.to_lowercase();
-            !ALLOWED_UNCLOSED.contains(&t.as_str())
-        }) {
-            report_diag(format!("unclosed HTML tag `{tag}`"), range, true);
-        }
+    for (tag, range) in tags.iter().filter(|(t, _)| {
+        let t = t.to_lowercase();
+        !ALLOWED_UNCLOSED.contains(&t.as_str())
+    }) {
+        report_diag(format!("unclosed HTML tag `{tag}`"), range, true);
+    }
 
-        if let Some(range) = is_in_comment {
-            report_diag("Unclosed HTML comment".to_string(), &range, false);
-        }
+    if let Some(range) = is_in_comment {
+        report_diag("Unclosed HTML comment".to_string(), &range, false);
     }
 }
 
diff --git a/src/librustdoc/passes/lint/redundant_explicit_links.rs b/src/librustdoc/passes/lint/redundant_explicit_links.rs
index 0a90c039dfb..9c37e11349a 100644
--- a/src/librustdoc/passes/lint/redundant_explicit_links.rs
+++ b/src/librustdoc/passes/lint/redundant_explicit_links.rs
@@ -24,12 +24,7 @@ struct LinkData {
     display_link: String,
 }
 
-pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
-    let Some(hir_id) = DocContext::as_local_hir_id(cx.tcx, item.item_id) else {
-        // If non-local, no need to check anything.
-        return;
-    };
-
+pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId) {
     let hunks = prepare_to_doc_link_resolution(&item.attrs.doc_strings);
     for (item_id, doc) in hunks {
         if let Some(item_id) = item_id.or(item.def_id())
diff --git a/src/librustdoc/passes/lint/unescaped_backticks.rs b/src/librustdoc/passes/lint/unescaped_backticks.rs
index a6c8db16f82..d79f682a580 100644
--- a/src/librustdoc/passes/lint/unescaped_backticks.rs
+++ b/src/librustdoc/passes/lint/unescaped_backticks.rs
@@ -4,6 +4,7 @@ use std::ops::Range;
 
 use pulldown_cmark::{BrokenLink, Event, Parser};
 use rustc_errors::Diag;
+use rustc_hir::HirId;
 use rustc_lint_defs::Applicability;
 use rustc_resolve::rustdoc::source_span_for_markdown_range;
 
@@ -11,17 +12,8 @@ use crate::clean::Item;
 use crate::core::DocContext;
 use crate::html::markdown::main_body_opts;
 
-pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
+pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
     let tcx = cx.tcx;
-    let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id) else {
-        // If non-local, no need to check anything.
-        return;
-    };
-
-    let dox = item.doc_value();
-    if dox.is_empty() {
-        return;
-    }
 
     let link_names = item.link_names(&cx.cache);
     let mut replacer = |broken_link: BrokenLink<'_>| {
diff --git a/src/librustdoc/passes/lint/unportable_markdown.rs b/src/librustdoc/passes/lint/unportable_markdown.rs
index 87fe0055883..f8368a866c8 100644
--- a/src/librustdoc/passes/lint/unportable_markdown.rs
+++ b/src/librustdoc/passes/lint/unportable_markdown.rs
@@ -12,6 +12,7 @@
 
 use std::collections::{BTreeMap, BTreeSet};
 
+use rustc_hir::HirId;
 use rustc_lint_defs::Applicability;
 use rustc_resolve::rustdoc::source_span_for_markdown_range;
 use {pulldown_cmark as cmarkn, pulldown_cmark_old as cmarko};
@@ -19,17 +20,8 @@ use {pulldown_cmark as cmarkn, pulldown_cmark_old as cmarko};
 use crate::clean::Item;
 use crate::core::DocContext;
 
-pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item) {
+pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) {
     let tcx = cx.tcx;
-    let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id) else {
-        // If non-local, no need to check anything.
-        return;
-    };
-
-    let dox = item.doc_value();
-    if dox.is_empty() {
-        return;
-    }
 
     // P1: unintended strikethrough was fixed by requiring single-tildes to flank
     // the same way underscores do, so nothing is done here
diff --git a/src/tools/miri/rust-version b/src/tools/miri/rust-version
index 48cc89b109d..4fbaee9dcbe 100644
--- a/src/tools/miri/rust-version
+++ b/src/tools/miri/rust-version
@@ -1 +1 @@
-304b7f801bab31233680879ca4fb6eb294706a59
+a9fb00bfa4b3038c855b2097b54e05e8c198c183
diff --git a/src/tools/miri/src/shims/unix/mem.rs b/src/tools/miri/src/shims/unix/mem.rs
index de5a5d0759c..33ed0e26982 100644
--- a/src/tools/miri/src/shims/unix/mem.rs
+++ b/src/tools/miri/src/shims/unix/mem.rs
@@ -42,10 +42,10 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
         let map_shared = this.eval_libc_i32("MAP_SHARED");
         let map_fixed = this.eval_libc_i32("MAP_FIXED");
 
-        // This is a horrible hack, but on MacOS and Solaris the guard page mechanism uses mmap
+        // This is a horrible hack, but on MacOS and Solarish the guard page mechanism uses mmap
         // in a way we do not support. We just give it the return value it expects.
         if this.frame_in_std()
-            && matches!(&*this.tcx.sess.target.os, "macos" | "solaris")
+            && matches!(&*this.tcx.sess.target.os, "macos" | "solaris" | "illumos")
             && (flags & map_fixed) != 0
         {
             return Ok(Scalar::from_maybe_pointer(Pointer::from_addr_invalid(addr), this));
diff --git a/src/tools/miri/tests/fail/intrinsics/simd-div-by-zero.rs b/src/tools/miri/tests/fail/intrinsics/simd-div-by-zero.rs
index ba474332b81..57a9b66d8ec 100644
--- a/src/tools/miri/tests/fail/intrinsics/simd-div-by-zero.rs
+++ b/src/tools/miri/tests/fail/intrinsics/simd-div-by-zero.rs
@@ -4,12 +4,12 @@ use std::intrinsics::simd::simd_div;
 
 #[repr(simd)]
 #[allow(non_camel_case_types)]
-struct i32x2(i32, i32);
+struct i32x2([i32; 2]);
 
 fn main() {
     unsafe {
-        let x = i32x2(1, 1);
-        let y = i32x2(1, 0);
+        let x = i32x2([1, 1]);
+        let y = i32x2([1, 0]);
         simd_div(x, y); //~ERROR: Undefined Behavior: dividing by zero
     }
 }
diff --git a/src/tools/miri/tests/fail/intrinsics/simd-div-overflow.rs b/src/tools/miri/tests/fail/intrinsics/simd-div-overflow.rs
index d01e41de0e4..8ffc2669828 100644
--- a/src/tools/miri/tests/fail/intrinsics/simd-div-overflow.rs
+++ b/src/tools/miri/tests/fail/intrinsics/simd-div-overflow.rs
@@ -4,12 +4,12 @@ use std::intrinsics::simd::simd_div;
 
 #[repr(simd)]
 #[allow(non_camel_case_types)]
-struct i32x2(i32, i32);
+struct i32x2([i32; 2]);
 
 fn main() {
     unsafe {
-        let x = i32x2(1, i32::MIN);
-        let y = i32x2(1, -1);
+        let x = i32x2([1, i32::MIN]);
+        let y = i32x2([1, -1]);
         simd_div(x, y); //~ERROR: Undefined Behavior: overflow in signed division
     }
 }
diff --git a/src/tools/miri/tests/fail/intrinsics/simd-reduce-invalid-bool.rs b/src/tools/miri/tests/fail/intrinsics/simd-reduce-invalid-bool.rs
index a194f0dd18a..ea0f908d996 100644
--- a/src/tools/miri/tests/fail/intrinsics/simd-reduce-invalid-bool.rs
+++ b/src/tools/miri/tests/fail/intrinsics/simd-reduce-invalid-bool.rs
@@ -4,11 +4,11 @@ use std::intrinsics::simd::simd_reduce_any;
 
 #[repr(simd)]
 #[allow(non_camel_case_types)]
-struct i32x2(i32, i32);
+struct i32x2([i32; 2]);
 
 fn main() {
     unsafe {
-        let x = i32x2(0, 1);
+        let x = i32x2([0, 1]);
         simd_reduce_any(x); //~ERROR: must be all-0-bits or all-1-bits
     }
 }
diff --git a/src/tools/miri/tests/fail/intrinsics/simd-rem-by-zero.rs b/src/tools/miri/tests/fail/intrinsics/simd-rem-by-zero.rs
index cd1e4b8162b..21c9520efc4 100644
--- a/src/tools/miri/tests/fail/intrinsics/simd-rem-by-zero.rs
+++ b/src/tools/miri/tests/fail/intrinsics/simd-rem-by-zero.rs
@@ -4,12 +4,12 @@ use std::intrinsics::simd::simd_rem;
 
 #[repr(simd)]
 #[allow(non_camel_case_types)]
-struct i32x2(i32, i32);
+struct i32x2([i32; 2]);
 
 fn main() {
     unsafe {
-        let x = i32x2(1, 1);
-        let y = i32x2(1, 0);
+        let x = i32x2([1, 1]);
+        let y = i32x2([1, 0]);
         simd_rem(x, y); //~ERROR: Undefined Behavior: calculating the remainder with a divisor of zero
     }
 }
diff --git a/src/tools/miri/tests/fail/intrinsics/simd-select-bitmask-invalid.rs b/src/tools/miri/tests/fail/intrinsics/simd-select-bitmask-invalid.rs
index 96802fae49c..409098ac3b5 100644
--- a/src/tools/miri/tests/fail/intrinsics/simd-select-bitmask-invalid.rs
+++ b/src/tools/miri/tests/fail/intrinsics/simd-select-bitmask-invalid.rs
@@ -5,11 +5,11 @@ use std::intrinsics::simd::simd_select_bitmask;
 #[repr(simd)]
 #[allow(non_camel_case_types)]
 #[derive(Copy, Clone)]
-struct i32x2(i32, i32);
+struct i32x2([i32; 2]);
 
 fn main() {
     unsafe {
-        let x = i32x2(0, 1);
+        let x = i32x2([0, 1]);
         simd_select_bitmask(0b11111111u8, x, x); //~ERROR: bitmask less than 8 bits long must be filled with 0s for the remaining bits
     }
 }
diff --git a/src/tools/miri/tests/fail/intrinsics/simd-select-invalid-bool.rs b/src/tools/miri/tests/fail/intrinsics/simd-select-invalid-bool.rs
index 388fb2e2a84..a81ce95ada6 100644
--- a/src/tools/miri/tests/fail/intrinsics/simd-select-invalid-bool.rs
+++ b/src/tools/miri/tests/fail/intrinsics/simd-select-invalid-bool.rs
@@ -5,11 +5,11 @@ use std::intrinsics::simd::simd_select;
 #[repr(simd)]
 #[allow(non_camel_case_types)]
 #[derive(Copy, Clone)]
-struct i32x2(i32, i32);
+struct i32x2([i32; 2]);
 
 fn main() {
     unsafe {
-        let x = i32x2(0, 1);
+        let x = i32x2([0, 1]);
         simd_select(x, x, x); //~ERROR: must be all-0-bits or all-1-bits
     }
 }
diff --git a/src/tools/miri/tests/fail/intrinsics/simd-shl-too-far.rs b/src/tools/miri/tests/fail/intrinsics/simd-shl-too-far.rs
index 12aa7c10af4..ed317254ee6 100644
--- a/src/tools/miri/tests/fail/intrinsics/simd-shl-too-far.rs
+++ b/src/tools/miri/tests/fail/intrinsics/simd-shl-too-far.rs
@@ -4,12 +4,12 @@ use std::intrinsics::simd::simd_shl;
 
 #[repr(simd)]
 #[allow(non_camel_case_types)]
-struct i32x2(i32, i32);
+struct i32x2([i32; 2]);
 
 fn main() {
     unsafe {
-        let x = i32x2(1, 1);
-        let y = i32x2(100, 0);
+        let x = i32x2([1, 1]);
+        let y = i32x2([100, 0]);
         simd_shl(x, y); //~ERROR: overflowing shift by 100 in `simd_shl` in lane 0
     }
 }
diff --git a/src/tools/miri/tests/fail/intrinsics/simd-shr-too-far.rs b/src/tools/miri/tests/fail/intrinsics/simd-shr-too-far.rs
index ada7cf408c4..5d2ff1b82ed 100644
--- a/src/tools/miri/tests/fail/intrinsics/simd-shr-too-far.rs
+++ b/src/tools/miri/tests/fail/intrinsics/simd-shr-too-far.rs
@@ -4,12 +4,12 @@ use std::intrinsics::simd::simd_shr;
 
 #[repr(simd)]
 #[allow(non_camel_case_types)]
-struct i32x2(i32, i32);
+struct i32x2([i32; 2]);
 
 fn main() {
     unsafe {
-        let x = i32x2(1, 1);
-        let y = i32x2(20, 40);
+        let x = i32x2([1, 1]);
+        let y = i32x2([20, 40]);
         simd_shr(x, y); //~ERROR: overflowing shift by 40 in `simd_shr` in lane 1
     }
 }
diff --git a/src/tools/miri/tests/pass/simd-intrinsic-generic-elements.rs b/src/tools/miri/tests/pass/simd-intrinsic-generic-elements.rs
index 4a87f8c3ca7..9cf0c2ddef3 100644
--- a/src/tools/miri/tests/pass/simd-intrinsic-generic-elements.rs
+++ b/src/tools/miri/tests/pass/simd-intrinsic-generic-elements.rs
@@ -3,22 +3,22 @@
 #[repr(simd)]
 #[derive(Copy, Clone, Debug, PartialEq)]
 #[allow(non_camel_case_types)]
-struct i32x2(i32, i32);
+struct i32x2([i32; 2]);
 #[repr(simd)]
 #[derive(Copy, Clone, Debug, PartialEq)]
 #[allow(non_camel_case_types)]
-struct i32x4(i32, i32, i32, i32);
+struct i32x4([i32; 4]);
 #[repr(simd)]
 #[derive(Copy, Clone, Debug, PartialEq)]
 #[allow(non_camel_case_types)]
-struct i32x8(i32, i32, i32, i32, i32, i32, i32, i32);
+struct i32x8([i32; 8]);
 
 fn main() {
-    let _x2 = i32x2(20, 21);
-    let _x4 = i32x4(40, 41, 42, 43);
-    let _x8 = i32x8(80, 81, 82, 83, 84, 85, 86, 87);
+    let _x2 = i32x2([20, 21]);
+    let _x4 = i32x4([40, 41, 42, 43]);
+    let _x8 = i32x8([80, 81, 82, 83, 84, 85, 86, 87]);
 
-    let _y2 = i32x2(120, 121);
-    let _y4 = i32x4(140, 141, 142, 143);
-    let _y8 = i32x8(180, 181, 182, 183, 184, 185, 186, 187);
+    let _y2 = i32x2([120, 121]);
+    let _y4 = i32x4([140, 141, 142, 143]);
+    let _y8 = i32x8([180, 181, 182, 183, 184, 185, 186, 187]);
 }
diff --git a/src/tools/run-make-support/src/external_deps/cargo.rs b/src/tools/run-make-support/src/external_deps/cargo.rs
new file mode 100644
index 00000000000..b0e045dc80b
--- /dev/null
+++ b/src/tools/run-make-support/src/external_deps/cargo.rs
@@ -0,0 +1,7 @@
+use crate::command::Command;
+use crate::env_var;
+
+/// Returns a command that can be used to invoke Cargo.
+pub fn cargo() -> Command {
+    Command::new(env_var("BOOTSTRAP_CARGO"))
+}
diff --git a/src/tools/run-make-support/src/external_deps/mod.rs b/src/tools/run-make-support/src/external_deps/mod.rs
index f7c84724d0e..80c34a9070f 100644
--- a/src/tools/run-make-support/src/external_deps/mod.rs
+++ b/src/tools/run-make-support/src/external_deps/mod.rs
@@ -2,6 +2,7 @@
 //! such as `cc` or `python`.
 
 pub mod c_build;
+pub mod cargo;
 pub mod cc;
 pub mod clang;
 pub mod htmldocck;
diff --git a/src/tools/run-make-support/src/external_deps/rustc.rs b/src/tools/run-make-support/src/external_deps/rustc.rs
index f60ea972839..35d983dc607 100644
--- a/src/tools/run-make-support/src/external_deps/rustc.rs
+++ b/src/tools/run-make-support/src/external_deps/rustc.rs
@@ -36,10 +36,13 @@ pub struct Rustc {
 
 crate::macros::impl_common_helpers!(Rustc);
 
+pub fn rustc_path() -> String {
+    env_var("RUSTC")
+}
+
 #[track_caller]
 fn setup_common() -> Command {
-    let rustc = env_var("RUSTC");
-    let mut cmd = Command::new(rustc);
+    let mut cmd = Command::new(rustc_path());
     set_host_rpath(&mut cmd);
     cmd
 }
diff --git a/src/tools/run-make-support/src/lib.rs b/src/tools/run-make-support/src/lib.rs
index 980bd37dca8..15d813ccf53 100644
--- a/src/tools/run-make-support/src/lib.rs
+++ b/src/tools/run-make-support/src/lib.rs
@@ -50,6 +50,7 @@ pub use external_deps::{c_build, cc, clang, htmldocck, llvm, python, rustc, rust
 // These rely on external dependencies.
 pub use cc::{cc, cxx, extra_c_flags, extra_cxx_flags, Cc};
 pub use c_build::{build_native_dynamic_lib, build_native_static_lib, build_native_static_lib_optimized, build_native_static_lib_cxx};
+pub use cargo::cargo;
 pub use clang::{clang, Clang};
 pub use htmldocck::htmldocck;
 pub use llvm::{
@@ -58,7 +59,7 @@ pub use llvm::{
     LlvmProfdata, LlvmReadobj,
 };
 pub use python::python_command;
-pub use rustc::{aux_build, bare_rustc, rustc, Rustc};
+pub use rustc::{aux_build, bare_rustc, rustc, rustc_path, Rustc};
 pub use rustdoc::{bare_rustdoc, rustdoc, Rustdoc};
 
 /// [`diff`][mod@diff] is implemented in terms of the [similar] library.
@@ -98,3 +99,4 @@ pub use assertion_helpers::{
 pub use string::{
     count_regex_matches_in_files_with_extension, invalid_utf8_contains, invalid_utf8_not_contains,
 };
+use crate::external_deps::cargo;