Auto merge of #8287 - ehuss:rustdoc-map, r=alexcrichton

Add support for rustdoc root URL mappings.

This adds an experimental configuration setting to allow Cargo to pass the `--extern-html-root-url` flag to rustdoc. This flag allows rustdoc to link to other locations when a dependency is not locally documented. See the documentation in `unstable.md` for more details.

There are some known issues with this implementation:

* Rustdoc doesn't seem to know much about renamed dependencies. The links it generates are to the package name, not the renamed name. The code is written to pass in package names, but if there are multiple dependencies to the same package, it won't work properly.

* Similarly, if there are multiple versions of the same package within the dep graph, rustdoc will only link to one of them. To fix this, Cargo would need to pass metadata info into rustdoc (such as the package version).

* If a dependency is built with different features than what is on docs.rs, some links may break.

* This explodes the command-line length significantly. Before stabilizing, we may want to consider addressing that. I'm not sure if it would make sense to change rustdoc's interface, or to use response files?

* This does not pass mappings for transitive dependencies. This normally isn't an issue, but can arise for re-exports (see the `alt_registry` test for an example). I'm not sure if this is a bug in rustdoc or not (there is a large number of issues regarding reexports and rustdoc). Cargo could include these, but this would make the command-line length even longer. Not sure what to do here.

* The config value does not support environment variables. This would be very difficult to support, because Cargo doesn't retain the registry name in `SourceId`. I looked into fixing that, but it is very difficult, and hard to make it reliable.

I have tried to consider future changes in this design, to ensure it doesn't make them more difficult:

* Single-tab browsing. This would be a mode where the std docs are merged with the local crate's docs so that the std docs are shown in the same place (and included in the index). This could be expressed with something like `doc.extern-map.std = "include"` or something like that.  (Or maybe just use build-std?)

* Direct-dependencies only. Often transitive dependencies aren't that interesting, and take up a lot of space in the output, and clog the search index. Some users want the ability to (locally) document their package + direct dependencies only. I think this could be implemented with some kind of command-line flag, perhaps with a config setting in the `[doc]` table. `--extern-html-root-url` flag will automatically handle second-level dependencies.

* Manual-exclusions. Sometimes there are specific dependencies that are very expensive to document locally, but you still want everything else. I think this could be implemented with a command-line flag (`--exclude winapi`?), and the rustdoc-map feature would automatically link those excluded crates' items to docs.rs.  This could also be added to the `[doc]` table.

We can also consider at any time to change the defaults (such as making `crates-io = "https://docs.rs"` the default). It could also potentially auto-detect `std = "local"`, although rustdoc could do the same internally.

Closes #6279
This commit is contained in:
bors 2020-05-30 17:08:19 +00:00
commit 00fe8a5d25
10 changed files with 650 additions and 11 deletions

View File

@ -5,7 +5,7 @@ steps:
rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed"
rustup update --no-self-update $TOOLCHAIN
if [[ "$TOOLCHAIN" == "nightly"* ]]; then
rustup component add --toolchain=$TOOLCHAIN rustc-dev llvm-tools-preview
rustup component add --toolchain=$TOOLCHAIN rustc-dev llvm-tools-preview rust-docs
fi
rustup default $TOOLCHAIN
displayName: Install rust

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(),
}
}
@ -1008,12 +1011,16 @@ impl Config {
/// Gets the index for a registry.
pub fn get_registry_index(&self, registry: &str) -> CargoResult<Url> {
validate_package_name(registry, "registry name", "")?;
Ok(
match self.get_string(&format!("registries.{}.index", registry))? {
Some(index) => self.resolve_registry_index(index)?,
None => bail!("No index found for registry: `{}`", registry),
},
)
if let Some(index) = self.get_string(&format!("registries.{}.index", registry))? {
self.resolve_registry_index(&index).chain_err(|| {
format!(
"invalid index URL for registry `{}` defined in {}",
registry, index.definition
)
})
} else {
bail!("no index found for registry: `{}`", registry);
}
}
/// Returns an error if `registry.index` is set.
@ -1027,7 +1034,8 @@ impl Config {
Ok(())
}
fn resolve_registry_index(&self, index: Value<String>) -> CargoResult<Url> {
fn resolve_registry_index(&self, index: &Value<String>) -> CargoResult<Url> {
// This handles relative file: URLs, relative to the config definition.
let base = index
.definition
.root(self)
@ -1036,7 +1044,7 @@ impl Config {
let _parsed = index.val.into_url()?;
let url = index.val.into_url_with_base(Some(&*base))?;
if url.password().is_some() {
bail!("Registry URLs may not contain passwords");
bail!("registry URLs may not contain passwords");
}
Ok(url)
}
@ -1154,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: [#8296](https://github.com/rust-lang/cargo/issues/8296)
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

@ -548,7 +548,14 @@ fn passwords_in_registries_index_url_forbidden() {
p.cargo("publish --registry alternative")
.with_status(101)
.with_stderr_contains("error: Registry URLs may not contain passwords")
.with_stderr(
"\
error: invalid index URL for registry `alternative` defined in [..]/home/.cargo/config
Caused by:
registry URLs may not contain passwords
",
)
.run();
}
@ -1240,6 +1247,9 @@ fn registries_index_relative_path_not_allowed() {
"\
error: failed to parse manifest at `{root}/foo/Cargo.toml`
Caused by:
invalid index URL for registry `relative` defined in [..]/.cargo/config
Caused by:
invalid url `alternative-registry`: relative URL without a base
",

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,375 @@
//! Tests for the -Zrustdoc-map feature.
use cargo_test_support::registry::Package;
use cargo_test_support::{is_nightly, paths, 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;
}
// For local developers, skip this test if docs aren't installed.
let docs = std::path::Path::new(&paths::sysroot()).join("share/doc/rust/html");
if !docs.exists() {
if cargo::util::is_ci() {
panic!("std docs are not installed, check that the rust-docs component is installed");
} else {
eprintln!(
"documentation not found at {}, \
skipping test (run `rustdoc component add rust-docs` to install",
docs.display()
);
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();
}