#
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}