Add support for rustdoc root URL mappings.

This commit is contained in:
Eric Huss 2020-05-26 10:44:57 -07:00
parent ff9126d0d2
commit e0f9643b0f
8 changed files with 611 additions and 1 deletions

View File

@ -73,6 +73,7 @@
//! mtime of sources | ✓[^3] |
//! RUSTFLAGS/RUSTDOCFLAGS | ✓ |
//! LTO flags | ✓ |
//! config settings[^5] | ✓ |
//! is_std | | ✓
//!
//! [^1]: Build script and bin dependencies are not included.
@ -82,6 +83,9 @@
//! [^4]: `__CARGO_DEFAULT_LIB_METADATA` is set by rustbuild to embed the
//! release channel (bootstrap/stable/beta/nightly) in libstd.
//!
//! [^5]: Config settings that are not otherwise captured anywhere else.
//! Currently, this is only `doc.extern-map`.
//!
//! When deciding what should go in the Metadata vs the Fingerprint, consider
//! that some files (like dylibs) do not have a hash in their filename. Thus,
//! if a value changes, only the fingerprint will detect the change (consider,
@ -533,6 +537,8 @@ pub struct Fingerprint {
/// "description", which are exposed as environment variables during
/// compilation.
metadata: u64,
/// Hash of various config settings that change how things are compiled.
config: u64,
/// Description of whether the filesystem status for this unit is up to date
/// or should be considered stale.
#[serde(skip)]
@ -746,6 +752,7 @@ impl Fingerprint {
memoized_hash: Mutex::new(None),
rustflags: Vec::new(),
metadata: 0,
config: 0,
fs_status: FsStatus::Stale,
outputs: Vec::new(),
}
@ -806,6 +813,9 @@ impl Fingerprint {
if self.metadata != old.metadata {
bail!("metadata changed")
}
if self.config != old.config {
bail!("configuration settings have changed")
}
let my_local = self.local.lock().unwrap();
let old_local = old.local.lock().unwrap();
if my_local.len() != old_local.len() {
@ -1040,12 +1050,13 @@ impl hash::Hash for Fingerprint {
ref deps,
ref local,
metadata,
config,
ref rustflags,
..
} = *self;
let local = local.lock().unwrap();
(
rustc, features, target, path, profile, &*local, metadata, rustflags,
rustc, features, target, path, profile, &*local, metadata, config, rustflags,
)
.hash(h);
@ -1252,6 +1263,14 @@ fn calculate_normal(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Finger
// Include metadata since it is exposed as environment variables.
let m = unit.pkg.manifest().metadata();
let metadata = util::hash_u64((&m.authors, &m.description, &m.homepage, &m.repository));
let config = if unit.mode.is_doc() && cx.bcx.config.cli_unstable().rustdoc_map {
cx.bcx
.config
.doc_extern_map()
.map_or(0, |map| util::hash_u64(map))
} else {
0
};
Ok(Fingerprint {
rustc: util::hash_u64(&cx.bcx.rustc().verbose_version),
target: util::hash_u64(&unit.target),
@ -1264,6 +1283,7 @@ fn calculate_normal(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Finger
local: Mutex::new(local),
memoized_hash: Mutex::new(None),
metadata,
config,
rustflags: extra_flags,
fs_status: FsStatus::Stale,
outputs,

View File

@ -13,6 +13,7 @@ mod layout;
mod links;
mod lto;
mod output_depinfo;
pub mod rustdoc;
pub mod standard_lib;
mod timings;
mod unit;
@ -570,6 +571,7 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
}
build_deps_args(&mut rustdoc, cx, unit)?;
rustdoc::add_root_urls(cx, unit, &mut rustdoc)?;
rustdoc.args(bcx.rustdocflags_args(unit));

View File

@ -0,0 +1,172 @@
//! Utilities for building with rustdoc.
use crate::core::compiler::context::Context;
use crate::core::compiler::unit::Unit;
use crate::core::compiler::CompileKind;
use crate::sources::CRATES_IO_REGISTRY;
use crate::util::errors::{internal, CargoResult};
use crate::util::ProcessBuilder;
use std::collections::HashMap;
use std::fmt;
use std::hash;
use url::Url;
/// Mode used for `std`.
#[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())
}
}
#[derive(serde::Deserialize, Debug)]
pub struct RustdocExternMap {
registries: HashMap<String, String>,
std: Option<RustdocExternMode>,
}
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);
}
}
}
pub fn add_root_urls(
cx: &Context<'_, '_>,
unit: &Unit,
rustdoc: &mut ProcessBuilder,
) -> CargoResult<()> {
let config = cx.bcx.config;
if !config.cli_unstable().rustdoc_map {
log::debug!("`doc.extern-map` ignored, requires -Zrustdoc-map flag");
return Ok(());
}
let map = config.doc_extern_map()?;
if map.registries.len() == 0 && map.std.is_none() {
// Skip doing unnecessary work.
return Ok(());
}
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) {
return Some((name, index_url));
} else {
log::warn!(
"`doc.extern-map.{}` specifies a registry that is not defined",
name
);
return 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_default_registry() {
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 {
log::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(())
}

View File

@ -356,6 +356,7 @@ pub struct CliUnstable {
pub crate_versions: bool,
pub separate_nightlies: bool,
pub multitarget: bool,
pub rustdoc_map: bool,
}
impl CliUnstable {
@ -435,6 +436,7 @@ impl CliUnstable {
"crate-versions" => self.crate_versions = parse_empty(k, v)?,
"separate-nightlies" => self.separate_nightlies = parse_empty(k, v)?,
"multitarget" => self.multitarget = parse_empty(k, v)?,
"rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?,
_ => bail!("unknown `-Z` flag specified: {}", k),
}

View File

@ -70,6 +70,7 @@ use serde::Deserialize;
use url::Url;
use self::ConfigValue as CV;
use crate::core::compiler::rustdoc::RustdocExternMap;
use crate::core::shell::Verbosity;
use crate::core::{nightly_features_allowed, CliUnstable, Shell, SourceId, Workspace};
use crate::ops;
@ -172,6 +173,7 @@ pub struct Config {
net_config: LazyCell<CargoNetConfig>,
build_config: LazyCell<CargoBuildConfig>,
target_cfgs: LazyCell<Vec<(String, TargetCfgConfig)>>,
doc_extern_map: LazyCell<RustdocExternMap>,
}
impl Config {
@ -241,6 +243,7 @@ impl Config {
net_config: LazyCell::new(),
build_config: LazyCell::new(),
target_cfgs: LazyCell::new(),
doc_extern_map: LazyCell::new(),
}
}
@ -1159,6 +1162,14 @@ impl Config {
.try_borrow_with(|| target::load_target_cfgs(self))
}
pub fn doc_extern_map(&self) -> CargoResult<&RustdocExternMap> {
// Note: This does not support environment variables. The `Unit`
// fundamentally does not have access to the registry name, so there is
// nothing to query. Plumbing the name into SourceId is quite challenging.
self.doc_extern_map
.try_borrow_with(|| self.get::<RustdocExternMap>("doc.extern-map"))
}
/// Returns the `[target]` table definition for the given target triple.
pub fn target_cfg_triple(&self, target: &str) -> CargoResult<TargetConfig> {
target::load_target_triple(self, target)

View File

@ -785,3 +785,44 @@ strip = "debuginfo"
Other possible values of `strip` are `none` and `symbols`. The default is
`none`.
### rustdoc-map
* Tracking Issue: TODO
This feature adds configuration settings that are passed to `rustdoc` so that
it can generate links to dependencies whose documentation is hosted elsewhere
when the dependency is not documented. First, add this to `.cargo/config`:
```toml
[doc.extern-map.registries]
crates-io = "https://docs.rs/"
```
Then, when building documentation, use the following flags to cause links
to dependencies to link to [docs.rs](https://docs.rs/):
```
cargo +nightly doc --no-deps -Zrustdoc-map
```
The `registries` table contains a mapping of registry name to the URL to link
to. The URL may have the markers `{pkg_name}` and `{version}` which will get
replaced with the corresponding values. If neither are specified, then Cargo
defaults to appending `{pkg_name}/{version}/` to the end of the URL.
Another config setting is available to redirect standard library links. By
default, rustdoc creates links to <https://doc.rust-lang.org/nightly/>. To
change this behavior, use the `doc.extern-map.std` setting:
```toml
[doc.extern-map]
std = "local"
```
A value of `"local"` means to link to the documentation found in the `rustc`
sysroot. If you are using rustup, this documentation can be installed with
`rustup component add rust-docs`.
The default value is `"remote"`.
The value may also take a URL for a custom location.

View File

@ -98,6 +98,7 @@ mod run;
mod rustc;
mod rustc_info_cache;
mod rustdoc;
mod rustdoc_extern_html;
mod rustdocflags;
mod rustflags;
mod search;

View File

@ -0,0 +1,361 @@
//! Tests for the -Zrustdoc-map feature.
use cargo_test_support::registry::Package;
use cargo_test_support::{is_nightly, project, Project};
fn basic_project() -> Project {
Package::new("bar", "1.0.0")
.file("src/lib.rs", "pub struct Straw;")
.publish();
project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[dependencies]
bar = "1.0"
"#,
)
.file(
"src/lib.rs",
r#"
pub fn myfun() -> Option<bar::Straw> {
None
}
"#,
)
.build()
}
fn docs_rs(p: &Project) {
p.change_file(
".cargo/config",
r#"
[doc.extern-map.registries]
crates-io = "https://docs.rs/"
"#,
);
}
#[cargo_test]
fn ignores_on_stable() {
// Requires -Zrustdoc-map to use.
let p = basic_project();
docs_rs(&p);
p.cargo("doc -v --no-deps")
.with_stderr_does_not_contain("[..]--extern-html-root-url[..]")
.run();
}
#[cargo_test]
fn simple() {
// Basic test that it works with crates.io.
if !is_nightly() {
// --extern-html-root-url is unstable
return;
}
let p = basic_project();
docs_rs(&p);
p.cargo("doc -v --no-deps -Zrustdoc-map")
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[RUNNING] `rustdoc [..]--crate-name foo [..]bar=https://docs.rs/bar/1.0.0/[..]",
)
.run();
let myfun = p.read_file("target/doc/foo/fn.myfun.html");
assert!(myfun.contains(r#"href="https://docs.rs/bar/1.0.0/bar/struct.Straw.html""#));
}
#[cargo_test]
fn std_docs() {
// Mapping std docs somewhere else.
if !is_nightly() {
// --extern-html-root-url is unstable
return;
}
let p = basic_project();
p.change_file(
".cargo/config",
r#"
[doc.extern-map]
std = "local"
"#,
);
p.cargo("doc -v --no-deps -Zrustdoc-map")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[RUNNING] `rustdoc [..]--crate-name foo [..]std=file://[..]")
.run();
let myfun = p.read_file("target/doc/foo/fn.myfun.html");
assert!(myfun.contains(r#"share/doc/rust/html/core/option/enum.Option.html""#));
p.change_file(
".cargo/config",
r#"
[doc.extern-map]
std = "https://example.com/rust/"
"#,
);
p.cargo("doc -v --no-deps -Zrustdoc-map")
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[RUNNING] `rustdoc [..]--crate-name foo [..]std=https://example.com/rust/[..]",
)
.run();
let myfun = p.read_file("target/doc/foo/fn.myfun.html");
assert!(myfun.contains(r#"href="https://example.com/rust/core/option/enum.Option.html""#));
}
#[cargo_test]
fn renamed_dep() {
// Handles renamed dependencies.
if !is_nightly() {
// --extern-html-root-url is unstable
return;
}
Package::new("bar", "1.0.0")
.file("src/lib.rs", "pub struct Straw;")
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[dependencies]
groovy = { version = "1.0", package = "bar" }
"#,
)
.file(
"src/lib.rs",
r#"
pub fn myfun() -> Option<groovy::Straw> {
None
}
"#,
)
.build();
docs_rs(&p);
p.cargo("doc -v --no-deps -Zrustdoc-map")
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[RUNNING] `rustdoc [..]--crate-name foo [..]bar=https://docs.rs/bar/1.0.0/[..]",
)
.run();
let myfun = p.read_file("target/doc/foo/fn.myfun.html");
assert!(myfun.contains(r#"href="https://docs.rs/bar/1.0.0/bar/struct.Straw.html""#));
}
#[cargo_test]
fn lib_name() {
// Handles lib name != package name.
if !is_nightly() {
// --extern-html-root-url is unstable
return;
}
Package::new("bar", "1.0.0")
.file(
"Cargo.toml",
r#"
[package]
name = "bar"
version = "1.0.0"
[lib]
name = "rumpelstiltskin"
"#,
)
.file("src/lib.rs", "pub struct Straw;")
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "1.0"
"#,
)
.file(
"src/lib.rs",
r#"
pub fn myfun() -> Option<rumpelstiltskin::Straw> {
None
}
"#,
)
.build();
docs_rs(&p);
p.cargo("doc -v --no-deps -Zrustdoc-map")
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[RUNNING] `rustdoc [..]--crate-name foo [..]rumpelstiltskin=https://docs.rs/bar/1.0.0/[..]",
)
.run();
let myfun = p.read_file("target/doc/foo/fn.myfun.html");
assert!(myfun.contains(r#"href="https://docs.rs/bar/1.0.0/rumpelstiltskin/struct.Straw.html""#));
}
#[cargo_test]
fn alt_registry() {
// Supports other registry names.
if !is_nightly() {
// --extern-html-root-url is unstable
return;
}
Package::new("bar", "1.0.0")
.alternative(true)
.file(
"src/lib.rs",
r#"
extern crate baz;
pub struct Queen;
pub use baz::King;
"#,
)
.registry_dep("baz", "1.0")
.publish();
Package::new("baz", "1.0.0")
.alternative(true)
.file("src/lib.rs", "pub struct King;")
.publish();
Package::new("grimm", "1.0.0")
.file("src/lib.rs", "pub struct Gold;")
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[dependencies]
bar = { version = "1.0", registry="alternative" }
grimm = "1.0"
"#,
)
.file(
"src/lib.rs",
r#"
pub fn queen() -> bar::Queen { bar::Queen }
pub fn king() -> bar::King { bar::King }
pub fn gold() -> grimm::Gold { grimm::Gold }
"#,
)
.file(
".cargo/config",
r#"
[doc.extern-map.registries]
alternative = "https://example.com/{pkg_name}/{version}/"
crates-io = "https://docs.rs/"
"#,
)
.build();
p.cargo("doc -v --no-deps -Zrustdoc-map")
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[RUNNING] `rustdoc [..]--crate-name foo \
[..]bar=https://example.com/bar/1.0.0/[..]grimm=https://docs.rs/grimm/1.0.0/[..]",
)
.run();
let queen = p.read_file("target/doc/foo/fn.queen.html");
assert!(queen.contains(r#"href="https://example.com/bar/1.0.0/bar/struct.Queen.html""#));
// The king example fails to link. Rustdoc seems to want the origin crate
// name (baz) for re-exports. There are many issues in the issue tracker
// for rustdoc re-exports, so I'm not sure, but I think this is maybe a
// rustdoc issue. Alternatively, Cargo could provide mappings for all
// transitive dependencies to fix this.
let king = p.read_file("target/doc/foo/fn.king.html");
assert!(king.contains(r#"-&gt; King"#));
let gold = p.read_file("target/doc/foo/fn.gold.html");
assert!(gold.contains(r#"href="https://docs.rs/grimm/1.0.0/grimm/struct.Gold.html""#));
}
#[cargo_test]
fn multiple_versions() {
// What happens when there are multiple versions.
// NOTE: This is currently broken behavior. Rustdoc does not provide a way
// to match renamed dependencies.
if !is_nightly() {
// --extern-html-root-url is unstable
return;
}
Package::new("bar", "1.0.0")
.file("src/lib.rs", "pub struct Spin;")
.publish();
Package::new("bar", "2.0.0")
.file("src/lib.rs", "pub struct Straw;")
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[dependencies]
bar = "1.0"
bar2 = {version="2.0", package="bar"}
"#,
)
.file(
"src/lib.rs",
"
pub fn fn1() -> bar::Spin {bar::Spin}
pub fn fn2() -> bar2::Straw {bar2::Straw}
",
)
.build();
docs_rs(&p);
p.cargo("doc -v --no-deps -Zrustdoc-map")
.masquerade_as_nightly_cargo()
.with_stderr_contains(
"[RUNNING] `rustdoc [..]--crate-name foo \
[..]bar=https://docs.rs/bar/1.0.0/[..]bar=https://docs.rs/bar/2.0.0/[..]",
)
.run();
let fn1 = p.read_file("target/doc/foo/fn.fn1.html");
// This should be 1.0.0, rustdoc seems to use the last entry when there
// are duplicates.
assert!(fn1.contains(r#"href="https://docs.rs/bar/2.0.0/bar/struct.Spin.html""#));
let fn2 = p.read_file("target/doc/foo/fn.fn2.html");
assert!(fn2.contains(r#"href="https://docs.rs/bar/2.0.0/bar/struct.Straw.html""#));
}
#[cargo_test]
fn rebuilds_when_changing() {
// Make sure it rebuilds if the map changes.
if !is_nightly() {
// --extern-html-root-url is unstable
return;
}
let p = basic_project();
p.cargo("doc -v --no-deps -Zrustdoc-map")
.masquerade_as_nightly_cargo()
.with_stderr_does_not_contain("[..]--extern-html-root-url[..]")
.run();
docs_rs(&p);
p.cargo("doc -v --no-deps -Zrustdoc-map")
.masquerade_as_nightly_cargo()
.with_stderr_contains("[..]--extern-html-root-url[..]")
.run();
}