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 use_stable,
93 } = options;
94
95 let mut command = match OVERRIDDEN_TOOLCHAIN.or(requested_toolchain.as_deref()) {
96 None => Command::new("cargo"),
97 Some(toolchain) => {
98 if !rustup_installed() {
99 return Err(BuildError::General(String::from(
100 "required program rustup not found in PATH. Is it installed?",
101 )));
102 }
103 let mut cmd = Command::new("rustup");
104 cmd.args(["run", toolchain, "cargo"]);
105
106 if *use_stable {
107 let env_vars = vec![("RUSTC_BOOTSTRAP", "1".to_string())];
108 cmd.envs(env_vars);
109 }
110
111 cmd
112 }
113 };
114
115 command.arg("rustdoc");
116 match package_target {
117 PackageTarget::Lib => command.arg("--lib"),
118 PackageTarget::Bin(target) => command.args(["--bin", target]),
119 PackageTarget::Example(target) => command.args(["--example", target]),
120 PackageTarget::Test(target) => command.args(["--test", target]),
121 PackageTarget::Bench(target) => command.args(["--bench", target]),
122 };
123 if let Some(target_dir) = target_dir {
124 command.arg("--target-dir");
125 command.arg(target_dir);
126 }
127 if *quiet {
128 command.arg("--quiet");
129 }
130 if *silent {
131 command.stdout(std::process::Stdio::null());
132 command.stderr(std::process::Stdio::null());
133 }
134 match *color {
135 Color::Always => command.arg("--color").arg("always"),
136 Color::Never => command.arg("--color").arg("never"),
137 Color::Auto => command.arg("--color").arg("auto"),
138 };
139 command.arg("--manifest-path");
140 command.arg(manifest_path);
141 if let Some(target) = target {
142 command.arg("--target");
143 command.arg(target);
144 }
145 if *no_default_features {
146 command.arg("--no-default-features");
147 }
148 if *all_features {
149 command.arg("--all-features");
150 }
151 for feature in features {
152 command.args(["--features", feature]);
153 }
154 if let Some(package) = package {
155 command.args(["--package", package]);
156 }
157 command.arg("--");
158 command.args(["-Z", "unstable-options"]);
159 command.args(["--output-format", "json"]);
160 if *document_private_items {
161 command.arg("--document-private-items");
162 }
163 if let Some(cap_lints) = cap_lints {
164 command.args(["--cap-lints", cap_lints]);
165 }
166 Ok(command)
167}
168
169#[instrument(ret(level = Level::DEBUG))]
172fn rustdoc_json_path_for_manifest_path(
173 manifest_path: &Path,
174 package: Option<&str>,
175 package_target: &PackageTarget,
176 target_dir: Option<&Path>,
177 target: Option<&str>,
178) -> Result<PathBuf, BuildError> {
179 let target_dir = match target_dir {
180 Some(target_dir) => target_dir.to_owned(),
181 None => target_directory(manifest_path)?,
182 };
183
184 let package_target_name = match package_target {
186 PackageTarget::Lib => library_name(manifest_path, package)?,
187 PackageTarget::Bin(name)
188 | PackageTarget::Example(name)
189 | PackageTarget::Test(name)
190 | PackageTarget::Bench(name) => name.clone(),
191 }
192 .replace('-', "_");
193
194 let mut rustdoc_json_path = target_dir;
195 if let Some(target) = target {
197 rustdoc_json_path.push(target);
198 }
199 rustdoc_json_path.push("doc");
200 rustdoc_json_path.push(package_target_name);
201 rustdoc_json_path.set_extension("json");
202 Ok(rustdoc_json_path)
203}
204
205pub fn rustup_installed() -> bool {
207 let mut check_rustup = std::process::Command::new("rustup");
208 check_rustup.arg("--version");
209 check_rustup.stdout(std::process::Stdio::null());
210 check_rustup.stderr(std::process::Stdio::null());
211 check_rustup.status().map(|s| s.success()).unwrap_or(false)
212}
213
214fn target_directory(manifest_path: impl AsRef<Path>) -> Result<PathBuf, BuildError> {
217 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
218 metadata_cmd.manifest_path(manifest_path.as_ref());
219 let metadata = metadata_cmd.exec()?;
220 Ok(metadata.target_directory.as_std_path().to_owned())
221}
222
223fn library_name(
226 manifest_path: impl AsRef<Path>,
227 package_name: Option<&str>,
228) -> Result<String, BuildError> {
229 let package_name = if let Some(package_name) = package_name {
230 package_name.to_owned()
231 } else {
232 let manifest = cargo_manifest::Manifest::from_path(manifest_path.as_ref())?;
234 manifest
235 .package
236 .ok_or_else(|| BuildError::VirtualManifest(manifest_path.as_ref().to_owned()))?
237 .name
238 .to_owned()
239 };
240
241 let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
242 metadata_cmd.manifest_path(manifest_path.as_ref());
243 let metadata = metadata_cmd.exec()?;
244
245 let package = metadata
246 .packages
247 .into_iter()
248 .find(|p| p.name.as_str() == package_name)
249 .ok_or_else(|| BuildError::VirtualManifest(manifest_path.as_ref().to_owned()))?;
250
251 for target in &package.targets {
252 if target.kind.contains(&TargetKind::Lib) {
253 return Ok(target.name.to_owned());
254 }
255 }
256
257 Ok(package.name.into_inner())
258}
259
260#[derive(Clone, Copy, Debug)]
262pub enum Color {
263 Always,
265 Never,
267 Auto,
269}
270
271#[derive(Clone, Debug)]
274pub struct Builder {
275 toolchain: Option<String>,
276 manifest_path: PathBuf,
277 target_dir: Option<PathBuf>,
278 target: Option<String>,
279 quiet: bool,
280 silent: bool,
281 color: Color,
282 no_default_features: bool,
283 all_features: bool,
284 features: Vec<String>,
285 package: Option<String>,
286 package_target: PackageTarget,
287 document_private_items: bool,
288 cap_lints: Option<String>,
289 use_stable: bool,
290}
291
292impl Default for Builder {
293 fn default() -> Self {
294 Self {
295 toolchain: None,
296 manifest_path: PathBuf::from("Cargo.toml"),
297 target_dir: None,
298 target: None,
299 quiet: false,
300 silent: false,
301 color: Color::Auto,
302 no_default_features: false,
303 all_features: false,
304 features: vec![],
305 package: None,
306 package_target: PackageTarget::default(),
307 document_private_items: false,
308 cap_lints: Some(String::from("warn")),
309 use_stable: false,
310 }
311 }
312}
313
314impl Builder {
315 pub fn stable() -> Self {
326 Self {
327 use_stable: true,
328 ..Self::default()
329 }
330 }
331}
332
333impl Builder {
334 #[must_use]
347 pub fn toolchain(mut self, toolchain: impl Into<String>) -> Self {
348 self.toolchain = Some(toolchain.into());
349 self
350 }
351
352 #[must_use]
354 pub fn clear_toolchain(mut self) -> Self {
355 self.toolchain = None;
356 self
357 }
358
359 #[must_use]
361 pub fn manifest_path(mut self, manifest_path: impl AsRef<Path>) -> Self {
362 manifest_path.as_ref().clone_into(&mut self.manifest_path);
363 self
364 }
365
366 #[must_use]
370 pub fn target_dir(mut self, target_dir: impl AsRef<Path>) -> Self {
371 self.target_dir = Some(target_dir.as_ref().to_owned());
372 self
373 }
374
375 #[must_use]
377 pub fn clear_target_dir(mut self) -> Self {
378 self.target_dir = None;
379 self
380 }
381
382 #[must_use]
384 pub const fn quiet(mut self, quiet: bool) -> Self {
385 self.quiet = quiet;
386 self
387 }
388
389 #[must_use]
391 pub const fn silent(mut self, silent: bool) -> Self {
392 self.silent = silent;
393 self
394 }
395
396 #[must_use]
398 pub const fn color(mut self, color: Color) -> Self {
399 self.color = color;
400 self
401 }
402
403 #[must_use]
405 pub fn target(mut self, target: String) -> Self {
406 self.target = Some(target);
407 self
408 }
409
410 #[must_use]
412 pub const fn no_default_features(mut self, no_default_features: bool) -> Self {
413 self.no_default_features = no_default_features;
414 self
415 }
416
417 #[must_use]
419 pub const fn all_features(mut self, all_features: bool) -> Self {
420 self.all_features = all_features;
421 self
422 }
423
424 #[must_use]
426 pub fn features<I: IntoIterator<Item = S>, S: AsRef<str>>(mut self, features: I) -> Self {
427 self.features = features
428 .into_iter()
429 .map(|item| item.as_ref().to_owned())
430 .collect();
431 self
432 }
433
434 #[must_use]
436 pub fn package(mut self, package: impl AsRef<str>) -> Self {
437 self.package = Some(package.as_ref().to_owned());
438 self
439 }
440
441 #[must_use]
443 pub fn package_target(mut self, package_target: PackageTarget) -> Self {
444 self.package_target = package_target;
445 self
446 }
447
448 #[must_use]
450 pub fn document_private_items(mut self, document_private_items: bool) -> Self {
451 self.document_private_items = document_private_items;
452 self
453 }
454
455 #[must_use]
457 pub fn cap_lints(mut self, cap_lints: Option<impl AsRef<str>>) -> Self {
458 self.cap_lints = cap_lints.map(|c| c.as_ref().to_owned());
459 self
460 }
461
462 pub fn build(self) -> Result<PathBuf, BuildError> {
476 run_cargo_rustdoc::<std::io::Sink, std::io::Sink>(self, None)
477 }
478
479 pub fn build_with_captured_output(
510 self,
511 stdout: impl Write,
512 stderr: impl Write,
513 ) -> Result<PathBuf, BuildError> {
514 let capture_output = CaptureOutput { stdout, stderr };
515 run_cargo_rustdoc(self, Some(capture_output))
516 }
517}
518
519#[derive(Default, Debug, Clone)]
521#[non_exhaustive]
522pub enum PackageTarget {
523 #[default]
525 Lib,
526 Bin(String),
528 Example(String),
530 Test(String),
532 Bench(String),
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn ensure_toolchain_not_overridden() {
542 if option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK").is_none() {
546 assert!(OVERRIDDEN_TOOLCHAIN.is_none());
547 }
548 }
549}