cli_xtask/subcommand/
docsrs.rs1use 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#[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 #[clap(flatten)]
20 pub env_args: EnvArgs,
21 #[clap(flatten)]
23 pub package_args: PackageArgs,
24 #[clap(long)]
26 pub default_target: bool,
27 #[clap(long)]
29 pub all_targets: bool,
30 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 #[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 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#[derive(Debug, Clone, Default, Deserialize)]
108#[serde(rename_all = "kebab-case")]
109struct DocsrsMetadata {
110 #[serde(default)]
112 features: Vec<String>,
113 #[serde(default)]
115 all_features: bool,
116 #[serde(default)]
118 no_default_features: bool,
119 #[serde(default)]
124 default_target: Option<String>,
125 #[serde(default = "default_targets")]
144 targets: Vec<String>,
145 #[serde(default)]
147 rustc_args: Vec<String>,
148 #[serde(default)]
150 rustdoc_args: Vec<String>,
151 #[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 let mut rustdoc_args = vec![
232 "-Zunstable-options",
233 "--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}