1use crate::core::compiler::build_runner::BuildRunner;
4use crate::core::compiler::unit::Unit;
5use crate::core::compiler::{BuildContext, CompileKind};
6use crate::sources::CRATES_IO_REGISTRY;
7use crate::util::errors::{internal, CargoResult};
8use cargo_util::ProcessBuilder;
9use std::collections::HashMap;
10use std::collections::HashSet;
11use std::fmt;
12use std::hash;
13use url::Url;
14
15const DOCS_RS_URL: &'static str = "https://docs.rs/";
16
17#[derive(Debug, Hash)]
21pub enum RustdocExternMode {
22 Local,
24 Remote,
26 Url(String),
28}
29
30impl From<String> for RustdocExternMode {
31 fn from(s: String) -> RustdocExternMode {
32 match s.as_ref() {
33 "local" => RustdocExternMode::Local,
34 "remote" => RustdocExternMode::Remote,
35 _ => RustdocExternMode::Url(s),
36 }
37 }
38}
39
40impl fmt::Display for RustdocExternMode {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 RustdocExternMode::Local => "local".fmt(f),
44 RustdocExternMode::Remote => "remote".fmt(f),
45 RustdocExternMode::Url(s) => s.fmt(f),
46 }
47 }
48}
49
50impl<'de> serde::de::Deserialize<'de> for RustdocExternMode {
51 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52 where
53 D: serde::de::Deserializer<'de>,
54 {
55 let s = String::deserialize(deserializer)?;
56 Ok(s.into())
57 }
58}
59
60#[derive(serde::Deserialize, Debug)]
65#[serde(default)]
66pub struct RustdocExternMap {
67 #[serde(deserialize_with = "default_crates_io_to_docs_rs")]
68 registries: HashMap<String, String>,
71 std: Option<RustdocExternMode>,
72}
73
74impl Default for RustdocExternMap {
75 fn default() -> Self {
76 Self {
77 registries: HashMap::from([(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into())]),
78 std: None,
79 }
80 }
81}
82
83fn default_crates_io_to_docs_rs<'de, D: serde::Deserializer<'de>>(
84 de: D,
85) -> Result<HashMap<String, String>, D::Error> {
86 use serde::Deserialize;
87 let mut registries = HashMap::deserialize(de)?;
88 if !registries.contains_key(CRATES_IO_REGISTRY) {
89 registries.insert(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into());
90 }
91 Ok(registries)
92}
93
94impl hash::Hash for RustdocExternMap {
95 fn hash<H: hash::Hasher>(&self, into: &mut H) {
96 self.std.hash(into);
97 for (key, value) in &self.registries {
98 key.hash(into);
99 value.hash(into);
100 }
101 }
102}
103
104fn build_all_urls(
109 build_runner: &BuildRunner<'_, '_>,
110 rustdoc: &mut ProcessBuilder,
111 unit: &Unit,
112 name2url: &HashMap<&String, Url>,
113 map: &RustdocExternMap,
114 unstable_opts: &mut bool,
115 seen: &mut HashSet<Unit>,
116) {
117 for dep in build_runner.unit_deps(unit) {
118 if !seen.insert(dep.unit.clone()) {
119 continue;
120 }
121 if !dep.unit.target.is_linkable() || dep.unit.mode.is_doc() {
122 continue;
123 }
124 for (registry, location) in &map.registries {
125 let sid = dep.unit.pkg.package_id().source_id();
126 let matches_registry = || -> bool {
127 if !sid.is_registry() {
128 return false;
129 }
130 if sid.is_crates_io() {
131 return registry == CRATES_IO_REGISTRY;
132 }
133 if let Some(index_url) = name2url.get(registry) {
134 return index_url == sid.url();
135 }
136 false
137 };
138 if matches_registry() {
139 let mut url = location.clone();
140 if !url.contains("{pkg_name}") && !url.contains("{version}") {
141 if !url.ends_with('/') {
142 url.push('/');
143 }
144 url.push_str("{pkg_name}/{version}/");
145 }
146 let url = url
147 .replace("{pkg_name}", &dep.unit.pkg.name())
148 .replace("{version}", &dep.unit.pkg.version().to_string());
149 rustdoc.arg("--extern-html-root-url");
150 rustdoc.arg(format!("{}={}", dep.unit.target.crate_name(), url));
151 *unstable_opts = true;
152 }
153 }
154 build_all_urls(
155 build_runner,
156 rustdoc,
157 &dep.unit,
158 name2url,
159 map,
160 unstable_opts,
161 seen,
162 );
163 }
164}
165
166pub fn add_root_urls(
172 build_runner: &BuildRunner<'_, '_>,
173 unit: &Unit,
174 rustdoc: &mut ProcessBuilder,
175) -> CargoResult<()> {
176 let gctx = build_runner.bcx.gctx;
177 if !gctx.cli_unstable().rustdoc_map {
178 tracing::debug!("`doc.extern-map` ignored, requires -Zrustdoc-map flag");
179 return Ok(());
180 }
181 let map = gctx.doc_extern_map()?;
182 let mut unstable_opts = false;
183 let name2url: HashMap<&String, Url> = map
185 .registries
186 .keys()
187 .filter_map(|name| {
188 if let Ok(index_url) = gctx.get_registry_index(name) {
189 Some((name, index_url))
190 } else {
191 tracing::warn!(
192 "`doc.extern-map.{}` specifies a registry that is not defined",
193 name
194 );
195 None
196 }
197 })
198 .collect();
199 build_all_urls(
200 build_runner,
201 rustdoc,
202 unit,
203 &name2url,
204 map,
205 &mut unstable_opts,
206 &mut HashSet::new(),
207 );
208 let std_url = match &map.std {
209 None | Some(RustdocExternMode::Remote) => None,
210 Some(RustdocExternMode::Local) => {
211 let sysroot = &build_runner.bcx.target_data.info(CompileKind::Host).sysroot;
212 let html_root = sysroot.join("share").join("doc").join("rust").join("html");
213 if html_root.exists() {
214 let url = Url::from_file_path(&html_root).map_err(|()| {
215 internal(format!(
216 "`{}` failed to convert to URL",
217 html_root.display()
218 ))
219 })?;
220 Some(url.to_string())
221 } else {
222 tracing::warn!(
223 "`doc.extern-map.std` is \"local\", but local docs don't appear to exist at {}",
224 html_root.display()
225 );
226 None
227 }
228 }
229 Some(RustdocExternMode::Url(s)) => Some(s.to_string()),
230 };
231 if let Some(url) = std_url {
232 for name in &["std", "core", "alloc", "proc_macro"] {
233 rustdoc.arg("--extern-html-root-url");
234 rustdoc.arg(format!("{}={}", name, url));
235 unstable_opts = true;
236 }
237 }
238
239 if unstable_opts {
240 rustdoc.arg("-Zunstable-options");
241 }
242 Ok(())
243}
244
245pub fn add_output_format(
250 build_runner: &BuildRunner<'_, '_>,
251 rustdoc: &mut ProcessBuilder,
252) -> CargoResult<()> {
253 let gctx = build_runner.bcx.gctx;
254 if !gctx.cli_unstable().unstable_options {
255 tracing::debug!("`unstable-options` is ignored, required -Zunstable-options flag");
256 return Ok(());
257 }
258
259 if build_runner.bcx.build_config.intent.wants_doc_json_output() {
260 rustdoc.arg("-Zunstable-options");
261 rustdoc.arg("--output-format=json");
262 }
263
264 Ok(())
265}
266
267#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Copy)]
273pub enum RustdocScrapeExamples {
274 Enabled,
275 Disabled,
276 Unset,
277}
278
279impl RustdocScrapeExamples {
280 pub fn is_enabled(&self) -> bool {
281 matches!(self, RustdocScrapeExamples::Enabled)
282 }
283
284 pub fn is_unset(&self) -> bool {
285 matches!(self, RustdocScrapeExamples::Unset)
286 }
287}
288
289impl BuildContext<'_, '_> {
290 pub fn scrape_units_have_dep_on<'a>(&'a self, unit: &'a Unit) -> Vec<&'a Unit> {
300 self.scrape_units
301 .iter()
302 .filter(|scrape_unit| {
303 self.unit_graph[scrape_unit]
304 .iter()
305 .any(|dep| &dep.unit == unit && !dep.unit.mode.is_run_custom_build())
306 })
307 .collect()
308 }
309
310 pub fn unit_can_fail_for_docscraping(&self, unit: &Unit) -> bool {
313 let for_scrape_units = if unit.mode.is_doc_scrape() {
317 vec![unit]
318 } else {
319 self.scrape_units_have_dep_on(unit)
320 };
321
322 if for_scrape_units.is_empty() {
323 false
324 } else {
325 for_scrape_units
328 .iter()
329 .all(|unit| unit.target.doc_scrape_examples().is_unset())
330 }
331 }
332}