#
cargo/ops/
cargo_doc.rs

    1use crate::core::Workspace;
    2use crate::core::compiler::{Compilation, CompileKind};
    3use crate::core::shell::Verbosity;
    4use crate::ops;
    5use crate::util;
    6use crate::util::CargoResult;
    7
    8use anyhow::{Error, bail};
    9use cargo_util::ProcessBuilder;
    10
    11use std::ffi::OsString;
    12use std::path::PathBuf;
    13use std::str::FromStr;
    14
    15/// Format of rustdoc [`--output-format`][1].
    16///
    17/// [1]: https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html#-w--output-format-output-format
    18#[derive(Debug, Default, Clone)]
    19pub enum OutputFormat {
    20    #[default]
    21    Html,
    22    Json,
    23}
    24
    25impl OutputFormat {
    26    pub const POSSIBLE_VALUES: [&'static str; 2] = ["html", "json"];
    27}
    28
    29impl FromStr for OutputFormat {
    30    // bail! error instead of string error like impl FromStr for Edition {
    31    type Err = Error;
    32
    33    fn from_str(s: &str) -> Result<Self, Self::Err> {
    34        match s {
    35            "json" => Ok(OutputFormat::Json),
    36            "html" => Ok(OutputFormat::Html),
    37            _ => bail!(
    38                "supported values for --output-format are `json` and `html`, \
    39						 but `{}` is unknown",
    40                s
    41            ),
    42        }
    43    }
    44}
    45
    46/// Strongly typed options for the `cargo doc` command.
    47#[derive(Debug)]
    48pub struct DocOptions {
    49    /// Whether to attempt to open the browser after compiling the docs
    50    pub open_result: bool,
    51    /// Same as `rustdoc --output-format`
    52    pub output_format: OutputFormat,
    53    /// Options to pass through to the compiler
    54    pub compile_opts: ops::CompileOptions,
    55}
    56
    57/// Main method for `cargo doc`.
    58pub fn doc(ws: &Workspace<'_>, options: &DocOptions) -> CargoResult<()> {
    59    let compilation = ops::compile(ws, &options.compile_opts)?;
    60
    61    if ws.gctx().cli_unstable().rustdoc_mergeable_info {
    62        merge_cross_crate_info(ws, &compilation)?;
    63    }
    64
    65    if options.open_result {
    66        let name = &compilation.root_crate_names.get(0).ok_or_else(|| {
    67            anyhow::anyhow!(
    68                "cannot open specified crate's documentation: no documentation generated"
    69            )
    70        })?;
    71        let kind = options.compile_opts.build_config.single_requested_kind()?;
    72
    73        let path = path_by_output_format(&compilation, &kind, &name, &options.output_format);
    74
    75        if path.exists() {
    76            util::open::open(&path, ws.gctx())?;
    77        }
    78    } else if ws.gctx().shell().verbosity() == Verbosity::Verbose {
    79        for name in &compilation.root_crate_names {
    80            for kind in &options.compile_opts.build_config.requested_kinds {
    81                let path =
    82                    path_by_output_format(&compilation, &kind, &name, &options.output_format);
    83                if path.exists() {
    84                    let mut shell = ws.gctx().shell();
    85                    let link = shell.err_file_hyperlink(&path);
    86                    shell.status("Generated", format!("{link}{}{link:#}", path.display()))?;
    87                }
    88            }
    89        }
    90    } else {
    91        let mut output = compilation.root_crate_names.iter().flat_map(|name| {
    92            options
    93                .compile_opts
    94                .build_config
    95                .requested_kinds
    96                .iter()
    97                .map(|kind| path_by_output_format(&compilation, kind, name, &options.output_format))
    98                .filter(|path| path.exists())
    99        });
    100        if let Some(first_path) = output.next() {
    101            let remaining = output.count();
    102            let remaining = match remaining {
    103                0 => "".to_owned(),
    104                1 => " and 1 other file".to_owned(),
    105                n => format!(" and {n} other files"),
    106            };
    107
    108            let mut shell = ws.gctx().shell();
    109            let link = shell.err_file_hyperlink(&first_path);
    110            shell.status(
    111                "Generated",
    112                format!("{link}{}{link:#}{remaining}", first_path.display(),),
    113            )?;
    114        }
    115    }
    116
    117    Ok(())
    118}
    119
    120fn merge_cross_crate_info(ws: &Workspace<'_>, compilation: &Compilation<'_>) -> CargoResult<()> {
    121    let Some(fingerprints) = compilation.rustdoc_fingerprints.as_ref() else {
    122        return Ok(());
    123    };
    124
    125    let now = std::time::Instant::now();
    126    for (kind, fingerprint) in fingerprints.iter() {
    127        let (target_name, build_dir, artifact_dir) = match kind {
    128            CompileKind::Host => ("host", ws.build_dir(), ws.target_dir()),
    129            CompileKind::Target(t) => {
    130                let name = t.short_name();
    131                let build_dir = ws.build_dir().join(name);
    132                let artifact_dir = ws.target_dir().join(name);
    133                (name, build_dir, artifact_dir)
    134            }
    135        };
    136
    137        // rustdoc needs to read doc parts files from build dir
    138        build_dir.open_ro_shared_create(".cargo-lock", ws.gctx(), "build directory")?;
    139        // rustdoc will write to `<artifact-dir>/doc/`
    140        artifact_dir.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "artifact directory")?;
    141        // We're leaking the layout implementation detail here.
    142        // This detail should be hidden when doc merge becomes a Unit of work inside the build.
    143        let rustdoc_artifact_dir = artifact_dir.join("doc");
    144
    145        if !fingerprint.is_dirty() {
    146            ws.gctx().shell().verbose(|shell| {
    147                shell.status("Fresh", format_args!("doc-merge for {target_name}"))
    148            })?;
    149            continue;
    150        }
    151
    152        fingerprint.persist(|doc_parts_dirs| {
    153            let mut cmd = ProcessBuilder::new(ws.gctx().rustdoc()?);
    154            if ws.gctx().extra_verbose() {
    155                cmd.display_env_vars();
    156            }
    157            cmd.retry_with_argfile(true);
    158            cmd.arg("-o")
    159                .arg(rustdoc_artifact_dir.as_path_unlocked())
    160                .arg("-Zunstable-options")
    161                .arg("--merge=finalize");
    162            for parts_dir in doc_parts_dirs {
    163                let mut include_arg = OsString::from("--include-parts-dir=");
    164                include_arg.push(parts_dir);
    165                cmd.arg(include_arg);
    166            }
    167
    168            let num_crates = doc_parts_dirs.len();
    169            let plural = if num_crates == 1 { "" } else { "s" };
    170
    171            ws.gctx().shell().status(
    172                "Merging",
    173                format_args!("{num_crates} doc{plural} for {target_name}"),
    174            )?;
    175            ws.gctx()
    176                .shell()
    177                .verbose(|shell| shell.status("Running", cmd.to_string()))?;
    178            cmd.exec()?;
    179
    180            Ok(())
    181        })?;
    182    }
    183
    184    let time_elapsed = util::elapsed(now.elapsed());
    185    ws.gctx().shell().status(
    186        "Finished",
    187        format_args!("documentation merge in {time_elapsed}"),
    188    )?;
    189
    190    Ok(())
    191}
    192
    193fn path_by_output_format(
    194    compilation: &Compilation<'_>,
    195    kind: &CompileKind,
    196    name: &str,
    197    output_format: &OutputFormat,
    198) -> PathBuf {
    199    if matches!(output_format, OutputFormat::Json) {
    200        compilation.root_output[kind]
    201            .with_file_name("doc")
    202            .join(format!("{}.json", name))
    203    } else {
    204        compilation.root_output[kind]
    205            .with_file_name("doc")
    206            .join(name)
    207            .join("index.html")
    208    }
    209}