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