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