blob: f088ae8ad33fbea309096885e467a608383b8880 [file] [edit]
use crate::compiler::{Compilation, CompileKind};
use crate::ops;
use crate::util;
use crate::util::CargoResult;
use crate::workspace::Workspace;
use anyhow::{Error, bail};
use cargo_util::ProcessBuilder;
use cargo_util_terminal::Verbosity;
use std::ffi::OsString;
use std::path::PathBuf;
use std::str::FromStr;
/// Format of rustdoc [`--output-format`][1].
///
/// [1]: https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html#-w--output-format-output-format
#[derive(Debug, Default, Clone)]
pub enum OutputFormat {
#[default]
Html,
Json,
}
impl OutputFormat {
pub const POSSIBLE_VALUES: [&'static str; 2] = ["html", "json"];
}
impl FromStr for OutputFormat {
// bail! error instead of string error like impl FromStr for Edition {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"json" => Ok(OutputFormat::Json),
"html" => Ok(OutputFormat::Html),
_ => bail!(
"supported values for --output-format are `json` and `html`, \
but `{}` is unknown",
s
),
}
}
}
/// Strongly typed options for the `cargo doc` command.
#[derive(Debug)]
pub struct DocOptions {
/// Whether to attempt to open the browser after compiling the docs
pub open_result: bool,
/// Same as `rustdoc --output-format`
pub output_format: OutputFormat,
/// Options to pass through to the compiler
pub compile_opts: ops::CompileOptions,
}
/// Main method for `cargo doc`.
pub fn doc(ws: &Workspace<'_>, options: &DocOptions) -> CargoResult<()> {
let compilation = ops::compile(ws, &options.compile_opts)?;
if ws.gctx().cli_unstable().rustdoc_mergeable_info {
merge_cross_crate_info(ws, &compilation)?;
}
if options.open_result {
let name = &compilation.root_crate_names.get(0).ok_or_else(|| {
anyhow::anyhow!(
"cannot open specified crate's documentation: no documentation generated"
)
})?;
let kind = options.compile_opts.build_config.single_requested_kind()?;
let path = path_by_output_format(&compilation, &kind, &name, &options.output_format);
if path.exists() {
util::open::open(&path, ws.gctx())?;
}
} else if ws.gctx().shell().verbosity() == Verbosity::Verbose {
for name in &compilation.root_crate_names {
for kind in &options.compile_opts.build_config.requested_kinds {
let path =
path_by_output_format(&compilation, &kind, &name, &options.output_format);
if path.exists() {
let mut shell = ws.gctx().shell();
let link = shell.err_file_hyperlink(&path);
shell.status("Generated", format!("{link}{}{link:#}", path.display()))?;
}
}
}
} else {
let mut output = compilation.root_crate_names.iter().flat_map(|name| {
options
.compile_opts
.build_config
.requested_kinds
.iter()
.map(|kind| path_by_output_format(&compilation, kind, name, &options.output_format))
.filter(|path| path.exists())
});
if let Some(first_path) = output.next() {
let remaining = output.count();
let remaining = match remaining {
0 => "".to_owned(),
1 => " and 1 other file".to_owned(),
n => format!(" and {n} other files"),
};
let mut shell = ws.gctx().shell();
let link = shell.err_file_hyperlink(&first_path);
shell.status(
"Generated",
format!("{link}{}{link:#}{remaining}", first_path.display(),),
)?;
}
}
Ok(())
}
fn merge_cross_crate_info(ws: &Workspace<'_>, compilation: &Compilation<'_>) -> CargoResult<()> {
let Some(fingerprints) = compilation.rustdoc_fingerprints.as_ref() else {
return Ok(());
};
let now = std::time::Instant::now();
for (kind, fingerprint) in fingerprints.iter() {
let (target_name, build_dir, artifact_dir) = match kind {
CompileKind::Host => ("host", ws.build_dir(), ws.target_dir()),
CompileKind::Target(t) => {
let name = t.short_name();
let build_dir = ws.build_dir().join(name);
let artifact_dir = ws.target_dir().join(name);
(name, build_dir, artifact_dir)
}
};
// rustdoc needs to read doc parts files from build dir
build_dir.open_ro_shared_create(".cargo-lock", ws.gctx(), "build directory")?;
// rustdoc will write to `<artifact-dir>/doc/`
artifact_dir.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "artifact directory")?;
// We're leaking the layout implementation detail here.
// This detail should be hidden when doc merge becomes a Unit of work inside the build.
let rustdoc_artifact_dir = artifact_dir.join("doc");
if !fingerprint.is_dirty() {
ws.gctx().shell().verbose(|shell| {
shell.status("Fresh", format_args!("doc-merge for {target_name}"))
})?;
continue;
}
fingerprint.persist(|doc_parts_dirs| {
let mut cmd = ProcessBuilder::new(ws.gctx().rustdoc()?);
if ws.gctx().extra_verbose() {
cmd.display_env_vars();
}
cmd.retry_with_argfile(true);
cmd.arg("-o")
.arg(rustdoc_artifact_dir.as_path_unlocked())
.arg("-Zunstable-options");
cmd.args(&compilation.rustdocflags[kind]);
for parts_dir in doc_parts_dirs {
let mut include_arg = OsString::from("--read-doc-meta-dir=");
include_arg.push(parts_dir);
cmd.arg(include_arg);
}
let num_crates = doc_parts_dirs.len();
let plural = if num_crates == 1 { "" } else { "s" };
ws.gctx().shell().status(
"Merging",
format_args!("{num_crates} doc{plural} for {target_name}"),
)?;
ws.gctx()
.shell()
.verbose(|shell| shell.status("Running", cmd.to_string()))?;
cmd.exec()?;
Ok(())
})?;
}
let time_elapsed = util::elapsed(now.elapsed());
ws.gctx().shell().status(
"Finished",
format_args!("documentation merge in {time_elapsed}"),
)?;
Ok(())
}
fn path_by_output_format(
compilation: &Compilation<'_>,
kind: &CompileKind,
name: &str,
output_format: &OutputFormat,
) -> PathBuf {
if matches!(output_format, OutputFormat::Json) {
compilation.root_output[kind]
.with_file_name("doc")
.join(format!("{}.json", name))
} else {
compilation.root_output[kind]
.with_file_name("doc")
.join(name)
.join("index.html")
}
}