diff --git a/ci/azure-install-rust.yml b/ci/azure-install-rust.yml index cc807d2011b..6f26e42f4d9 100644 --- a/ci/azure-install-rust.yml +++ b/ci/azure-install-rust.yml @@ -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 diff --git a/src/cargo/core/compiler/fingerprint.rs b/src/cargo/core/compiler/fingerprint.rs index b432839850a..4e554202ec0 100644 --- a/src/cargo/core/compiler/fingerprint.rs +++ b/src/cargo/core/compiler/fingerprint.rs @@ -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, unit: &Unit) -> CargoResult, unit: &Unit) -> CargoResult { } build_deps_args(&mut rustdoc, cx, unit)?; + rustdoc::add_root_urls(cx, unit, &mut rustdoc)?; rustdoc.args(bcx.rustdocflags_args(unit)); diff --git a/src/cargo/core/compiler/rustdoc.rs b/src/cargo/core/compiler/rustdoc.rs new file mode 100644 index 00000000000..ad0524a2fbc --- /dev/null +++ b/src/cargo/core/compiler/rustdoc.rs @@ -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 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(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(s.into()) + } +} + +#[derive(serde::Deserialize, Debug)] +pub struct RustdocExternMap { + registries: HashMap, + std: Option, +} + +impl hash::Hash for RustdocExternMap { + fn hash(&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(()) +} diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 6a05b0f091f..8ed33b2f615 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -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), } diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index 603f2581f95..bcc776d6c91 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -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, build_config: LazyCell, target_cfgs: LazyCell>, + doc_extern_map: LazyCell, } 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 { 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) -> CargoResult { + fn resolve_registry_index(&self, index: &Value) -> CargoResult { + // 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::("doc.extern-map")) + } + /// Returns the `[target]` table definition for the given target triple. pub fn target_cfg_triple(&self, target: &str) -> CargoResult { target::load_target_triple(self, target) diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 5ade6453fcb..658231e3541 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -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 . 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. diff --git a/tests/testsuite/alt_registry.rs b/tests/testsuite/alt_registry.rs index f9f5b04fc35..cbbab9f69b3 100644 --- a/tests/testsuite/alt_registry.rs +++ b/tests/testsuite/alt_registry.rs @@ -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 ", diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index fe337440aed..7aec3227b40 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -98,6 +98,7 @@ mod run; mod rustc; mod rustc_info_cache; mod rustdoc; +mod rustdoc_extern_html; mod rustdocflags; mod rustflags; mod search; diff --git a/tests/testsuite/rustdoc_extern_html.rs b/tests/testsuite/rustdoc_extern_html.rs new file mode 100644 index 00000000000..368794785fd --- /dev/null +++ b/tests/testsuite/rustdoc_extern_html.rs @@ -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 { + 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 { + 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 { + 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#"-> 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(); +}