1use super::BuildError;
2use cargo_metadata::TargetKind;
3use tracing::*;
4
5use std::io::Write;
6use std::{
7 path::{Path, PathBuf},
8 process::Command,
9};
10
11const OVERRIDDEN_TOOLCHAIN: Option<&str> =
15 option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK"); struct CaptureOutput<O, E> {
18 stdout: O,
19 stderr: E,
20}
21
22fn run_cargo_rustdoc<O, E>(
23 options: Builder,
24 capture_output: Option<CaptureOutput<O, E>>,
25) -> Result<PathBuf, BuildError>
26where
27 O: Write,
28 E: Write,
29{
30
31 let mut cmd = cargo_rustdoc_command(&options)?;
32
33 info!("Running {cmd:?}");
34
35 let status = match capture_output {
36 Some(CaptureOutput {
37 mut stdout,
38 mut stderr,
39 }) => {
40 let output = cmd.output().map_err(|e| {
41 BuildError::CommandExecutionError(format!(
42 "Failed to run `{cmd:?}`: {e}"
43 ))
44 })?;
45 stdout.write_all(&output.stdout).map_err(|e| {
46 BuildError::CapturedOutputError(format!("Failed to write stdout: {e}"))
47 })?;
48 stderr.write_all(&output.stderr).map_err(|e| {
49 BuildError::CapturedOutputError(format!("Failed to write stderr: {e}"))
50 })?;
51 output.status
52 }
53 None => cmd.status().map_err(|e| {
54 BuildError::CommandExecutionError(format!("Failed to run `{cmd:?}`: {e}"))
55 })?,
56 };
57
58 if status.success() {
59 rustdoc_json_path_for_manifest_path(
60 &options.manifest_path,
61 options.package.as_deref(),
62 &options.package_target,
63 options.target_dir.as_deref(),
64 options.target.as_deref(),
65 )
66 } else {
67 let manifest = cargo_manifest::Manifest::from_path(&options.manifest_path)?;
68 if manifest.package.is_none() && manifest.workspace.is_some() {
69 Err(BuildError::VirtualManifest(options.manifest_path))
70 } else {
71 Err(BuildError::BuildRustdocJsonError)
72 }
73 }
74}
75
76fn cargo_rustdoc_command(options: &Builder) -> Result<Command, BuildError> {
82 let Builder {
83 toolchain: requested_toolchain,
84 manifest_path,
85 target_dir,
86 target,
87 quiet,
88 silent,
89 color,
90 no_default_features,
91 all_features,
92 features,
93 package,
94 package_target,
95 document_private_items,
96 cap_lints,
97 } = options;
98
99 let mut command =
100 match OVERRIDDEN_TOOLCHAIN.or(requested_toolchain.as_deref()) {
101 None => Command::new("cargo"),
102 Some(toolchain) => {
103 if !rustup_installed() {
104 return Err(BuildError::General(String::from(
105 "required program rustup not found in PATH. Is it installed?",
106 )));
107 }
108 let mut cmd = Command::new("rustup");
109 cmd.args(["run", toolchain, "cargo"]);
110
111 let env_vars = vec![
112 ("RUSTC_BOOTSTRAP", "1".to_string()),
113 ];
114 cmd.envs(env_vars);
115 cmd
116 }
117 };
118
119 command.arg("rustdoc");
120 match package_target {
121 PackageTarget::Lib => command.arg("--lib"),
122 PackageTarget::Bin(target) => command.args(["--bin", target]),
123 PackageTarget::Example(target) => command.args(["--example", target]),
124 PackageTarget::Test(target) => command.args(["--test", target]),
125 PackageTarget::Bench(target) => command.args(["--bench", target]),
126 };
127 if let Some(target_dir) = target_dir {
128 command.arg("--target-dir");
129 command.arg(target_dir);
130 }
131 if *quiet {
132 command.arg("--quiet");
133 }
134 if *silent {
135 command.stdout(std::process::Stdio::null());
136 command.stderr(std::process::Stdio::null());
137 }
138 match *color {
139 Color::Always => command.arg("--color").arg("always"),
140 Color::Never => command.arg("--color").arg("never"),
141 Color::Auto => command.arg("--color").arg("auto"),
142 };
143 command.arg("--manifest-path");
144 command.arg(manifest_path);
145 if let Some(target) = target {
146 command.arg("--target");
147 command.arg(target);
148 }
149 if *no_default_features {
150 command.arg("--no-default-features");
151 }
152 if *all_features {
153 command.arg("--all-features");
154 }
155 for feature in features {
156 command.args(["--features", feature]);
157 }
158 if let Some(package) = package {
159 command.args(["--package", package]);
160 }
161 command.arg("--");
162 command.args(["-Z", "unstable-options"]);
163 command.args(["--output-format", "json"]);
164 if *document_private_items {
165 command.arg("--document-private-items");
166 }
167 if let Some(cap_lints) = cap_lints {
168 command.args(["--cap-lints", cap_lints]);
169 }
170 println!("QAQ {command:?}");
171 Ok(command)
172}
173
174#[instrument(ret(level = Level::DEBUG))]
177fn rustdoc_json_path_for_manifest_path(
178 manifest_path: &Path,
179 package: Option<&str>,
180 package_target: &PackageTarget,
181 target_dir: Option<&Path>,
182 target: Option<&str>,
183) -> Result<PathBuf, BuildError> {
184 let target_dir = match target_dir {
185 Some(target_dir) => target_dir.to_owned(),
186 None => target_directory(manifest_path)?,
187 };
188
189 let package_target_name = match package_target {
191 PackageTarget::Lib => library_name(manifest_path, package)?,
192 PackageTarget::Bin(name)
193 | PackageTarget::Example(name)
194 | PackageTarget::Test(name)
195 | PackageTarget::Bench(name) => name.clone(),
196 }
197 .replace('-', "_");
198
199 let mut rustdoc_json_path = target_dir;
200 if let Some(target) = target {
202 rustdoc_json_path.push(target);
203 }
204 rustdoc_json_path.push("doc");
205 rustdoc_json_path.push(package_target_name);
206 rustdoc_json_path.set_extension("json");
207 Ok(rustdoc_json_path)
208}
209
210pub fn rustup_installed() -> bool {
212 let mut check_rustup = std::process::Command::new("rustup");
213 check_rustup.arg("--version");
214 check_rustup.stdout(std::process::Stdio::null());
215 check_rustup.stderr(std::process::Stdio::null());
216 check_rustup.status().map(|s| s.success()).unwrap_or(false)
217}
218
219fn target_directory(
222 manifest_path: impl AsRef<Path>,
223) -> Result<PathBuf, BuildError> {
224 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
225 metadata_cmd.manifest_path(manifest_path.as_ref());
226 let metadata = metadata_cmd.exec()?;
227 Ok(metadata.target_directory.as_std_path().to_owned())
228}
229
230fn library_name(
233 manifest_path: impl AsRef<Path>,
234 package_name: Option<&str>,
235) -> Result<String, BuildError> {
236 let package_name = if let Some(package_name) = package_name {
237 package_name.to_owned()
238 } else {
239 let manifest = cargo_manifest::Manifest::from_path(manifest_path.as_ref())?;
241 manifest
242 .package
243 .ok_or_else(|| {
244 BuildError::VirtualManifest(manifest_path.as_ref().to_owned())
245 })?
246 .name
247 .to_owned()
248 };
249
250 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
251 metadata_cmd.manifest_path(manifest_path.as_ref());
252 let metadata = metadata_cmd.exec()?;
253
254 let package = metadata
255 .packages
256 .iter()
257 .find(|p| p.name == package_name)
258 .ok_or_else(|| {
259 BuildError::VirtualManifest(manifest_path.as_ref().to_owned())
260 })?;
261
262 for target in &package.targets {
263 if target.kind.contains(&TargetKind::Lib) {
264 return Ok(target.name.to_owned());
265 }
266 }
267
268 Ok(package.name.clone())
269}
270
271#[derive(Clone, Copy, Debug)]
273pub enum Color {
274 Always,
276 Never,
278 Auto,
280}
281
282#[derive(Clone, Debug)]
285pub struct Builder {
286 toolchain: Option<String>,
287 manifest_path: PathBuf,
288 target_dir: Option<PathBuf>,
289 target: Option<String>,
290 quiet: bool,
291 silent: bool,
292 color: Color,
293 no_default_features: bool,
294 all_features: bool,
295 features: Vec<String>,
296 package: Option<String>,
297 package_target: PackageTarget,
298 document_private_items: bool,
299 cap_lints: Option<String>,
300}
301
302impl Default for Builder {
303 fn default() -> Self {
304 Self {
305 toolchain: None,
306 manifest_path: PathBuf::from("Cargo.toml"),
307 target_dir: None,
308 target: None,
309 quiet: false,
310 silent: false,
311 color: Color::Auto,
312 no_default_features: false,
313 all_features: false,
314 features: vec![],
315 package: None,
316 package_target: PackageTarget::default(),
317 document_private_items: false,
318 cap_lints: Some(String::from("warn")),
319 }
320 }
321}
322
323impl Builder {
324 #[must_use]
337 pub fn toolchain(mut self, toolchain: impl Into<String>) -> Self {
338 self.toolchain = Some(toolchain.into());
339 self
340 }
341
342 #[must_use]
344 pub fn clear_toolchain(mut self) -> Self {
345 self.toolchain = None;
346 self
347 }
348
349 #[must_use]
351 pub fn manifest_path(mut self, manifest_path: impl AsRef<Path>) -> Self {
352 manifest_path.as_ref().clone_into(&mut self.manifest_path);
353 self
354 }
355
356 #[must_use]
360 pub fn target_dir(mut self, target_dir: impl AsRef<Path>) -> Self {
361 self.target_dir = Some(target_dir.as_ref().to_owned());
362 self
363 }
364
365 #[must_use]
367 pub fn clear_target_dir(mut self) -> Self {
368 self.target_dir = None;
369 self
370 }
371
372 #[must_use]
374 pub const fn quiet(mut self, quiet: bool) -> Self {
375 self.quiet = quiet;
376 self
377 }
378
379 #[must_use]
381 pub const fn silent(mut self, silent: bool) -> Self {
382 self.silent = silent;
383 self
384 }
385
386 #[must_use]
388 pub const fn color(mut self, color: Color) -> Self {
389 self.color = color;
390 self
391 }
392
393 #[must_use]
395 pub fn target(mut self, target: String) -> Self {
396 self.target = Some(target);
397 self
398 }
399
400 #[must_use]
402 pub const fn no_default_features(
403 mut self,
404 no_default_features: bool,
405 ) -> Self {
406 self.no_default_features = no_default_features;
407 self
408 }
409
410 #[must_use]
412 pub const fn all_features(mut self, all_features: bool) -> Self {
413 self.all_features = all_features;
414 self
415 }
416
417 #[must_use]
419 pub fn features<I: IntoIterator<Item = S>, S: AsRef<str>>(
420 mut self,
421 features: I,
422 ) -> Self {
423 self.features = features
424 .into_iter()
425 .map(|item| item.as_ref().to_owned())
426 .collect();
427 self
428 }
429
430 #[must_use]
432 pub fn package(mut self, package: impl AsRef<str>) -> Self {
433 self.package = Some(package.as_ref().to_owned());
434 self
435 }
436
437 #[must_use]
439 pub fn package_target(mut self, package_target: PackageTarget) -> Self {
440 self.package_target = package_target;
441 self
442 }
443
444 #[must_use]
446 pub fn document_private_items(
447 mut self,
448 document_private_items: bool,
449 ) -> Self {
450 self.document_private_items = document_private_items;
451 self
452 }
453
454 #[must_use]
456 pub fn cap_lints(mut self, cap_lints: Option<impl AsRef<str>>) -> Self {
457 self.cap_lints = cap_lints.map(|c| c.as_ref().to_owned());
458 self
459 }
460
461 pub fn build(self) -> Result<PathBuf, BuildError> {
475 run_cargo_rustdoc::<std::io::Sink, std::io::Sink>(self, None)
476 }
477
478 pub fn build_with_captured_output(
509 self,
510 stdout: impl Write,
511 stderr: impl Write,
512 ) -> Result<PathBuf, BuildError> {
513 let capture_output = CaptureOutput { stdout, stderr };
514 run_cargo_rustdoc(self, Some(capture_output))
515 }
516}
517
518#[derive(Default, Debug, Clone)]
520#[non_exhaustive]
521pub enum PackageTarget {
522 #[default]
524 Lib,
525 Bin(String),
527 Example(String),
529 Test(String),
531 Bench(String),
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
540 fn ensure_toolchain_not_overridden() {
541 if option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK").is_none() {
545 assert!(OVERRIDDEN_TOOLCHAIN.is_none());
546 }
547 }
548}