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 let mut cmd = cargo_rustdoc_command(&options)?;
31
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!(
41 "Failed to run `{cmd:?}`: {e}"
42 ))
43 })?;
44 stdout.write_all(&output.stdout).map_err(|e| {
45 BuildError::CapturedOutputError(format!("Failed to write stdout: {e}"))
46 })?;
47 stderr.write_all(&output.stderr).map_err(|e| {
48 BuildError::CapturedOutputError(format!("Failed to write stderr: {e}"))
49 })?;
50 output.status
51 }
52 None => cmd.status().map_err(|e| {
53 BuildError::CommandExecutionError(format!("Failed to run `{cmd:?}`: {e}"))
54 })?,
55 };
56
57 if status.success() {
58 rustdoc_json_path_for_manifest_path(
59 &options.manifest_path,
60 options.package.as_deref(),
61 &options.package_target,
62 options.target_dir.as_deref(),
63 options.target.as_deref(),
64 )
65 } else {
66 let manifest = cargo_manifest::Manifest::from_path(&options.manifest_path)?;
67 if manifest.package.is_none() && manifest.workspace.is_some() {
68 Err(BuildError::VirtualManifest(options.manifest_path))
69 } else {
70 Err(BuildError::BuildRustdocJsonError)
71 }
72 }
73}
74
75fn cargo_rustdoc_command(options: &Builder) -> Result<Command, BuildError> {
81 let Builder {
82 toolchain: requested_toolchain,
83 manifest_path,
84 target_dir,
85 target,
86 quiet,
87 silent,
88 color,
89 no_default_features,
90 all_features,
91 features,
92 package,
93 package_target,
94 document_private_items,
95 cap_lints,
96 use_stable,
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 if *use_stable {
112 let env_vars = vec![("RUSTC_BOOTSTRAP", "1".to_string())];
113 cmd.envs(env_vars);
114 }
115
116 cmd
117 }
118 };
119
120 command.arg("rustdoc");
121 match package_target {
122 PackageTarget::Lib => command.arg("--lib"),
123 PackageTarget::Bin(target) => command.args(["--bin", target]),
124 PackageTarget::Example(target) => command.args(["--example", target]),
125 PackageTarget::Test(target) => command.args(["--test", target]),
126 PackageTarget::Bench(target) => command.args(["--bench", target]),
127 };
128 if let Some(target_dir) = target_dir {
129 command.arg("--target-dir");
130 command.arg(target_dir);
131 }
132 if *quiet {
133 command.arg("--quiet");
134 }
135 if *silent {
136 command.stdout(std::process::Stdio::null());
137 command.stderr(std::process::Stdio::null());
138 }
139 match *color {
140 Color::Always => command.arg("--color").arg("always"),
141 Color::Never => command.arg("--color").arg("never"),
142 Color::Auto => command.arg("--color").arg("auto"),
143 };
144 command.arg("--manifest-path");
145 command.arg(manifest_path);
146 if let Some(target) = target {
147 command.arg("--target");
148 command.arg(target);
149 }
150 if *no_default_features {
151 command.arg("--no-default-features");
152 }
153 if *all_features {
154 command.arg("--all-features");
155 }
156 for feature in features {
157 command.args(["--features", feature]);
158 }
159 if let Some(package) = package {
160 command.args(["--package", package]);
161 }
162 command.arg("--");
163 command.args(["-Z", "unstable-options"]);
164 command.args(["--output-format", "json"]);
165 if *document_private_items {
166 command.arg("--document-private-items");
167 }
168 if let Some(cap_lints) = cap_lints {
169 command.args(["--cap-lints", cap_lints]);
170 }
171
172 Ok(command)
173}
174
175#[instrument(ret(level = Level::DEBUG))]
178fn rustdoc_json_path_for_manifest_path(
179 manifest_path: &Path,
180 package: Option<&str>,
181 package_target: &PackageTarget,
182 target_dir: Option<&Path>,
183 target: Option<&str>,
184) -> Result<PathBuf, BuildError> {
185 let target_dir = match target_dir {
186 Some(target_dir) => target_dir.to_owned(),
187 None => target_directory(manifest_path)?,
188 };
189
190 let package_target_name = match package_target {
192 PackageTarget::Lib => library_name(manifest_path, package)?,
193 PackageTarget::Bin(name)
194 | PackageTarget::Example(name)
195 | PackageTarget::Test(name)
196 | PackageTarget::Bench(name) => name.clone(),
197 }
198 .replace('-', "_");
199
200 let mut rustdoc_json_path = target_dir;
201 if let Some(target) = target {
203 rustdoc_json_path.push(target);
204 }
205 rustdoc_json_path.push("doc");
206 rustdoc_json_path.push(package_target_name);
207 rustdoc_json_path.set_extension("json");
208 Ok(rustdoc_json_path)
209}
210
211pub fn rustup_installed() -> bool {
213 let mut check_rustup = std::process::Command::new("rustup");
214 check_rustup.arg("--version");
215 check_rustup.stdout(std::process::Stdio::null());
216 check_rustup.stderr(std::process::Stdio::null());
217 check_rustup.status().map(|s| s.success()).unwrap_or(false)
218}
219
220fn target_directory(
223 manifest_path: impl AsRef<Path>,
224) -> Result<PathBuf, BuildError> {
225 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
226 metadata_cmd.manifest_path(manifest_path.as_ref());
227 let metadata = metadata_cmd.exec()?;
228 Ok(metadata.target_directory.as_std_path().to_owned())
229}
230
231fn library_name(
234 manifest_path: impl AsRef<Path>,
235 package_name: Option<&str>,
236) -> Result<String, BuildError> {
237 let package_name = if let Some(package_name) = package_name {
238 package_name.to_owned()
239 } else {
240 let manifest = cargo_manifest::Manifest::from_path(manifest_path.as_ref())?;
242 manifest
243 .package
244 .ok_or_else(|| {
245 BuildError::VirtualManifest(manifest_path.as_ref().to_owned())
246 })?
247 .name
248 .to_owned()
249 };
250
251 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
252 metadata_cmd.manifest_path(manifest_path.as_ref());
253 let metadata = metadata_cmd.exec()?;
254
255 let package = metadata
256 .packages
257 .iter()
258 .find(|p| p.name == package_name)
259 .ok_or_else(|| {
260 BuildError::VirtualManifest(manifest_path.as_ref().to_owned())
261 })?;
262
263 for target in &package.targets {
264 if target.kind.contains(&TargetKind::Lib) {
265 return Ok(target.name.to_owned());
266 }
267 }
268
269 Ok(package.name.clone())
270}
271
272#[derive(Clone, Copy, Debug)]
274pub enum Color {
275 Always,
277 Never,
279 Auto,
281}
282
283#[derive(Clone, Debug)]
286pub struct Builder {
287 toolchain: Option<String>,
288 manifest_path: PathBuf,
289 target_dir: Option<PathBuf>,
290 target: Option<String>,
291 quiet: bool,
292 silent: bool,
293 color: Color,
294 no_default_features: bool,
295 all_features: bool,
296 features: Vec<String>,
297 package: Option<String>,
298 package_target: PackageTarget,
299 document_private_items: bool,
300 cap_lints: Option<String>,
301 use_stable: bool,
302}
303
304impl Default for Builder {
305 fn default() -> Self {
306 Self {
307 toolchain: None,
308 manifest_path: PathBuf::from("Cargo.toml"),
309 target_dir: None,
310 target: None,
311 quiet: false,
312 silent: false,
313 color: Color::Auto,
314 no_default_features: false,
315 all_features: false,
316 features: vec![],
317 package: None,
318 package_target: PackageTarget::default(),
319 document_private_items: false,
320 cap_lints: Some(String::from("warn")),
321 use_stable: false,
322 }
323 }
324}
325
326impl Builder {
327 pub fn stable() -> Self {
338 Self {
339 use_stable: true,
340 ..Self::default()
341 }
342 }
343}
344
345impl Builder {
346 #[must_use]
359 pub fn toolchain(mut self, toolchain: impl Into<String>) -> Self {
360 self.toolchain = Some(toolchain.into());
361 self
362 }
363
364 #[must_use]
366 pub fn clear_toolchain(mut self) -> Self {
367 self.toolchain = None;
368 self
369 }
370
371 #[must_use]
373 pub fn manifest_path(mut self, manifest_path: impl AsRef<Path>) -> Self {
374 manifest_path.as_ref().clone_into(&mut self.manifest_path);
375 self
376 }
377
378 #[must_use]
382 pub fn target_dir(mut self, target_dir: impl AsRef<Path>) -> Self {
383 self.target_dir = Some(target_dir.as_ref().to_owned());
384 self
385 }
386
387 #[must_use]
389 pub fn clear_target_dir(mut self) -> Self {
390 self.target_dir = None;
391 self
392 }
393
394 #[must_use]
396 pub const fn quiet(mut self, quiet: bool) -> Self {
397 self.quiet = quiet;
398 self
399 }
400
401 #[must_use]
403 pub const fn silent(mut self, silent: bool) -> Self {
404 self.silent = silent;
405 self
406 }
407
408 #[must_use]
410 pub const fn color(mut self, color: Color) -> Self {
411 self.color = color;
412 self
413 }
414
415 #[must_use]
417 pub fn target(mut self, target: String) -> Self {
418 self.target = Some(target);
419 self
420 }
421
422 #[must_use]
424 pub const fn no_default_features(
425 mut self,
426 no_default_features: bool,
427 ) -> Self {
428 self.no_default_features = no_default_features;
429 self
430 }
431
432 #[must_use]
434 pub const fn all_features(mut self, all_features: bool) -> Self {
435 self.all_features = all_features;
436 self
437 }
438
439 #[must_use]
441 pub fn features<I: IntoIterator<Item = S>, S: AsRef<str>>(
442 mut self,
443 features: I,
444 ) -> Self {
445 self.features = features
446 .into_iter()
447 .map(|item| item.as_ref().to_owned())
448 .collect();
449 self
450 }
451
452 #[must_use]
454 pub fn package(mut self, package: impl AsRef<str>) -> Self {
455 self.package = Some(package.as_ref().to_owned());
456 self
457 }
458
459 #[must_use]
461 pub fn package_target(mut self, package_target: PackageTarget) -> Self {
462 self.package_target = package_target;
463 self
464 }
465
466 #[must_use]
468 pub fn document_private_items(
469 mut self,
470 document_private_items: bool,
471 ) -> Self {
472 self.document_private_items = document_private_items;
473 self
474 }
475
476 #[must_use]
478 pub fn cap_lints(mut self, cap_lints: Option<impl AsRef<str>>) -> Self {
479 self.cap_lints = cap_lints.map(|c| c.as_ref().to_owned());
480 self
481 }
482
483 pub fn build(self) -> Result<PathBuf, BuildError> {
497 run_cargo_rustdoc::<std::io::Sink, std::io::Sink>(self, None)
498 }
499
500 pub fn build_with_captured_output(
531 self,
532 stdout: impl Write,
533 stderr: impl Write,
534 ) -> Result<PathBuf, BuildError> {
535 let capture_output = CaptureOutput { stdout, stderr };
536 run_cargo_rustdoc(self, Some(capture_output))
537 }
538}
539
540#[derive(Default, Debug, Clone)]
542#[non_exhaustive]
543pub enum PackageTarget {
544 #[default]
546 Lib,
547 Bin(String),
549 Example(String),
551 Test(String),
553 Bench(String),
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn ensure_toolchain_not_overridden() {
563 if option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK").is_none() {
567 assert!(OVERRIDDEN_TOOLCHAIN.is_none());
568 }
569 }
570}