#
cargo/ops/
cargo_doc.rs

    1use crate::core::compiler::{Compilation, CompileKind};
    2use crate::core::{shell::Verbosity, Shell, Workspace};
    3use crate::ops;
    4use crate::util::context::{GlobalContext, PathAndArgs};
    5use crate::util::CargoResult;
    6use anyhow::{bail, Error};
    7use std::path::Path;
    8use std::path::PathBuf;
    9use std::process::Command;
    10use std::str::FromStr;
    11
    12/// Format of rustdoc [`--output-format`][1].
    13///
    14/// [1]: https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html#-w--output-format-output-format
    15#[derive(Debug, Default, Clone)]
    16pub enum OutputFormat {
    17    #[default]
    18    Html,
    19    Json,
    20}
    21
    22impl OutputFormat {
    23    pub const POSSIBLE_VALUES: [&'static str; 2] = ["html", "json"];
    24}
    25
    26impl FromStr for OutputFormat {
    27    // bail! error instead of string error like impl FromStr for Edition {
    28    type Err = Error;
    29
    30    fn from_str(s: &str) -> Result<Self, Self::Err> {
    31        match s {
    32            "json" => Ok(OutputFormat::Json),
    33            "html" => Ok(OutputFormat::Html),
    34            _ => bail!(
    35                "supported values for --output-format are `json` and `html`, \
    36						 but `{}` is unknown",
    37                s
    38            ),
    39        }
    40    }
    41}
    42
    43/// Strongly typed options for the `cargo doc` command.
    44#[derive(Debug)]
    45pub struct DocOptions {
    46    /// Whether to attempt to open the browser after compiling the docs
    47    pub open_result: bool,
    48    /// Same as `rustdoc --output-format`
    49    pub output_format: OutputFormat,
    50    /// Options to pass through to the compiler
    51    pub compile_opts: ops::CompileOptions,
    52}
    53
    54/// Main method for `cargo doc`.
    55pub fn doc(ws: &Workspace<'_>, options: &DocOptions) -> CargoResult<()> {
    56    let compilation = ops::compile(ws, &options.compile_opts)?;
    57
    58    if options.open_result {
    59        let name = &compilation.root_crate_names.get(0).ok_or_else(|| {
    60            anyhow::anyhow!(
    61                "cannot open specified crate's documentation: no documentation generated"
    62            )
    63        })?;
    64        let kind = options.compile_opts.build_config.single_requested_kind()?;
    65
    66        let path = path_by_output_format(&compilation, &kind, &name, &options.output_format);
    67
    68        if path.exists() {
    69            let config_browser = {
    70                let cfg: Option<PathAndArgs> = ws.gctx().get("doc.browser")?;
    71                cfg.map(|path_args| (path_args.path.resolve_program(ws.gctx()), path_args.args))
    72            };
    73            let mut shell = ws.gctx().shell();
    74            let link = shell.err_file_hyperlink(&path);
    75            shell.status("Opening", format!("{link}{}{link:#}", path.display()))?;
    76            open_docs(&path, &mut shell, config_browser, 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 path_by_output_format(
    121    compilation: &Compilation<'_>,
    122    kind: &CompileKind,
    123    name: &str,
    124    output_format: &OutputFormat,
    125) -> PathBuf {
    126    if matches!(output_format, OutputFormat::Json) {
    127        compilation.root_output[kind]
    128            .with_file_name("doc")
    129            .join(format!("{}.json", name))
    130    } else {
    131        compilation.root_output[kind]
    132            .with_file_name("doc")
    133            .join(name)
    134            .join("index.html")
    135    }
    136}
    137
    138fn open_docs(
    139    path: &Path,
    140    shell: &mut Shell,
    141    config_browser: Option<(PathBuf, Vec<String>)>,
    142    gctx: &GlobalContext,
    143) -> CargoResult<()> {
    144    let browser =
    145        config_browser.or_else(|| Some((PathBuf::from(gctx.get_env_os("BROWSER")?), Vec::new())));
    146
    147    match browser {
    148        Some((browser, initial_args)) => {
    149            if let Err(e) = Command::new(&browser).args(initial_args).arg(path).status() {
    150                shell.warn(format!(
    151                    "Couldn't open docs with {}: {}",
    152                    browser.to_string_lossy(),
    153                    e
    154                ))?;
    155            }
    156        }
    157        None => {
    158            if let Err(e) = opener::open(&path) {
    159                let e = e.into();
    160                crate::display_warning_with_error("couldn't open docs", &e, shell);
    161            }
    162        }
    163    };
    164
    165    Ok(())
    166}