blob: aa4bd0dd4cba8e02d1536e100041bdab6ed84445 [file] [log] [blame]
//! Utilities for building with rustdoc.
use crate::core::compiler::context::Context;
use crate::core::compiler::unit::Unit;
use crate::core::compiler::{BuildContext, CompileKind};
use crate::sources::CRATES_IO_REGISTRY;
use crate::util::errors::{internal, CargoResult};
use cargo_util::ProcessBuilder;
use std::collections::HashMap;
use std::fmt;
use std::hash;
use url::Url;
const DOCS_RS_URL: &'static str = "https://docs.rs/";
/// Mode used for `std`. This is for unstable feature [`-Zrustdoc-map`][1].
///
/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
#[derive(Debug, Hash)]
pub enum RustdocExternMode {
/// Use a local `file://` URL.
Local,
/// Use a remote URL to <https://doc.rust-lang.org/> (default).
Remote,
/// An arbitrary URL.
Url(String),
}
impl From<String> for RustdocExternMode {
fn from(s: String) -> RustdocExternMode {
match s.as_ref() {
"local" => RustdocExternMode::Local,
"remote" => RustdocExternMode::Remote,
_ => RustdocExternMode::Url(s),
}
}
}
impl fmt::Display for RustdocExternMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RustdocExternMode::Local => "local".fmt(f),
RustdocExternMode::Remote => "remote".fmt(f),
RustdocExternMode::Url(s) => s.fmt(f),
}
}
}
impl<'de> serde::de::Deserialize<'de> for RustdocExternMode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(s.into())
}
}
/// A map of registry names to URLs where documentations are hosted.
/// This is for unstable feature [`-Zrustdoc-map`][1].
///
/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
#[derive(serde::Deserialize, Debug)]
#[serde(default)]
pub struct RustdocExternMap {
#[serde(deserialize_with = "default_crates_io_to_docs_rs")]
/// * Key is the registry name in the configuration `[registries.<name>]`.
/// * Value is the URL where the documentation is hosted.
registries: HashMap<String, String>,
std: Option<RustdocExternMode>,
}
impl Default for RustdocExternMap {
fn default() -> Self {
Self {
registries: HashMap::from([(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into())]),
std: None,
}
}
}
fn default_crates_io_to_docs_rs<'de, D: serde::Deserializer<'de>>(
de: D,
) -> Result<HashMap<String, String>, D::Error> {
use serde::Deserialize;
let mut registries = HashMap::deserialize(de)?;
if !registries.contains_key(CRATES_IO_REGISTRY) {
registries.insert(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into());
}
Ok(registries)
}
impl hash::Hash for RustdocExternMap {
fn hash<H: hash::Hasher>(&self, into: &mut H) {
self.std.hash(into);
for (key, value) in &self.registries {
key.hash(into);
value.hash(into);
}
}
}
/// Adds unstable flag [`--extern-html-root-url`][1] to the given `rustdoc`
/// invocation. This is for unstable feature [`-Zrustdoc-map`][2].
///
/// [1]: https://doc.rust-lang.org/nightly/rustdoc/unstable-features.html#--extern-html-root-url-control-how-rustdoc-links-to-non-local-crates
/// [2]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
pub fn add_root_urls(
cx: &Context<'_, '_>,
unit: &Unit,
rustdoc: &mut ProcessBuilder,
) -> CargoResult<()> {
let config = cx.bcx.config;
if !config.cli_unstable().rustdoc_map {
tracing::debug!("`doc.extern-map` ignored, requires -Zrustdoc-map flag");
return Ok(());
}
let map = config.doc_extern_map()?;
let mut unstable_opts = false;
// Collect mapping of registry name -> index url.
let name2url: HashMap<&String, Url> = map
.registries
.keys()
.filter_map(|name| {
if let Ok(index_url) = config.get_registry_index(name) {
Some((name, index_url))
} else {
tracing::warn!(
"`doc.extern-map.{}` specifies a registry that is not defined",
name
);
None
}
})
.collect();
for dep in cx.unit_deps(unit) {
if dep.unit.target.is_linkable() && !dep.unit.mode.is_doc() {
for (registry, location) in &map.registries {
let sid = dep.unit.pkg.package_id().source_id();
let matches_registry = || -> bool {
if !sid.is_registry() {
return false;
}
if sid.is_crates_io() {
return registry == CRATES_IO_REGISTRY;
}
if let Some(index_url) = name2url.get(registry) {
return index_url == sid.url();
}
false
};
if matches_registry() {
let mut url = location.clone();
if !url.contains("{pkg_name}") && !url.contains("{version}") {
if !url.ends_with('/') {
url.push('/');
}
url.push_str("{pkg_name}/{version}/");
}
let url = url
.replace("{pkg_name}", &dep.unit.pkg.name())
.replace("{version}", &dep.unit.pkg.version().to_string());
rustdoc.arg("--extern-html-root-url");
rustdoc.arg(format!("{}={}", dep.unit.target.crate_name(), url));
unstable_opts = true;
}
}
}
}
let std_url = match &map.std {
None | Some(RustdocExternMode::Remote) => None,
Some(RustdocExternMode::Local) => {
let sysroot = &cx.bcx.target_data.info(CompileKind::Host).sysroot;
let html_root = sysroot.join("share").join("doc").join("rust").join("html");
if html_root.exists() {
let url = Url::from_file_path(&html_root).map_err(|()| {
internal(format!(
"`{}` failed to convert to URL",
html_root.display()
))
})?;
Some(url.to_string())
} else {
tracing::warn!(
"`doc.extern-map.std` is \"local\", but local docs don't appear to exist at {}",
html_root.display()
);
None
}
}
Some(RustdocExternMode::Url(s)) => Some(s.to_string()),
};
if let Some(url) = std_url {
for name in &["std", "core", "alloc", "proc_macro"] {
rustdoc.arg("--extern-html-root-url");
rustdoc.arg(format!("{}={}", name, url));
unstable_opts = true;
}
}
if unstable_opts {
rustdoc.arg("-Zunstable-options");
}
Ok(())
}
/// Indicates whether a target should have examples scraped from it by rustdoc.
/// Configured within Cargo.toml and only for unstable feature
/// [`-Zrustdoc-scrape-examples`][1].
///
/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#scrape-examples
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Copy)]
pub enum RustdocScrapeExamples {
Enabled,
Disabled,
Unset,
}
impl RustdocScrapeExamples {
pub fn is_enabled(&self) -> bool {
matches!(self, RustdocScrapeExamples::Enabled)
}
pub fn is_unset(&self) -> bool {
matches!(self, RustdocScrapeExamples::Unset)
}
}
impl BuildContext<'_, '_> {
/// Returns the set of [`Docscrape`] units that have a direct dependency on `unit`.
///
/// [`RunCustomBuild`] units are excluded because we allow failures
/// from type checks but not build script executions.
/// A plain old `cargo doc` would just die if a build script execution fails,
/// there is no reason for `-Zrustdoc-scrape-examples` to keep going.
///
/// [`Docscrape`]: crate::core::compiler::CompileMode::Docscrape
/// [`RunCustomBuild`]: crate::core::compiler::CompileMode::Docscrape
pub fn scrape_units_have_dep_on<'a>(&'a self, unit: &'a Unit) -> Vec<&'a Unit> {
self.scrape_units
.iter()
.filter(|scrape_unit| {
self.unit_graph[scrape_unit]
.iter()
.any(|dep| &dep.unit == unit && !dep.unit.mode.is_run_custom_build())
})
.collect()
}
/// Returns true if this unit is needed for doing doc-scraping and is also
/// allowed to fail without killing the build.
pub fn unit_can_fail_for_docscraping(&self, unit: &Unit) -> bool {
// If the unit is not a Docscrape unit, e.g. a Lib target that is
// checked to scrape an Example target, then we need to get the doc-scrape-examples
// configuration for the reverse-dependent Example target.
let for_scrape_units = if unit.mode.is_doc_scrape() {
vec![unit]
} else {
self.scrape_units_have_dep_on(unit)
};
if for_scrape_units.is_empty() {
false
} else {
// All Docscrape units must have doc-scrape-examples unset. If any are true,
// then the unit is not allowed to fail.
for_scrape_units
.iter()
.all(|unit| unit.target.doc_scrape_examples().is_unset())
}
}
}