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