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}