Skip to content

Add support for rustdoc root URL mappings. #8287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 30, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci/azure-install-rust.yml
Original file line number Diff line number Diff line change
@@ -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
22 changes: 21 additions & 1 deletion src/cargo/core/compiler/fingerprint.rs
Original file line number Diff line number Diff line change
@@ -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,
2 changes: 2 additions & 0 deletions src/cargo/core/compiler/mod.rs
Original file line number Diff line number Diff line change
@@ -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));

172 changes: 172 additions & 0 deletions src/cargo/core/compiler/rustdoc.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
2 changes: 2 additions & 0 deletions src/cargo/core/features.rs
Original file line number Diff line number Diff line change
@@ -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),
}

32 changes: 24 additions & 8 deletions src/cargo/util/config/mod.rs
Original file line number Diff line number Diff line change
@@ -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)
41 changes: 41 additions & 0 deletions src/doc/src/reference/unstable.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 11 additions & 1 deletion tests/testsuite/alt_registry.rs
Original file line number Diff line number Diff line change
@@ -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
",
1 change: 1 addition & 0 deletions tests/testsuite/main.rs
Original file line number Diff line number Diff line change
@@ -98,6 +98,7 @@ mod run;
mod rustc;
mod rustc_info_cache;
mod rustdoc;
mod rustdoc_extern_html;
mod rustdocflags;
mod rustflags;
mod search;
375 changes: 375 additions & 0 deletions tests/testsuite/rustdoc_extern_html.rs
Original file line number Diff line number Diff line change
@@ -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();
}