#
cargo/ops/
cargo_doc.rs

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