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