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
This commit is contained in:
EtomicBomb 2024-08-20 23:34:19 +00:00 committed by ethan
parent 3782251c2c
commit 2e1cba6415
7 changed files with 419 additions and 135 deletions

View File

@ -53,6 +53,14 @@ fn try_from(value: &str) -> Result<Self, Self::Error> {
}
}
/// 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 @@ pub(crate) fn from_matches(
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 @@ fn println_condition(condition: Condition) {
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 @@ fn println_condition(condition: Condition) {
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 @@ fn println_condition(condition: Condition) {
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 @@ fn println_condition(condition: Condition) {
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`"),
}
}

View File

@ -16,12 +16,11 @@
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::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 @@ fn init(
span_correspondence_map: matches,
cache,
call_locations,
should_merge: options.should_merge,
};
let dst = output;
@ -640,92 +643,96 @@ fn after_krate(&mut self) -> Result<(), Error> {
);
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(

View File

@ -61,6 +61,7 @@
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;

View File

@ -39,7 +39,7 @@
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::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
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)?;
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_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
);
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
}
None if opt.enable_index_page => {
write_rendered_cci::<CratesIndexPart, _>(|| CratesIndexPart::blank(cx), dst, &crates)?;
}
_ => {} // 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 @@ fn write_rendered_cci<T: CciPart, F>(
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);
}

View File

@ -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>");
}

View File

@ -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),

View File

@ -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