cli_xtask/subcommand/
docsrs.rs

1use std::{collections::HashMap, fs, process::Command};
2
3use cargo_metadata::Package;
4use serde::Deserialize;
5
6use crate::{
7    args::{EnvArgs, PackageArgs},
8    config::Config,
9    process::CommandExt,
10    Error, Result, Run,
11};
12
13/// Arguments definition of the `docsrs` subcommand.
14#[cfg_attr(doc, doc = include_str!("../../doc/cargo-xtask-docsrs.md"))]
15#[derive(Debug, Clone, Default, clap::Args)]
16#[non_exhaustive]
17pub struct Docsrs {
18    /// Environment variables to set for `cargo doc`.
19    #[clap(flatten)]
20    pub env_args: EnvArgs,
21    /// Packages to run the `cargo doc` with.
22    #[clap(flatten)]
23    pub package_args: PackageArgs,
24    /// Build documents for docs.rs's default target
25    #[clap(long)]
26    pub default_target: bool,
27    /// Build documents for all supported targets.
28    #[clap(long)]
29    pub all_targets: bool,
30    /// Options to pass to the `cargo doc`.
31    pub extra_options: Vec<String>,
32}
33
34impl Run for Docsrs {
35    fn run(&self, config: &Config) -> Result<()> {
36        self.run(config)
37    }
38}
39
40impl Docsrs {
41    /// Runs the `docsrs` subcommand.
42    #[tracing::instrument(name = "docsrs", skip_all, err)]
43    pub fn run(&self, _config: &Config) -> Result<()> {
44        let Self {
45            env_args,
46            package_args,
47            default_target,
48            all_targets,
49            extra_options,
50        } = self;
51
52        for res in package_args.packages() {
53            let (workspace, package) = res?;
54            let metadata = DocsrsMetadata::try_from(package)?;
55            let target_options = if *all_targets || *default_target {
56                metadata
57                    .target_options(*all_targets)
58                    .into_iter()
59                    .map(Some)
60                    .collect::<Vec<_>>()
61            } else {
62                vec![None]
63            };
64            for target in target_options {
65                // rustup run nightly cargo doc --package <pkg> <docsrs_options> <extra_options>
66                // `cargo +nightly doc` fails on windows, so use rustup instead
67                let mut cmd = Command::new("rustup");
68                cmd.args([
69                    "run",
70                    "nightly",
71                    "cargo",
72                    "doc",
73                    "--no-deps",
74                    "--package",
75                    &package.name,
76                ]);
77                if let Some(target) = target {
78                    cmd.args(["--target", target]);
79                }
80                cmd.arg("-Zunstable-options")
81                    .arg("-Zrustdoc-map")
82                    .args(metadata.args())
83                    .args(extra_options)
84                    .envs(metadata.envs(&env_args.env))
85                    .workspace_spawn(workspace)?;
86            }
87
88            if let Some(package) = workspace.root_package() {
89                let index = workspace.target_directory.join("doc/index.html");
90                fs::write(
91                    index,
92                    format!(
93                        r#"<meta http-equiv="refresh" content="0; url=./{}/">"#,
94                        package.name.replace('-', "_")
95                    ),
96                )?;
97            }
98        }
99
100        Ok(())
101    }
102}
103
104/// Package metadata for docs.rs
105///
106/// <https://docs.rs/about/metadata>
107#[derive(Debug, Clone, Default, Deserialize)]
108#[serde(rename_all = "kebab-case")]
109struct DocsrsMetadata {
110    /// Features to pass to Cargo (default: [])
111    #[serde(default)]
112    features: Vec<String>,
113    /// Whether to pass `--all-features` to Cargo (default: false)
114    #[serde(default)]
115    all_features: bool,
116    /// Whether to pass `--no-default-features` to Cargo (default: false)
117    #[serde(default)]
118    no_default_features: bool,
119    /// Target to test build on, used as the default landing page (default:
120    /// "x86_64-unknown-linux-gnu")
121    ///
122    /// Any target supported by rustup can be used.
123    #[serde(default)]
124    default_target: Option<String>,
125    /// Targets to build (default: see below)
126    ///
127    /// Any target supported by rustup can be used.
128    ///
129    /// Default targets:
130    /// - x86_64-unknown-linux-gnu
131    /// - x86_64-apple-darwin
132    /// - x86_64-pc-windows-msvc
133    /// - i686-unknown-linux-gnu
134    /// - i686-pc-windows-msvc
135    ///
136    /// Set this to `[]` to only build the default target.
137    ///
138    /// # If `default-target` is unset, the first element of `targets` is treated as the default target.
139    /// Otherwise, these `targets` are built in addition to the default target.
140    /// If both `default-target` and `targets` are unset,
141    ///   all tier-one targets will be built and `x86_64-unknown-linux-gnu` will
142    /// be used as the default target.
143    #[serde(default = "default_targets")]
144    targets: Vec<String>,
145    /// Additional `RUSTFLAGS` to set (default: [])
146    #[serde(default)]
147    rustc_args: Vec<String>,
148    /// Additional `RUSTDOCFLAGS` to set (default: [])
149    #[serde(default)]
150    rustdoc_args: Vec<String>,
151    /// List of command line arguments for `cargo`.
152    ///
153    /// These cannot be a subcommand, they may only be options.
154    #[serde(default)]
155    cargo_args: Vec<String>,
156}
157
158impl TryFrom<&Package> for DocsrsMetadata {
159    type Error = Error;
160
161    fn try_from(value: &Package) -> Result<Self> {
162        let table = || value.metadata.get("docs")?.get("rs");
163        let table = match table() {
164            Some(table) => table,
165            None => return Ok(Self::default()),
166        };
167        let metadata = serde_json::from_value(table.clone())?;
168        Ok(metadata)
169    }
170}
171
172fn default_targets() -> Vec<String> {
173    [
174        "x86_64-unknown-linux-gnu",
175        "x86_64-apple-darwin",
176        "x86_64-pc-windows-msvc",
177        "i686-unknown-linux-gnu",
178        "i686-pc-windows-msvc",
179    ]
180    .into_iter()
181    .map(String::from)
182    .collect()
183}
184
185impl DocsrsMetadata {
186    fn target_options(&self, all_targets: bool) -> Vec<&str> {
187        if all_targets {
188            self.targets.iter().map(|s| s.as_str()).collect()
189        } else {
190            vec![self.default_target()]
191        }
192    }
193
194    fn default_target(&self) -> &str {
195        self.default_target.as_deref().unwrap_or_else(|| {
196            self.targets
197                .first()
198                .map(|s| s.as_str())
199                .unwrap_or("x86_64-unknown-linux-gnu")
200        })
201    }
202
203    fn args(&self) -> Vec<&str> {
204        let mut args = vec![];
205        for feature in &self.features {
206            args.extend(["--feature", feature]);
207        }
208        if self.all_features {
209            args.push("--all-features");
210        }
211        if self.no_default_features {
212            args.push("--no-default-features");
213        }
214        if !self.cargo_args.is_empty() {
215            args.extend(self.cargo_args.iter().map(|s| s.as_str()));
216        }
217        args
218    }
219
220    fn envs(&self, base_env: &[(String, String)]) -> HashMap<String, String> {
221        let mut envs: HashMap<String, String> = base_env.iter().cloned().collect();
222        if !self.rustc_args.is_empty() {
223            let s = envs.entry("RUSTFLAGS".to_string()).or_default();
224            if !s.is_empty() {
225                s.push(' ');
226            }
227            s.push_str(&self.rustc_args.join(" "));
228        }
229
230        // copied from https://github.com/rust-lang/docs.rs/blob/4635eb745e77c6de9c055cb7334f48375c0cda5d/src/docbuilder/rustwide_builder.rs#L776
231        let mut rustdoc_args = vec![
232            "-Zunstable-options",
233            // Coment out so that static resouces are loaded when the document is published on
234            // GitHub Pages
235            // "--static-root-path", "/-/rustdoc.static/",
236
237            // Comment out to accept `-D warnings` for CI
238            // "--cap-lints", "warn",
239            "--extern-html-root-takes-precedence",
240        ];
241        rustdoc_args.extend(self.rustdoc_args.iter().map(|s| s.as_str()));
242        let s = envs.entry("RUSTDOCFLAGS".to_string()).or_default();
243        if !s.is_empty() {
244            s.push(' ');
245        }
246        s.push_str(&rustdoc_args.join(" "));
247
248        envs
249    }
250}