Skip to main content

cargo/ops/
cargo_doc.rs

1use crate::compiler::{Compilation, CompileKind};
2use crate::ops;
3use crate::util;
4use crate::util::CargoResult;
5use crate::workspace::Workspace;
6
7use anyhow::{Error, bail};
8use cargo_util::ProcessBuilder;
9use cargo_util_terminal::Verbosity;
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    let wants_json_doc = matches!(options.output_format, OutputFormat::Json);
62    if ws.gctx().cli_unstable().rustdoc_mergeable_info && !wants_json_doc {
63        merge_cross_crate_info(ws, &compilation)?;
64    }
65
66    if options.open_result {
67        let name = &compilation.root_crate_names.get(0).ok_or_else(|| {
68            anyhow::anyhow!(
69                "cannot open specified crate's documentation: no documentation generated"
70            )
71        })?;
72        let kind = options.compile_opts.build_config.single_requested_kind()?;
73
74        let path = path_by_output_format(&compilation, &kind, &name, &options.output_format);
75
76        if path.exists() {
77            util::open::open(&path, ws.gctx())?;
78        }
79    } else if ws.gctx().shell().verbosity() == Verbosity::Verbose {
80        for name in &compilation.root_crate_names {
81            for kind in &options.compile_opts.build_config.requested_kinds {
82                let path =
83                    path_by_output_format(&compilation, &kind, &name, &options.output_format);
84                if path.exists() {
85                    let mut shell = ws.gctx().shell();
86                    let link = shell.err_file_hyperlink(&path);
87                    shell.status("Generated", format!("{link}{}{link:#}", path.display()))?;
88                }
89            }
90        }
91    } else {
92        let mut output = compilation.root_crate_names.iter().flat_map(|name| {
93            options
94                .compile_opts
95                .build_config
96                .requested_kinds
97                .iter()
98                .map(|kind| path_by_output_format(&compilation, kind, name, &options.output_format))
99                .filter(|path| path.exists())
100        });
101        if let Some(first_path) = output.next() {
102            let remaining = output.count();
103            let remaining = match remaining {
104                0 => "".to_owned(),
105                1 => " and 1 other file".to_owned(),
106                n => format!(" and {n} other files"),
107            };
108
109            let mut shell = ws.gctx().shell();
110            let link = shell.err_file_hyperlink(&first_path);
111            shell.status(
112                "Generated",
113                format!("{link}{}{link:#}{remaining}", first_path.display(),),
114            )?;
115        }
116    }
117
118    Ok(())
119}
120
121fn merge_cross_crate_info(ws: &Workspace<'_>, compilation: &Compilation<'_>) -> CargoResult<()> {
122    let Some(fingerprints) = compilation.rustdoc_fingerprints.as_ref() else {
123        return Ok(());
124    };
125
126    let now = std::time::Instant::now();
127    for (kind, fingerprint) in fingerprints.iter() {
128        let (target_name, build_dir, artifact_dir) = match kind {
129            CompileKind::Host => ("host", ws.build_dir(), ws.target_dir()),
130            CompileKind::Target(t) => {
131                let name = t.short_name();
132                let build_dir = ws.build_dir().join(name);
133                let artifact_dir = ws.target_dir().join(name);
134                (name, build_dir, artifact_dir)
135            }
136        };
137
138        // rustdoc needs to read doc parts files from build dir
139        build_dir.open_ro_shared_create(".cargo-lock", ws.gctx(), "build directory")?;
140        // rustdoc will write to `<artifact-dir>/doc/`
141        artifact_dir.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "artifact directory")?;
142        // We're leaking the layout implementation detail here.
143        // This detail should be hidden when doc merge becomes a Unit of work inside the build.
144        let rustdoc_artifact_dir = artifact_dir.join("doc");
145
146        if !fingerprint.is_dirty() {
147            ws.gctx().shell().verbose(|shell| {
148                shell.status("Fresh", format_args!("doc-merge for {target_name}"))
149            })?;
150            continue;
151        }
152
153        fingerprint.persist(|doc_parts_dirs| {
154            let mut cmd = ProcessBuilder::new(ws.gctx().rustdoc()?);
155            if ws.gctx().extra_verbose() {
156                cmd.display_env_vars();
157            }
158            cmd.retry_with_argfile(true);
159            cmd.arg("-o")
160                .arg(rustdoc_artifact_dir.as_path_unlocked())
161                .arg("-Zunstable-options");
162            cmd.args(&compilation.rustdocflags[kind]);
163            for parts_dir in doc_parts_dirs {
164                let mut include_arg = OsString::from("--read-doc-meta-dir=");
165                include_arg.push(parts_dir);
166                cmd.arg(include_arg);
167            }
168
169            let num_crates = doc_parts_dirs.len();
170            let plural = if num_crates == 1 { "" } else { "s" };
171
172            ws.gctx().shell().status(
173                "Merging",
174                format_args!("{num_crates} doc{plural} for {target_name}"),
175            )?;
176            ws.gctx()
177                .shell()
178                .verbose(|shell| shell.status("Running", cmd.to_string()))?;
179            cmd.exec()?;
180
181            Ok(())
182        })?;
183    }
184
185    let time_elapsed = util::elapsed(now.elapsed());
186    ws.gctx().shell().status(
187        "Finished",
188        format_args!("documentation merge in {time_elapsed}"),
189    )?;
190
191    Ok(())
192}
193
194fn path_by_output_format(
195    compilation: &Compilation<'_>,
196    kind: &CompileKind,
197    name: &str,
198    output_format: &OutputFormat,
199) -> PathBuf {
200    if matches!(output_format, OutputFormat::Json) {
201        compilation.root_output[kind]
202            .with_file_name("doc")
203            .join(format!("{}.json", name))
204    } else {
205        compilation.root_output[kind]
206            .with_file_name("doc")
207            .join(name)
208            .join("index.html")
209    }
210}