cargo/core/compiler/
rustdoc.rs

1//! Utilities for building with rustdoc.
2
3use 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/// Mode used for `std`. This is for unstable feature [`-Zrustdoc-map`][1].
20///
21/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
22#[derive(Debug, Hash)]
23pub enum RustdocExternMode {
24    /// Use a local `file://` URL.
25    Local,
26    /// Use a remote URL to <https://doc.rust-lang.org/> (default).
27    Remote,
28    /// An arbitrary URL.
29    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/// A map of registry names to URLs where documentations are hosted.
63/// This is for unstable feature [`-Zrustdoc-map`][1].
64///
65/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
66#[derive(serde::Deserialize, Debug)]
67#[serde(default)]
68pub struct RustdocExternMap {
69    #[serde(deserialize_with = "default_crates_io_to_docs_rs")]
70    /// * Key is the registry name in the configuration `[registries.<name>]`.
71    /// * Value is the URL where the documentation is hosted.
72    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
106/// Recursively generate html root url for all units and their children.
107///
108/// This is needed because in case there is a reexport of foreign reexport, you
109/// need to have information about grand-children deps level (deps of your deps).
110fn 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
168/// Adds unstable flag [`--extern-html-root-url`][1] to the given `rustdoc`
169/// invocation. This is for unstable feature [`-Zrustdoc-map`][2].
170///
171/// [1]: https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html#--extern-html-root-url-control-how-rustdoc-links-to-non-local-crates
172/// [2]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
173pub 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    // Collect mapping of registry name -> index url.
186    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
247/// Adds unstable flag [`--output-format`][1] to the given `rustdoc`
248/// invocation. This is for unstable feature [`-Zunstable-features`].
249///
250/// [1]: https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html?highlight=output-format#-w--output-format-output-format
251pub 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/// Indicates whether a target should have examples scraped from it by rustdoc.
271/// Configured within Cargo.toml and only for unstable feature
272/// [`-Zrustdoc-scrape-examples`][1].
273///
274/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#scrape-examples
275#[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    /// Returns the set of [`Docscrape`] units that have a direct dependency on `unit`.
294    ///
295    /// [`RunCustomBuild`] units are excluded because we allow failures
296    /// from type checks but not build script executions.
297    /// A plain old `cargo doc` would just die if a build script execution fails,
298    /// there is no reason for `-Zrustdoc-scrape-examples` to keep going.
299    ///
300    /// [`Docscrape`]: crate::core::compiler::CompileMode::Docscrape
301    /// [`RunCustomBuild`]: crate::core::compiler::CompileMode::Docscrape
302    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    /// Returns true if this unit is needed for doing doc-scraping and is also
314    /// allowed to fail without killing the build.
315    pub fn unit_can_fail_for_docscraping(&self, unit: &Unit) -> bool {
316        // If the unit is not a Docscrape unit, e.g. a Lib target that is
317        // checked to scrape an Example target, then we need to get the doc-scrape-examples
318        // configuration for the reverse-dependent Example target.
319        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            // All Docscrape units must have doc-scrape-examples unset. If any are true,
329            // then the unit is not allowed to fail.
330            for_scrape_units
331                .iter()
332                .all(|unit| unit.target.doc_scrape_examples().is_unset())
333        }
334    }
335}