#
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::{CargoResult, internal};
    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}