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> = option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK"); // Some("nightly-2022-07-16");
15
16struct 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
71/// Construct the `cargo rustdoc` command to use for building rustdoc JSON. The
72/// command typically ends up looks something like this:
73/// ```bash
74/// cargo +nightly rustdoc --lib --manifest-path Cargo.toml -- -Z unstable-options --output-format json --cap-lints warn
75/// ```
76fn 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/// Returns `./target/doc/crate_name.json`. Also takes care of transforming
170/// `crate-name` to `crate_name`. Also handles `[lib] name = "foo"`.
171#[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    // get the name of the crate/binary/example/test/bench
185    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 one has specified a target explicitly then Cargo appends that target triple name as a subfolder
196    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
205/// Checks if the `rustup` program can be found in `PATH`.
206pub 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
214/// Typically returns the absolute path to the regular cargo `./target`
215/// directory. But also handles packages part of workspaces.
216fn 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
223/// Figures out the name of the library crate corresponding to the given
224/// `Cargo.toml` and `package_name` (in case Cargo.toml is a workspace root).
225fn 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        // We must figure out the package name ourselves from the manifest.
233        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/// Color configuration for the output of `cargo rustdoc`.
261#[derive(Clone, Copy, Debug)]
262pub enum Color {
263    /// Always output colors.
264    Always,
265    /// Never output colors.
266    Never,
267    /// Cargo will decide whether to output colors based on the tty type.
268    Auto,
269}
270
271/// Builds rustdoc JSON. There are many build options. Refer to the docs to
272/// learn about them all. See [top-level docs](crate) for an example on how to use this builder.
273#[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    ///
316    /// Creates a new Builder instance configured to use stable Rust toolchain
317    /// This sets use_stable to true and keeps all other settings at their defaults
318    /// Useful when you want to generate rustdoc JSON using stable Rust instead of nightly
319    ///
320    /// # Example
321    /// ```no_run
322    /// use rustdoc_json_stable::Builder;
323    /// let builder = Builder::stable();
324    /// ```
325    pub fn stable() -> Self {
326        Self {
327            use_stable: true,
328            ..Self::default()
329        }
330    }
331}
332
333impl Builder {
334    /// Set the toolchain. Default: `None`.
335    /// Until rustdoc JSON has stabilized, you will want to set this to
336    /// be `"nightly"` or similar.
337    ///
338    /// If the toolchain is set as `None`, the current active toolchain will be used.
339    ///
340    /// # Notes
341    ///
342    /// The currently active toolchain is typically specified by the
343    /// `RUSTUP_TOOLCHAIN` environment variable, which the rustup proxy
344    /// mechanism sets. See <https://rust-lang.github.io/rustup/overrides.html>
345    /// for more info on how the active toolchain is determined.
346    #[must_use]
347    pub fn toolchain(mut self, toolchain: impl Into<String>) -> Self {
348        self.toolchain = Some(toolchain.into());
349        self
350    }
351
352    /// Clear a toolchain previously set with [`Self::toolchain`].
353    #[must_use]
354    pub fn clear_toolchain(mut self) -> Self {
355        self.toolchain = None;
356        self
357    }
358
359    /// Set the relative or absolute path to `Cargo.toml`. Default: `Cargo.toml`
360    #[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    /// Set what `--target-dir` to pass to `cargo`. Typically only needed if you
367    /// want to be able to build rustdoc JSON for the same crate concurrently,
368    /// for example to parallelize regression tests.
369    #[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    /// Clear a target dir previously set with [`Self::target_dir`].
376    #[must_use]
377    pub fn clear_target_dir(mut self) -> Self {
378        self.target_dir = None;
379        self
380    }
381
382    /// Whether or not to pass `--quiet` to `cargo rustdoc`. Default: `false`
383    #[must_use]
384    pub const fn quiet(mut self, quiet: bool) -> Self {
385        self.quiet = quiet;
386        self
387    }
388
389    /// Whether or not to redirect stdout and stderr to /dev/null. Default: `false`
390    #[must_use]
391    pub const fn silent(mut self, silent: bool) -> Self {
392        self.silent = silent;
393        self
394    }
395
396    /// Color configuration for the output of `cargo rustdoc`.
397    #[must_use]
398    pub const fn color(mut self, color: Color) -> Self {
399        self.color = color;
400        self
401    }
402
403    /// Whether or not to pass `--target` to `cargo rustdoc`. Default: `None`
404    #[must_use]
405    pub fn target(mut self, target: String) -> Self {
406        self.target = Some(target);
407        self
408    }
409
410    /// Whether to pass `--no-default-features` to `cargo rustdoc`. Default: `false`
411    #[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    /// Whether to pass `--all-features` to `cargo rustdoc`. Default: `false`
418    #[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    /// Features to pass to `cargo rustdoc` via `--features`. Default to an empty vector
425    #[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    /// Package to use for `cargo rustdoc` via `-p`. Default: `None`
435    #[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    /// What part of the package to document. Default: `PackageTarget::Lib`
442    #[must_use]
443    pub fn package_target(mut self, package_target: PackageTarget) -> Self {
444        self.package_target = package_target;
445        self
446    }
447
448    /// Whether to pass `--document-private-items` to `cargo rustdoc`. Default: `false`
449    #[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    /// What to pass as `--cap-lints` to rustdoc JSON build command
456    #[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    /// Generate rustdoc JSON for a crate. Returns the path to the freshly
463    /// built rustdoc JSON file.
464    ///
465    /// This method will print the stdout and stderr of the `cargo rustdoc` command to the stdout
466    /// and stderr of the calling process. If you want to capture the output, use
467    /// [`Builder::build_with_captured_output()`].
468    ///
469    /// See [top-level docs](crate) for an example on how to use it.
470    ///
471    /// # Errors
472    ///
473    /// E.g. if building the JSON fails or if the manifest path does not exist or is
474    /// invalid.
475    pub fn build(self) -> Result<PathBuf, BuildError> {
476        run_cargo_rustdoc::<std::io::Sink, std::io::Sink>(self, None)
477    }
478
479    /// Generate rustdoc JSON for a crate. This works like [`Builder::build()`], but will
480    /// capture the stdout and stderr of the `cargo rustdoc` command. The output will be written to
481    /// the `stdout` and `stderr` parameters. In particular, potential warnings and errors emitted
482    /// by `cargo rustdoc` will be captured to `stderr`. This can be useful if you want to present
483    /// these errors to the user only when the build failed. Here's an example of how that might
484    /// look like:
485    ///
486    /// ```no_run
487    /// # use std::path::PathBuf;
488    /// # use rustdoc_json::BuildError;
489    /// #
490    /// let mut stderr: Vec<u8> = Vec::new();
491    ///
492    /// let result: Result<PathBuf, BuildError> = rustdoc_json::Builder::default()
493    ///     .toolchain("nightly")
494    ///     .manifest_path("Cargo.toml")
495    ///     .build_with_captured_output(std::io::sink(), &mut stderr);
496    ///
497    /// match result {
498    ///     Err(BuildError::BuildRustdocJsonError) => {
499    ///         eprintln!("Crate failed to build:\n{}", String::from_utf8_lossy(&stderr));
500    ///     }
501    ///     Err(e) => {
502    ///        eprintln!("Error generating the rustdoc json: {}", e);
503    ///     }
504    ///     Ok(json_path) => {
505    ///         // Do something with the json_path.
506    ///     }
507    /// }
508    /// ```
509    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/// The part of the package to document
520#[derive(Default, Debug, Clone)]
521#[non_exhaustive]
522pub enum PackageTarget {
523    /// Document the package as a library, i.e. pass `--lib`
524    #[default]
525    Lib,
526    /// Document the given binary, i.e. pass `--bin <name>`
527    Bin(String),
528    /// Document the given binary, i.e. pass `--example <name>`
529    Example(String),
530    /// Document the given binary, i.e. pass `--test <name>`
531    Test(String),
532    /// Document the given binary, i.e. pass `--bench <name>`
533    Bench(String),
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    #[test]
541    fn ensure_toolchain_not_overridden() {
542        // The override is only meant to be changed locally, do not git commit!
543        // If the var is set from the env var, that's OK, so skip the check in
544        // that case.
545        if option_env!("RUSTDOC_JSON_OVERRIDDEN_TOOLCHAIN_HACK").is_none() {
546            assert!(OVERRIDDEN_TOOLCHAIN.is_none());
547        }
548    }
549}