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.contains(&TargetKind::Lib) {
250 return Ok(target.name.to_owned());
251 }
252 }
253
254 Ok(package.name.into_inner())
255}
256
257#[derive(Clone, Copy, Debug)]
259pub enum Color {
260 Always,
262 Never,
264 Auto,
266}
267
268#[derive(Clone, Debug)]
271pub struct Builder {
272 toolchain: Option<String>,
273 manifest_path: PathBuf,
274 target_dir: Option<PathBuf>,
275 target: Option<String>,
276 quiet: bool,
277 silent: bool,
278 color: Color,
279 no_default_features: bool,
280 all_features: bool,
281 features: Vec<String>,
282 package: Option<String>,
283 package_target: PackageTarget,
284 document_private_items: bool,
285 cap_lints: Option<String>,
286 envs: HashMap<OsString, OsString>,
287}
288
289impl Default for Builder {
290 fn default() -> Self {
291 Self {
292 toolchain: None,
293 manifest_path: PathBuf::from("Cargo.toml"),
294 target_dir: None,
295 target: None,
296 quiet: false,
297 silent: false,
298 color: Color::Auto,
299 no_default_features: false,
300 all_features: false,
301 features: vec![],
302 package: None,
303 package_target: PackageTarget::default(),
304 document_private_items: false,
305 cap_lints: Some(String::from("warn")),
306 envs: HashMap::new(),
307 }
308 }
309}
310
311impl Builder {
312 #[must_use]
325 pub fn toolchain(mut self, toolchain: impl Into<String>) -> Self {
326 self.toolchain = Some(toolchain.into());
327 self
328 }
329
330 #[must_use]
332 pub fn clear_toolchain(mut self) -> Self {
333 self.toolchain = None;
334 self
335 }
336
337 #[must_use]
339 pub fn manifest_path(mut self, manifest_path: impl AsRef<Path>) -> Self {
340 manifest_path.as_ref().clone_into(&mut self.manifest_path);
341 self
342 }
343
344 #[must_use]
348 pub fn target_dir(mut self, target_dir: impl AsRef<Path>) -> Self {
349 self.target_dir = Some(target_dir.as_ref().to_owned());
350 self
351 }
352
353 #[must_use]
355 pub fn clear_target_dir(mut self) -> Self {
356 self.target_dir = None;
357 self
358 }
359
360 #[must_use]
362 pub const fn quiet(mut self, quiet: bool) -> Self {
363 self.quiet = quiet;
364 self
365 }
366
367 #[must_use]
369 pub const fn silent(mut self, silent: bool) -> Self {
370 self.silent = silent;
371 self
372 }
373
374 #[must_use]
376 pub const fn color(mut self, color: Color) -> Self {
377 self.color = color;
378 self
379 }
380
381 #[must_use]
383 pub fn target(mut self, target: String) -> Self {
384 self.target = Some(target);
385 self
386 }
387
388 #[must_use]
390 pub const fn no_default_features(mut self, no_default_features: bool) -> Self {
391 self.no_default_features = no_default_features;
392 self
393 }
394
395 #[must_use]
397 pub const fn all_features(mut self, all_features: bool) -> Self {
398 self.all_features = all_features;
399 self
400 }
401
402 #[must_use]
404 pub fn features<I: IntoIterator<Item = S>, S: AsRef<str>>(mut self, features: I) -> Self {
405 self.features = features
406 .into_iter()
407 .map(|item| item.as_ref().to_owned())
408 .collect();
409 self
410 }
411
412 #[must_use]
414 pub fn package(mut self, package: impl AsRef<str>) -> Self {
415 self.package = Some(package.as_ref().to_owned());
416 self
417 }
418
419 #[must_use]
421 pub fn package_target(mut self, package_target: PackageTarget) -> Self {
422 self.package_target = package_target;
423 self
424 }
425
426 #[must_use]
428 pub fn document_private_items(mut self, document_private_items: bool) -> Self {
429 self.document_private_items = document_private_items;
430 self
431 }
432
433 #[must_use]
435 pub fn cap_lints(mut self, cap_lints: Option<impl AsRef<str>>) -> Self {
436 self.cap_lints = cap_lints.map(|c| c.as_ref().to_owned());
437 self
438 }
439
440 #[must_use]
448 pub fn env(mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> Self {
449 self.envs
450 .insert(key.as_ref().to_owned(), val.as_ref().to_owned());
451 self
452 }
453
454 pub fn build(self) -> Result<PathBuf, BuildError> {
468 run_cargo_rustdoc::<std::io::Sink, std::io::Sink>(self, None)
469 }
470
471 pub fn build_with_captured_output(
502 self,
503 stdout: impl Write,
504 stderr: impl Write,
505 ) -> Result<PathBuf, BuildError> {
506 let capture_output = CaptureOutput { stdout, stderr };
507 run_cargo_rustdoc(self, Some(capture_output))
508 }
509}
510
511#[derive(Default, Debug, Clone)]
513#[non_exhaustive]
514pub enum PackageTarget {
515 #[default]
517 Lib,
518 Bin(String),
520 Example(String),
522 Test(String),
524 Bench(String),
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
533 fn ensure_toolchain_not_overridden() {
534 if option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK").is_none() {
538 assert!(OVERRIDDEN_TOOLCHAIN.is_none());
539 }
540 }
541}