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