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#[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 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 build_dir.open_ro_shared_create(".cargo-lock", ws.gctx(), "build directory")?;
139 artifact_dir.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "artifact directory")?;
141 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}