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> = option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK"); struct CaptureOutput<O, E> {
17 stdout: O,
18 stderr: E,
19}
20
21fn run_cargo_rustdoc<O, E>(
22 options: Builder,
23 capture_output: Option<CaptureOutput<O, E>>,
24) -> Result<PathBuf, BuildError>
25where
26 O: Write,
27 E: Write,
28{
29 let mut cmd = cargo_rustdoc_command(&options)?;
30 info!("Running {cmd:?}");
31
32 let status = match capture_output {
33 Some(CaptureOutput {
34 mut stdout,
35 mut stderr,
36 }) => {
37 let output = cmd.output().map_err(|e| {
38 BuildError::CommandExecutionError(format!("Failed to run `{cmd:?}`: {e}"))
39 })?;
40 stdout.write_all(&output.stdout).map_err(|e| {
41 BuildError::CapturedOutputError(format!("Failed to write stdout: {e}"))
42 })?;
43 stderr.write_all(&output.stderr).map_err(|e| {
44 BuildError::CapturedOutputError(format!("Failed to write stderr: {e}"))
45 })?;
46 output.status
47 }
48 None => cmd.status().map_err(|e| {
49 BuildError::CommandExecutionError(format!("Failed to run `{cmd:?}`: {e}"))
50 })?,
51 };
52
53 if status.success() {
54 rustdoc_json_path_for_manifest_path(
55 &options.manifest_path,
56 options.package.as_deref(),
57 &options.package_target,
58 options.target_dir.as_deref(),
59 options.target.as_deref(),
60 )
61 } else {
62 let manifest = cargo_manifest::Manifest::from_path(&options.manifest_path)?;
63 if manifest.package.is_none() && manifest.workspace.is_some() {
64 Err(BuildError::VirtualManifest(options.manifest_path))
65 } else {
66 Err(BuildError::BuildRustdocJsonError)
67 }
68 }
69}
70
71fn cargo_rustdoc_command(options: &Builder) -> Result<Command, BuildError> {
77 let Builder {
78 toolchain: requested_toolchain,
79 manifest_path,
80 target_dir,
81 target,
82 quiet,
83 silent,
84 color,
85 no_default_features,
86 all_features,
87 features,
88 package,
89 package_target,
90 document_private_items,
91 cap_lints,
92 } = options;
93
94 let mut command = match OVERRIDDEN_TOOLCHAIN.or(requested_toolchain.as_deref()) {
95 None => Command::new("cargo"),
96 Some(toolchain) => {
97 if !rustup_installed() {
98 return Err(BuildError::General(String::from(
99 "required program rustup not found in PATH. Is it installed?",
100 )));
101 }
102 let mut cmd = Command::new("rustup");
103 cmd.args(["run", toolchain, "cargo"]);
104 cmd
105 }
106 };
107
108 command.arg("rustdoc");
109 match package_target {
110 PackageTarget::Lib => command.arg("--lib"),
111 PackageTarget::Bin(target) => command.args(["--bin", target]),
112 PackageTarget::Example(target) => command.args(["--example", target]),
113 PackageTarget::Test(target) => command.args(["--test", target]),
114 PackageTarget::Bench(target) => command.args(["--bench", target]),
115 };
116 if let Some(target_dir) = target_dir {
117 command.arg("--target-dir");
118 command.arg(target_dir);
119 }
120 if *quiet {
121 command.arg("--quiet");
122 }
123 if *silent {
124 command.stdout(std::process::Stdio::null());
125 command.stderr(std::process::Stdio::null());
126 }
127 match *color {
128 Color::Always => command.arg("--color").arg("always"),
129 Color::Never => command.arg("--color").arg("never"),
130 Color::Auto => command.arg("--color").arg("auto"),
131 };
132 command.arg("--manifest-path");
133 command.arg(manifest_path);
134 if let Some(target) = target {
135 command.arg("--target");
136 command.arg(target);
137 }
138 if *no_default_features {
139 command.arg("--no-default-features");
140 }
141 if *all_features {
142 command.arg("--all-features");
143 }
144 for feature in features {
145 command.args(["--features", feature]);
146 }
147 if let Some(package) = package {
148 command.args(["--package", package]);
149 }
150 command.arg("--");
151 command.args(["-Z", "unstable-options"]);
152 command.args(["--output-format", "json"]);
153 if *document_private_items {
154 command.arg("--document-private-items");
155 }
156 if let Some(cap_lints) = cap_lints {
157 command.args(["--cap-lints", cap_lints]);
158 }
159 Ok(command)
160}
161
162#[instrument(ret(level = Level::DEBUG))]
165fn rustdoc_json_path_for_manifest_path(
166 manifest_path: &Path,
167 package: Option<&str>,
168 package_target: &PackageTarget,
169 target_dir: Option<&Path>,
170 target: Option<&str>,
171) -> Result<PathBuf, BuildError> {
172 let target_dir = match target_dir {
173 Some(target_dir) => target_dir.to_owned(),
174 None => target_directory(manifest_path)?,
175 };
176
177 let package_target_name = match package_target {
179 PackageTarget::Lib => library_name(manifest_path, package)?,
180 PackageTarget::Bin(name)
181 | PackageTarget::Example(name)
182 | PackageTarget::Test(name)
183 | PackageTarget::Bench(name) => name.clone(),
184 }
185 .replace('-', "_");
186
187 let mut rustdoc_json_path = target_dir;
188 if let Some(target) = target {
190 rustdoc_json_path.push(target);
191 }
192 rustdoc_json_path.push("doc");
193 rustdoc_json_path.push(package_target_name);
194 rustdoc_json_path.set_extension("json");
195 Ok(rustdoc_json_path)
196}
197
198pub fn rustup_installed() -> bool {
200 let mut check_rustup = std::process::Command::new("rustup");
201 check_rustup.arg("--version");
202 check_rustup.stdout(std::process::Stdio::null());
203 check_rustup.stderr(std::process::Stdio::null());
204 check_rustup.status().map(|s| s.success()).unwrap_or(false)
205}
206
207fn target_directory(manifest_path: impl AsRef<Path>) -> Result<PathBuf, BuildError> {
210 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
211 metadata_cmd.manifest_path(manifest_path.as_ref());
212 let metadata = metadata_cmd.exec()?;
213 Ok(metadata.target_directory.as_std_path().to_owned())
214}
215
216fn library_name(
219 manifest_path: impl AsRef<Path>,
220 package_name: Option<&str>,
221) -> Result<String, BuildError> {
222 let package_name = if let Some(package_name) = package_name {
223 package_name.to_owned()
224 } else {
225 let manifest = cargo_manifest::Manifest::from_path(manifest_path.as_ref())?;
227 manifest
228 .package
229 .ok_or_else(|| BuildError::VirtualManifest(manifest_path.as_ref().to_owned()))?
230 .name
231 .to_owned()
232 };
233
234 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
235 metadata_cmd.manifest_path(manifest_path.as_ref());
236 let metadata = metadata_cmd.exec()?;
237
238 let package = metadata
239 .packages
240 .iter()
241 .find(|p| p.name == package_name)
242 .ok_or_else(|| BuildError::VirtualManifest(manifest_path.as_ref().to_owned()))?;
243
244 for target in &package.targets {
245 if target.kind.contains(&TargetKind::Lib) {
246 return Ok(target.name.to_owned());
247 }
248 }
249
250 Ok(package.name.clone())
251}
252
253#[derive(Clone, Copy, Debug)]
255pub enum Color {
256 Always,
258 Never,
260 Auto,
262}
263
264#[derive(Clone, Debug)]
267pub struct Builder {
268 toolchain: Option<String>,
269 manifest_path: PathBuf,
270 target_dir: Option<PathBuf>,
271 target: Option<String>,
272 quiet: bool,
273 silent: bool,
274 color: Color,
275 no_default_features: bool,
276 all_features: bool,
277 features: Vec<String>,
278 package: Option<String>,
279 package_target: PackageTarget,
280 document_private_items: bool,
281 cap_lints: Option<String>,
282}
283
284impl Default for Builder {
285 fn default() -> Self {
286 Self {
287 toolchain: None,
288 manifest_path: PathBuf::from("Cargo.toml"),
289 target_dir: None,
290 target: None,
291 quiet: false,
292 silent: false,
293 color: Color::Auto,
294 no_default_features: false,
295 all_features: false,
296 features: vec![],
297 package: None,
298 package_target: PackageTarget::default(),
299 document_private_items: false,
300 cap_lints: Some(String::from("warn")),
301 }
302 }
303}
304
305impl Builder {
306 #[must_use]
319 pub fn toolchain(mut self, toolchain: impl Into<String>) -> Self {
320 self.toolchain = Some(toolchain.into());
321 self
322 }
323
324 #[must_use]
326 pub fn clear_toolchain(mut self) -> Self {
327 self.toolchain = None;
328 self
329 }
330
331 #[must_use]
333 pub fn manifest_path(mut self, manifest_path: impl AsRef<Path>) -> Self {
334 manifest_path.as_ref().clone_into(&mut self.manifest_path);
335 self
336 }
337
338 #[must_use]
342 pub fn target_dir(mut self, target_dir: impl AsRef<Path>) -> Self {
343 self.target_dir = Some(target_dir.as_ref().to_owned());
344 self
345 }
346
347 #[must_use]
349 pub fn clear_target_dir(mut self) -> Self {
350 self.target_dir = None;
351 self
352 }
353
354 #[must_use]
356 pub const fn quiet(mut self, quiet: bool) -> Self {
357 self.quiet = quiet;
358 self
359 }
360
361 #[must_use]
363 pub const fn silent(mut self, silent: bool) -> Self {
364 self.silent = silent;
365 self
366 }
367
368 #[must_use]
370 pub const fn color(mut self, color: Color) -> Self {
371 self.color = color;
372 self
373 }
374
375 #[must_use]
377 pub fn target(mut self, target: String) -> Self {
378 self.target = Some(target);
379 self
380 }
381
382 #[must_use]
384 pub const fn no_default_features(mut self, no_default_features: bool) -> Self {
385 self.no_default_features = no_default_features;
386 self
387 }
388
389 #[must_use]
391 pub const fn all_features(mut self, all_features: bool) -> Self {
392 self.all_features = all_features;
393 self
394 }
395
396 #[must_use]
398 pub fn features<I: IntoIterator<Item = S>, S: AsRef<str>>(mut self, features: I) -> Self {
399 self.features = features
400 .into_iter()
401 .map(|item| item.as_ref().to_owned())
402 .collect();
403 self
404 }
405
406 #[must_use]
408 pub fn package(mut self, package: impl AsRef<str>) -> Self {
409 self.package = Some(package.as_ref().to_owned());
410 self
411 }
412
413 #[must_use]
415 pub fn package_target(mut self, package_target: PackageTarget) -> Self {
416 self.package_target = package_target;
417 self
418 }
419
420 #[must_use]
422 pub fn document_private_items(mut self, document_private_items: bool) -> Self {
423 self.document_private_items = document_private_items;
424 self
425 }
426
427 #[must_use]
429 pub fn cap_lints(mut self, cap_lints: Option<impl AsRef<str>>) -> Self {
430 self.cap_lints = cap_lints.map(|c| c.as_ref().to_owned());
431 self
432 }
433
434 pub fn build(self) -> Result<PathBuf, BuildError> {
448 run_cargo_rustdoc::<std::io::Sink, std::io::Sink>(self, None)
449 }
450
451 pub fn build_with_captured_output(
482 self,
483 stdout: impl Write,
484 stderr: impl Write,
485 ) -> Result<PathBuf, BuildError> {
486 let capture_output = CaptureOutput { stdout, stderr };
487 run_cargo_rustdoc(self, Some(capture_output))
488 }
489}
490
491#[derive(Default, Debug, Clone)]
493#[non_exhaustive]
494pub enum PackageTarget {
495 #[default]
497 Lib,
498 Bin(String),
500 Example(String),
502 Test(String),
504 Bench(String),
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn ensure_toolchain_not_overridden() {
514 if option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK").is_none() {
518 assert!(OVERRIDDEN_TOOLCHAIN.is_none());
519 }
520 }
521}