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#[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 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#[derive(Debug)]
50pub struct DocOptions {
51 pub open_result: bool,
53 pub output_format: OutputFormat,
55 pub compile_opts: ops::CompileOptions,
57}
58
59pub 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 build_dir.open_ro_shared_create(".cargo-lock", ws.gctx(), "build directory")?;
148 artifact_dir.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "artifact directory")?;
150 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}