Skip to main content

cargo/compiler/
rustdoc.rs

1//! Utilities for building with rustdoc.
2
3use crate::compiler::build_runner::BuildRunner;
4use crate::compiler::unit::Unit;
5use crate::compiler::{BuildContext, CompileKind};
6use crate::context::{RustdocExternMap, RustdocExternMode};
7use crate::sources::CRATES_IO_REGISTRY;
8use crate::util::data_structures::HashMap;
9use crate::util::data_structures::HashSet;
10use crate::util::errors::{CargoResult, internal};
11use cargo_util::ProcessBuilder;
12use url::Url;
13
14/// Recursively generate html root url for all units and their children.
15///
16/// This is needed because in case there is a reexport of foreign reexport, you
17/// need to have information about grand-children deps level (deps of your deps).
18fn build_all_urls(
19    build_runner: &BuildRunner<'_, '_>,
20    rustdoc: &mut ProcessBuilder,
21    unit: &Unit,
22    name2url: &HashMap<&String, Url>,
23    map: &RustdocExternMap,
24    unstable_opts: &mut bool,
25    seen: &mut HashSet<Unit>,
26) {
27    for dep in build_runner.unit_deps(unit) {
28        if !seen.insert(dep.unit.clone()) {
29            continue;
30        }
31        if !dep.unit.target.is_linkable() || dep.unit.mode.is_doc() {
32            continue;
33        }
34        for (registry, location) in &map.registries {
35            let sid = dep.unit.pkg.package_id().source_id();
36            let matches_registry = || -> bool {
37                if !sid.is_registry() {
38                    return false;
39                }
40                if sid.is_crates_io() {
41                    return registry == CRATES_IO_REGISTRY;
42                }
43                if let Some(index_url) = name2url.get(registry) {
44                    return index_url == sid.url();
45                }
46                false
47            };
48            if matches_registry() {
49                let mut url = location.clone();
50                if !url.contains("{pkg_name}") && !url.contains("{version}") {
51                    if !url.ends_with('/') {
52                        url.push('/');
53                    }
54                    url.push_str("{pkg_name}/{version}/");
55                }
56                let url = url
57                    .replace("{pkg_name}", &dep.unit.pkg.name())
58                    .replace("{version}", &dep.unit.pkg.version().to_string());
59                rustdoc.arg("--extern-html-root-url");
60                rustdoc.arg(format!("{}={}", dep.unit.target.crate_name(), url));
61                *unstable_opts = true;
62            }
63        }
64        build_all_urls(
65            build_runner,
66            rustdoc,
67            &dep.unit,
68            name2url,
69            map,
70            unstable_opts,
71            seen,
72        );
73    }
74}
75
76/// Adds unstable flag [`--extern-html-root-url`][1] to the given `rustdoc`
77/// invocation. This is for unstable feature [`-Zrustdoc-map`][2].
78///
79/// [1]: https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html#--extern-html-root-url-control-how-rustdoc-links-to-non-local-crates
80/// [2]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
81pub fn add_root_urls(
82    build_runner: &BuildRunner<'_, '_>,
83    unit: &Unit,
84    rustdoc: &mut ProcessBuilder,
85) -> CargoResult<()> {
86    let gctx = build_runner.bcx.gctx;
87    if !gctx.cli_unstable().rustdoc_map {
88        tracing::debug!("`doc.extern-map` ignored, requires -Zrustdoc-map flag");
89        return Ok(());
90    }
91    let map = gctx.doc_extern_map()?;
92    let mut unstable_opts = false;
93    // Collect mapping of registry name -> index url.
94    let name2url: HashMap<&String, Url> = map
95        .registries
96        .keys()
97        .filter_map(|name| {
98            if let Ok(index_url) = gctx.get_registry_index(name) {
99                Some((name, index_url))
100            } else {
101                tracing::warn!(
102                    "`doc.extern-map.{}` specifies a registry that is not defined",
103                    name
104                );
105                None
106            }
107        })
108        .collect();
109    build_all_urls(
110        build_runner,
111        rustdoc,
112        unit,
113        &name2url,
114        map,
115        &mut unstable_opts,
116        &mut HashSet::default(),
117    );
118    let std_url = match &map.std {
119        None | Some(RustdocExternMode::Remote) => None,
120        Some(RustdocExternMode::Local) => {
121            let sysroot = &build_runner.bcx.target_data.info(CompileKind::Host).sysroot;
122            let html_root = sysroot.join("share").join("doc").join("rust").join("html");
123            if html_root.exists() {
124                let url = Url::from_file_path(&html_root).map_err(|()| {
125                    internal(format!(
126                        "`{}` failed to convert to URL",
127                        html_root.display()
128                    ))
129                })?;
130                Some(url.to_string())
131            } else {
132                tracing::warn!(
133                    "`doc.extern-map.std` is \"local\", but local docs don't appear to exist at {}",
134                    html_root.display()
135                );
136                None
137            }
138        }
139        Some(RustdocExternMode::Url(s)) => Some(s.to_string()),
140    };
141    if let Some(url) = std_url {
142        for name in &["std", "core", "alloc", "proc_macro"] {
143            rustdoc.arg("--extern-html-root-url");
144            rustdoc.arg(format!("{}={}", name, url));
145            unstable_opts = true;
146        }
147    }
148
149    if unstable_opts {
150        rustdoc.arg("-Zunstable-options");
151    }
152    Ok(())
153}
154
155/// Adds unstable flag [`--output-format`][1] to the given `rustdoc`
156/// invocation. This is for unstable feature `-Zunstable-features`.
157///
158/// [1]: https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html?highlight=output-format#-w--output-format-output-format
159pub fn add_output_format(
160    build_runner: &BuildRunner<'_, '_>,
161    rustdoc: &mut ProcessBuilder,
162) -> CargoResult<()> {
163    if build_runner.bcx.build_config.intent.wants_doc_json_output() {
164        rustdoc.arg("-Zunstable-options");
165        rustdoc.arg("--output-format=json");
166    }
167
168    Ok(())
169}
170
171/// Indicates whether a target should have examples scraped from it by rustdoc.
172/// Configured within Cargo.toml and only for unstable feature
173/// [`-Zrustdoc-scrape-examples`][1].
174///
175/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#scrape-examples
176#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Copy)]
177pub enum RustdocScrapeExamples {
178    Enabled,
179    Disabled,
180    Unset,
181}
182
183impl RustdocScrapeExamples {
184    pub fn is_enabled(&self) -> bool {
185        matches!(self, RustdocScrapeExamples::Enabled)
186    }
187
188    pub fn is_unset(&self) -> bool {
189        matches!(self, RustdocScrapeExamples::Unset)
190    }
191}
192
193impl BuildContext<'_, '_> {
194    /// Returns the set of [`Docscrape`] units that have a direct dependency on `unit`.
195    ///
196    /// [`RunCustomBuild`] units are excluded because we allow failures
197    /// from type checks but not build script executions.
198    /// A plain old `cargo doc` would just die if a build script execution fails,
199    /// there is no reason for `-Zrustdoc-scrape-examples` to keep going.
200    ///
201    /// [`Docscrape`]: crate::compiler::CompileMode::Docscrape
202    /// [`RunCustomBuild`]: crate::compiler::CompileMode::Docscrape
203    pub fn scrape_units_have_dep_on<'a>(&'a self, unit: &'a Unit) -> Vec<&'a Unit> {
204        self.scrape_units
205            .iter()
206            .filter(|scrape_unit| {
207                self.unit_graph[scrape_unit]
208                    .iter()
209                    .any(|dep| &dep.unit == unit && !dep.unit.mode.is_run_custom_build())
210            })
211            .collect()
212    }
213
214    /// Returns true if this unit is needed for doing doc-scraping and is also
215    /// allowed to fail without killing the build.
216    pub fn unit_can_fail_for_docscraping(&self, unit: &Unit) -> bool {
217        // If the unit is not a Docscrape unit, e.g. a Lib target that is
218        // checked to scrape an Example target, then we need to get the doc-scrape-examples
219        // configuration for the reverse-dependent Example target.
220        let for_scrape_units = if unit.mode.is_doc_scrape() {
221            vec![unit]
222        } else {
223            self.scrape_units_have_dep_on(unit)
224        };
225
226        if for_scrape_units.is_empty() {
227            false
228        } else {
229            // All Docscrape units must have doc-scrape-examples unset. If any are true,
230            // then the unit is not allowed to fail.
231            for_scrape_units
232                .iter()
233                .all(|unit| unit.target.doc_scrape_examples().is_unset())
234        }
235    }
236}