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}