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}