rustdoc_json_stable/
builder.rs

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
11/// For development purposes only. Sometimes when you work on this project you
12/// want to quickly use a different toolchain to build rustdoc JSON. You can
13/// specify what toolchain, by temporarily changing this.
14const OVERRIDDEN_TOOLCHAIN: Option<&str> =
15  option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK"); // Some("nightly-2022-07-16");
16
17struct 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  
31  let mut cmd = cargo_rustdoc_command(&options)?;
32
33  info!("Running {cmd:?}");
34
35  let status = match capture_output {
36    Some(CaptureOutput {
37      mut stdout,
38      mut stderr,
39    }) => {
40      let output = cmd.output().map_err(|e| {
41        BuildError::CommandExecutionError(format!(
42          "Failed to run `{cmd:?}`: {e}"
43        ))
44      })?;
45      stdout.write_all(&output.stdout).map_err(|e| {
46        BuildError::CapturedOutputError(format!("Failed to write stdout: {e}"))
47      })?;
48      stderr.write_all(&output.stderr).map_err(|e| {
49        BuildError::CapturedOutputError(format!("Failed to write stderr: {e}"))
50      })?;
51      output.status
52    }
53    None => cmd.status().map_err(|e| {
54      BuildError::CommandExecutionError(format!("Failed to run `{cmd:?}`: {e}"))
55    })?,
56  };
57
58  if status.success() {
59    rustdoc_json_path_for_manifest_path(
60      &options.manifest_path,
61      options.package.as_deref(),
62      &options.package_target,
63      options.target_dir.as_deref(),
64      options.target.as_deref(),
65    )
66  } else {
67    let manifest = cargo_manifest::Manifest::from_path(&options.manifest_path)?;
68    if manifest.package.is_none() && manifest.workspace.is_some() {
69      Err(BuildError::VirtualManifest(options.manifest_path))
70    } else {
71      Err(BuildError::BuildRustdocJsonError)
72    }
73  }
74}
75
76/// Construct the `cargo rustdoc` command to use for building rustdoc JSON. The
77/// command typically ends up looks something like this:
78/// ```bash
79/// cargo +nightly rustdoc --lib --manifest-path Cargo.toml -- -Z unstable-options --output-format json --cap-lints warn
80/// ```
81fn cargo_rustdoc_command(options: &Builder) -> Result<Command, BuildError> {
82  let Builder {
83    toolchain: requested_toolchain,
84    manifest_path,
85    target_dir,
86    target,
87    quiet,
88    silent,
89    color,
90    no_default_features,
91    all_features,
92    features,
93    package,
94    package_target,
95    document_private_items,
96    cap_lints,
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        let env_vars = vec![
112          ("RUSTC_BOOTSTRAP", "1".to_string()),
113        ];
114        cmd.envs(env_vars);
115        cmd
116      }
117    };
118
119  command.arg("rustdoc");
120  match package_target {
121    PackageTarget::Lib => command.arg("--lib"),
122    PackageTarget::Bin(target) => command.args(["--bin", target]),
123    PackageTarget::Example(target) => command.args(["--example", target]),
124    PackageTarget::Test(target) => command.args(["--test", target]),
125    PackageTarget::Bench(target) => command.args(["--bench", target]),
126  };
127  if let Some(target_dir) = target_dir {
128    command.arg("--target-dir");
129    command.arg(target_dir);
130  }
131  if *quiet {
132    command.arg("--quiet");
133  }
134  if *silent {
135    command.stdout(std::process::Stdio::null());
136    command.stderr(std::process::Stdio::null());
137  }
138  match *color {
139    Color::Always => command.arg("--color").arg("always"),
140    Color::Never => command.arg("--color").arg("never"),
141    Color::Auto => command.arg("--color").arg("auto"),
142  };
143  command.arg("--manifest-path");
144  command.arg(manifest_path);
145  if let Some(target) = target {
146    command.arg("--target");
147    command.arg(target);
148  }
149  if *no_default_features {
150    command.arg("--no-default-features");
151  }
152  if *all_features {
153    command.arg("--all-features");
154  }
155  for feature in features {
156    command.args(["--features", feature]);
157  }
158  if let Some(package) = package {
159    command.args(["--package", package]);
160  }
161  command.arg("--");
162  command.args(["-Z", "unstable-options"]);
163  command.args(["--output-format", "json"]);
164  if *document_private_items {
165    command.arg("--document-private-items");
166  }
167  if let Some(cap_lints) = cap_lints {
168    command.args(["--cap-lints", cap_lints]);
169  }
170  println!("QAQ {command:?}");
171  Ok(command)
172}
173
174/// Returns `./target/doc/crate_name.json`. Also takes care of transforming
175/// `crate-name` to `crate_name`. Also handles `[lib] name = "foo"`.
176#[instrument(ret(level = Level::DEBUG))]
177fn rustdoc_json_path_for_manifest_path(
178  manifest_path: &Path,
179  package: Option<&str>,
180  package_target: &PackageTarget,
181  target_dir: Option<&Path>,
182  target: Option<&str>,
183) -> Result<PathBuf, BuildError> {
184  let target_dir = match target_dir {
185    Some(target_dir) => target_dir.to_owned(),
186    None => target_directory(manifest_path)?,
187  };
188
189  // get the name of the crate/binary/example/test/bench
190  let package_target_name = match package_target {
191    PackageTarget::Lib => library_name(manifest_path, package)?,
192    PackageTarget::Bin(name)
193    | PackageTarget::Example(name)
194    | PackageTarget::Test(name)
195    | PackageTarget::Bench(name) => name.clone(),
196  }
197  .replace('-', "_");
198
199  let mut rustdoc_json_path = target_dir;
200  // if one has specified a target explicitly then Cargo appends that target triple name as a subfolder
201  if let Some(target) = target {
202    rustdoc_json_path.push(target);
203  }
204  rustdoc_json_path.push("doc");
205  rustdoc_json_path.push(package_target_name);
206  rustdoc_json_path.set_extension("json");
207  Ok(rustdoc_json_path)
208}
209
210/// Checks if the `rustup` program can be found in `PATH`.
211pub fn rustup_installed() -> bool {
212  let mut check_rustup = std::process::Command::new("rustup");
213  check_rustup.arg("--version");
214  check_rustup.stdout(std::process::Stdio::null());
215  check_rustup.stderr(std::process::Stdio::null());
216  check_rustup.status().map(|s| s.success()).unwrap_or(false)
217}
218
219/// Typically returns the absolute path to the regular cargo `./target`
220/// directory. But also handles packages part of workspaces.
221fn target_directory(
222  manifest_path: impl AsRef<Path>,
223) -> Result<PathBuf, BuildError> {
224  let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
225  metadata_cmd.manifest_path(manifest_path.as_ref());
226  let metadata = metadata_cmd.exec()?;
227  Ok(metadata.target_directory.as_std_path().to_owned())
228}
229
230/// Figures out the name of the library crate corresponding to the given
231/// `Cargo.toml` and `package_name` (in case Cargo.toml is a workspace root).
232fn library_name(
233  manifest_path: impl AsRef<Path>,
234  package_name: Option<&str>,
235) -> Result<String, BuildError> {
236  let package_name = if let Some(package_name) = package_name {
237    package_name.to_owned()
238  } else {
239    // We must figure out the package name ourselves from the manifest.
240    let manifest = cargo_manifest::Manifest::from_path(manifest_path.as_ref())?;
241    manifest
242      .package
243      .ok_or_else(|| {
244        BuildError::VirtualManifest(manifest_path.as_ref().to_owned())
245      })?
246      .name
247      .to_owned()
248  };
249
250  let mut metadata_cmd = cargo_metadata::MetadataCommand::new();
251  metadata_cmd.manifest_path(manifest_path.as_ref());
252  let metadata = metadata_cmd.exec()?;
253
254  let package = metadata
255    .packages
256    .iter()
257    .find(|p| p.name == package_name)
258    .ok_or_else(|| {
259      BuildError::VirtualManifest(manifest_path.as_ref().to_owned())
260    })?;
261
262  for target in &package.targets {
263    if target.kind.contains(&TargetKind::Lib) {
264      return Ok(target.name.to_owned());
265    }
266  }
267
268  Ok(package.name.clone())
269}
270
271/// Color configuration for the output of `cargo rustdoc`.
272#[derive(Clone, Copy, Debug)]
273pub enum Color {
274  /// Always output colors.
275  Always,
276  /// Never output colors.
277  Never,
278  /// Cargo will decide whether to output colors based on the tty type.
279  Auto,
280}
281
282/// Builds rustdoc JSON. There are many build options. Refer to the docs to
283/// learn about them all. See [top-level docs](crate) for an example on how to use this builder.
284#[derive(Clone, Debug)]
285pub struct Builder {
286  toolchain: Option<String>,
287  manifest_path: PathBuf,
288  target_dir: Option<PathBuf>,
289  target: Option<String>,
290  quiet: bool,
291  silent: bool,
292  color: Color,
293  no_default_features: bool,
294  all_features: bool,
295  features: Vec<String>,
296  package: Option<String>,
297  package_target: PackageTarget,
298  document_private_items: bool,
299  cap_lints: Option<String>,
300}
301
302impl Default for Builder {
303  fn default() -> Self {
304    Self {
305      toolchain: None,
306      manifest_path: PathBuf::from("Cargo.toml"),
307      target_dir: None,
308      target: None,
309      quiet: false,
310      silent: false,
311      color: Color::Auto,
312      no_default_features: false,
313      all_features: false,
314      features: vec![],
315      package: None,
316      package_target: PackageTarget::default(),
317      document_private_items: false,
318      cap_lints: Some(String::from("warn")),
319    }
320  }
321}
322
323impl Builder {
324  /// Set the toolchain. Default: `None`.
325  /// Until rustdoc JSON has stabilized, you will want to set this to
326  /// be `"nightly"` or similar.
327  ///
328  /// If the toolchain is set as `None`, the current active toolchain will be used.
329  ///
330  /// # Notes
331  ///
332  /// The currently active toolchain is typically specified by the
333  /// `RUSTUP_TOOLCHAIN` environment variable, which the rustup proxy
334  /// mechanism sets. See <https://rust-lang.github.io/rustup/overrides.html>
335  /// for more info on how the active toolchain is determined.
336  #[must_use]
337  pub fn toolchain(mut self, toolchain: impl Into<String>) -> Self {
338    self.toolchain = Some(toolchain.into());
339    self
340  }
341
342  /// Clear a toolchain previously set with [`Self::toolchain`].
343  #[must_use]
344  pub fn clear_toolchain(mut self) -> Self {
345    self.toolchain = None;
346    self
347  }
348
349  /// Set the relative or absolute path to `Cargo.toml`. Default: `Cargo.toml`
350  #[must_use]
351  pub fn manifest_path(mut self, manifest_path: impl AsRef<Path>) -> Self {
352    manifest_path.as_ref().clone_into(&mut self.manifest_path);
353    self
354  }
355
356  /// Set what `--target-dir` to pass to `cargo`. Typically only needed if you
357  /// want to be able to build rustdoc JSON for the same crate concurrently,
358  /// for example to parallelize regression tests.
359  #[must_use]
360  pub fn target_dir(mut self, target_dir: impl AsRef<Path>) -> Self {
361    self.target_dir = Some(target_dir.as_ref().to_owned());
362    self
363  }
364
365  /// Clear a target dir previously set with [`Self::target_dir`].
366  #[must_use]
367  pub fn clear_target_dir(mut self) -> Self {
368    self.target_dir = None;
369    self
370  }
371
372  /// Whether or not to pass `--quiet` to `cargo rustdoc`. Default: `false`
373  #[must_use]
374  pub const fn quiet(mut self, quiet: bool) -> Self {
375    self.quiet = quiet;
376    self
377  }
378
379  /// Whether or not to redirect stdout and stderr to /dev/null. Default: `false`
380  #[must_use]
381  pub const fn silent(mut self, silent: bool) -> Self {
382    self.silent = silent;
383    self
384  }
385
386  /// Color configuration for the output of `cargo rustdoc`.
387  #[must_use]
388  pub const fn color(mut self, color: Color) -> Self {
389    self.color = color;
390    self
391  }
392
393  /// Whether or not to pass `--target` to `cargo rustdoc`. Default: `None`
394  #[must_use]
395  pub fn target(mut self, target: String) -> Self {
396    self.target = Some(target);
397    self
398  }
399
400  /// Whether to pass `--no-default-features` to `cargo rustdoc`. Default: `false`
401  #[must_use]
402  pub const fn no_default_features(
403    mut self,
404    no_default_features: bool,
405  ) -> Self {
406    self.no_default_features = no_default_features;
407    self
408  }
409
410  /// Whether to pass `--all-features` to `cargo rustdoc`. Default: `false`
411  #[must_use]
412  pub const fn all_features(mut self, all_features: bool) -> Self {
413    self.all_features = all_features;
414    self
415  }
416
417  /// Features to pass to `cargo rustdoc` via `--features`. Default to an empty vector
418  #[must_use]
419  pub fn features<I: IntoIterator<Item = S>, S: AsRef<str>>(
420    mut self,
421    features: I,
422  ) -> Self {
423    self.features = features
424      .into_iter()
425      .map(|item| item.as_ref().to_owned())
426      .collect();
427    self
428  }
429
430  /// Package to use for `cargo rustdoc` via `-p`. Default: `None`
431  #[must_use]
432  pub fn package(mut self, package: impl AsRef<str>) -> Self {
433    self.package = Some(package.as_ref().to_owned());
434    self
435  }
436
437  /// What part of the package to document. Default: `PackageTarget::Lib`
438  #[must_use]
439  pub fn package_target(mut self, package_target: PackageTarget) -> Self {
440    self.package_target = package_target;
441    self
442  }
443
444  /// Whether to pass `--document-private-items` to `cargo rustdoc`. Default: `false`
445  #[must_use]
446  pub fn document_private_items(
447    mut self,
448    document_private_items: bool,
449  ) -> Self {
450    self.document_private_items = document_private_items;
451    self
452  }
453
454  /// What to pass as `--cap-lints` to rustdoc JSON build command
455  #[must_use]
456  pub fn cap_lints(mut self, cap_lints: Option<impl AsRef<str>>) -> Self {
457    self.cap_lints = cap_lints.map(|c| c.as_ref().to_owned());
458    self
459  }
460
461  /// Generate rustdoc JSON for a crate. Returns the path to the freshly
462  /// built rustdoc JSON file.
463  ///
464  /// This method will print the stdout and stderr of the `cargo rustdoc` command to the stdout
465  /// and stderr of the calling process. If you want to capture the output, use
466  /// [`Builder::build_with_captured_output()`].
467  ///
468  /// See [top-level docs](crate) for an example on how to use it.
469  ///
470  /// # Errors
471  ///
472  /// E.g. if building the JSON fails or if the manifest path does not exist or is
473  /// invalid.
474  pub fn build(self) -> Result<PathBuf, BuildError> {
475    run_cargo_rustdoc::<std::io::Sink, std::io::Sink>(self, None)
476  }
477
478  /// Generate rustdoc JSON for a crate. This works like [`Builder::build()`], but will
479  /// capture the stdout and stderr of the `cargo rustdoc` command. The output will be written to
480  /// the `stdout` and `stderr` parameters. In particular, potential warnings and errors emitted
481  /// by `cargo rustdoc` will be captured to `stderr`. This can be useful if you want to present
482  /// these errors to the user only when the build failed. Here's an example of how that might
483  /// look like:
484  ///
485  /// ```no_run
486  /// # use std::path::PathBuf;
487  /// # use rustdoc_json::BuildError;
488  /// #
489  /// let mut stderr: Vec<u8> = Vec::new();
490  ///
491  /// let result: Result<PathBuf, BuildError> = rustdoc_json::Builder::default()
492  ///     .toolchain("nightly")
493  ///     .manifest_path("Cargo.toml")
494  ///     .build_with_captured_output(std::io::sink(), &mut stderr);
495  ///
496  /// match result {
497  ///     Err(BuildError::BuildRustdocJsonError) => {
498  ///         eprintln!("Crate failed to build:\n{}", String::from_utf8_lossy(&stderr));
499  ///     }
500  ///     Err(e) => {
501  ///        eprintln!("Error generating the rustdoc json: {}", e);
502  ///     }
503  ///     Ok(json_path) => {
504  ///         // Do something with the json_path.
505  ///     }
506  /// }
507  /// ```
508  pub fn build_with_captured_output(
509    self,
510    stdout: impl Write,
511    stderr: impl Write,
512  ) -> Result<PathBuf, BuildError> {
513    let capture_output = CaptureOutput { stdout, stderr };
514    run_cargo_rustdoc(self, Some(capture_output))
515  }
516}
517
518/// The part of the package to document
519#[derive(Default, Debug, Clone)]
520#[non_exhaustive]
521pub enum PackageTarget {
522  /// Document the package as a library, i.e. pass `--lib`
523  #[default]
524  Lib,
525  /// Document the given binary, i.e. pass `--bin <name>`
526  Bin(String),
527  /// Document the given binary, i.e. pass `--example <name>`
528  Example(String),
529  /// Document the given binary, i.e. pass `--test <name>`
530  Test(String),
531  /// Document the given binary, i.e. pass `--bench <name>`
532  Bench(String),
533}
534
535#[cfg(test)]
536mod tests {
537  use super::*;
538
539  #[test]
540  fn ensure_toolchain_not_overridden() {
541    // The override is only meant to be changed locally, do not git commit!
542    // If the var is set from the env var, that's OK, so skip the check in
543    // that case.
544    if option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK").is_none() {
545      assert!(OVERRIDDEN_TOOLCHAIN.is_none());
546    }
547  }
548}