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