Skip to content

Commit e0f9643

Browse files
committed
Add support for rustdoc root URL mappings.
1 parent ff9126d commit e0f9643

File tree

8 files changed

+611
-1
lines changed

8 files changed

+611
-1
lines changed

src/cargo/core/compiler/fingerprint.rs

+21-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
//! mtime of sources | ✓[^3] |
7474
//! RUSTFLAGS/RUSTDOCFLAGS | ✓ |
7575
//! LTO flags | ✓ |
76+
//! config settings[^5] | ✓ |
7677
//! is_std | | ✓
7778
//!
7879
//! [^1]: Build script and bin dependencies are not included.
@@ -82,6 +83,9 @@
8283
//! [^4]: `__CARGO_DEFAULT_LIB_METADATA` is set by rustbuild to embed the
8384
//! release channel (bootstrap/stable/beta/nightly) in libstd.
8485
//!
86+
//! [^5]: Config settings that are not otherwise captured anywhere else.
87+
//! Currently, this is only `doc.extern-map`.
88+
//!
8589
//! When deciding what should go in the Metadata vs the Fingerprint, consider
8690
//! that some files (like dylibs) do not have a hash in their filename. Thus,
8791
//! if a value changes, only the fingerprint will detect the change (consider,
@@ -533,6 +537,8 @@ pub struct Fingerprint {
533537
/// "description", which are exposed as environment variables during
534538
/// compilation.
535539
metadata: u64,
540+
/// Hash of various config settings that change how things are compiled.
541+
config: u64,
536542
/// Description of whether the filesystem status for this unit is up to date
537543
/// or should be considered stale.
538544
#[serde(skip)]
@@ -746,6 +752,7 @@ impl Fingerprint {
746752
memoized_hash: Mutex::new(None),
747753
rustflags: Vec::new(),
748754
metadata: 0,
755+
config: 0,
749756
fs_status: FsStatus::Stale,
750757
outputs: Vec::new(),
751758
}
@@ -806,6 +813,9 @@ impl Fingerprint {
806813
if self.metadata != old.metadata {
807814
bail!("metadata changed")
808815
}
816+
if self.config != old.config {
817+
bail!("configuration settings have changed")
818+
}
809819
let my_local = self.local.lock().unwrap();
810820
let old_local = old.local.lock().unwrap();
811821
if my_local.len() != old_local.len() {
@@ -1040,12 +1050,13 @@ impl hash::Hash for Fingerprint {
10401050
ref deps,
10411051
ref local,
10421052
metadata,
1053+
config,
10431054
ref rustflags,
10441055
..
10451056
} = *self;
10461057
let local = local.lock().unwrap();
10471058
(
1048-
rustc, features, target, path, profile, &*local, metadata, rustflags,
1059+
rustc, features, target, path, profile, &*local, metadata, config, rustflags,
10491060
)
10501061
.hash(h);
10511062

@@ -1252,6 +1263,14 @@ fn calculate_normal(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Finger
12521263
// Include metadata since it is exposed as environment variables.
12531264
let m = unit.pkg.manifest().metadata();
12541265
let metadata = util::hash_u64((&m.authors, &m.description, &m.homepage, &m.repository));
1266+
let config = if unit.mode.is_doc() && cx.bcx.config.cli_unstable().rustdoc_map {
1267+
cx.bcx
1268+
.config
1269+
.doc_extern_map()
1270+
.map_or(0, |map| util::hash_u64(map))
1271+
} else {
1272+
0
1273+
};
12551274
Ok(Fingerprint {
12561275
rustc: util::hash_u64(&cx.bcx.rustc().verbose_version),
12571276
target: util::hash_u64(&unit.target),
@@ -1264,6 +1283,7 @@ fn calculate_normal(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Finger
12641283
local: Mutex::new(local),
12651284
memoized_hash: Mutex::new(None),
12661285
metadata,
1286+
config,
12671287
rustflags: extra_flags,
12681288
fs_status: FsStatus::Stale,
12691289
outputs,

src/cargo/core/compiler/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod layout;
1313
mod links;
1414
mod lto;
1515
mod output_depinfo;
16+
pub mod rustdoc;
1617
pub mod standard_lib;
1718
mod timings;
1819
mod unit;
@@ -570,6 +571,7 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
570571
}
571572

572573
build_deps_args(&mut rustdoc, cx, unit)?;
574+
rustdoc::add_root_urls(cx, unit, &mut rustdoc)?;
573575

574576
rustdoc.args(bcx.rustdocflags_args(unit));
575577

src/cargo/core/compiler/rustdoc.rs

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
//! Utilities for building with rustdoc.
2+
3+
use crate::core::compiler::context::Context;
4+
use crate::core::compiler::unit::Unit;
5+
use crate::core::compiler::CompileKind;
6+
use crate::sources::CRATES_IO_REGISTRY;
7+
use crate::util::errors::{internal, CargoResult};
8+
use crate::util::ProcessBuilder;
9+
use std::collections::HashMap;
10+
use std::fmt;
11+
use std::hash;
12+
use url::Url;
13+
14+
/// Mode used for `std`.
15+
#[derive(Debug, Hash)]
16+
pub enum RustdocExternMode {
17+
/// Use a local `file://` URL.
18+
Local,
19+
/// Use a remote URL to https://doc.rust-lang.org/ (default).
20+
Remote,
21+
/// An arbitrary URL.
22+
Url(String),
23+
}
24+
25+
impl From<String> for RustdocExternMode {
26+
fn from(s: String) -> RustdocExternMode {
27+
match s.as_ref() {
28+
"local" => RustdocExternMode::Local,
29+
"remote" => RustdocExternMode::Remote,
30+
_ => RustdocExternMode::Url(s),
31+
}
32+
}
33+
}
34+
35+
impl fmt::Display for RustdocExternMode {
36+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37+
match self {
38+
RustdocExternMode::Local => "local".fmt(f),
39+
RustdocExternMode::Remote => "remote".fmt(f),
40+
RustdocExternMode::Url(s) => s.fmt(f),
41+
}
42+
}
43+
}
44+
45+
impl<'de> serde::de::Deserialize<'de> for RustdocExternMode {
46+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
47+
where
48+
D: serde::de::Deserializer<'de>,
49+
{
50+
let s = String::deserialize(deserializer)?;
51+
Ok(s.into())
52+
}
53+
}
54+
55+
#[derive(serde::Deserialize, Debug)]
56+
pub struct RustdocExternMap {
57+
registries: HashMap<String, String>,
58+
std: Option<RustdocExternMode>,
59+
}
60+
61+
impl hash::Hash for RustdocExternMap {
62+
fn hash<H: hash::Hasher>(&self, into: &mut H) {
63+
self.std.hash(into);
64+
for (key, value) in &self.registries {
65+
key.hash(into);
66+
value.hash(into);
67+
}
68+
}
69+
}
70+
71+
pub fn add_root_urls(
72+
cx: &Context<'_, '_>,
73+
unit: &Unit,
74+
rustdoc: &mut ProcessBuilder,
75+
) -> CargoResult<()> {
76+
let config = cx.bcx.config;
77+
if !config.cli_unstable().rustdoc_map {
78+
log::debug!("`doc.extern-map` ignored, requires -Zrustdoc-map flag");
79+
return Ok(());
80+
}
81+
let map = config.doc_extern_map()?;
82+
if map.registries.len() == 0 && map.std.is_none() {
83+
// Skip doing unnecessary work.
84+
return Ok(());
85+
}
86+
let mut unstable_opts = false;
87+
// Collect mapping of registry name -> index url.
88+
let name2url: HashMap<&String, Url> = map
89+
.registries
90+
.keys()
91+
.filter_map(|name| {
92+
if let Ok(index_url) = config.get_registry_index(name) {
93+
return Some((name, index_url));
94+
} else {
95+
log::warn!(
96+
"`doc.extern-map.{}` specifies a registry that is not defined",
97+
name
98+
);
99+
return None;
100+
}
101+
})
102+
.collect();
103+
for dep in cx.unit_deps(unit) {
104+
if dep.unit.target.is_linkable() && !dep.unit.mode.is_doc() {
105+
for (registry, location) in &map.registries {
106+
let sid = dep.unit.pkg.package_id().source_id();
107+
let matches_registry = || -> bool {
108+
if !sid.is_registry() {
109+
return false;
110+
}
111+
if sid.is_default_registry() {
112+
return registry == CRATES_IO_REGISTRY;
113+
}
114+
if let Some(index_url) = name2url.get(registry) {
115+
return index_url == sid.url();
116+
}
117+
false
118+
};
119+
if matches_registry() {
120+
let mut url = location.clone();
121+
if !url.contains("{pkg_name}") && !url.contains("{version}") {
122+
if !url.ends_with('/') {
123+
url.push('/');
124+
}
125+
url.push_str("{pkg_name}/{version}/");
126+
}
127+
let url = url
128+
.replace("{pkg_name}", &dep.unit.pkg.name())
129+
.replace("{version}", &dep.unit.pkg.version().to_string());
130+
rustdoc.arg("--extern-html-root-url");
131+
rustdoc.arg(format!("{}={}", dep.unit.target.crate_name(), url));
132+
unstable_opts = true;
133+
}
134+
}
135+
}
136+
}
137+
let std_url = match &map.std {
138+
None | Some(RustdocExternMode::Remote) => None,
139+
Some(RustdocExternMode::Local) => {
140+
let sysroot = &cx.bcx.target_data.info(CompileKind::Host).sysroot;
141+
let html_root = sysroot.join("share").join("doc").join("rust").join("html");
142+
if html_root.exists() {
143+
let url = Url::from_file_path(&html_root).map_err(|()| {
144+
internal(format!(
145+
"`{}` failed to convert to URL",
146+
html_root.display()
147+
))
148+
})?;
149+
Some(url.to_string())
150+
} else {
151+
log::warn!(
152+
"`doc.extern-map.std` is \"local\", but local docs don't appear to exist at {}",
153+
html_root.display()
154+
);
155+
None
156+
}
157+
}
158+
Some(RustdocExternMode::Url(s)) => Some(s.to_string()),
159+
};
160+
if let Some(url) = std_url {
161+
for name in &["std", "core", "alloc", "proc_macro"] {
162+
rustdoc.arg("--extern-html-root-url");
163+
rustdoc.arg(format!("{}={}", name, url));
164+
unstable_opts = true;
165+
}
166+
}
167+
168+
if unstable_opts {
169+
rustdoc.arg("-Zunstable-options");
170+
}
171+
Ok(())
172+
}

src/cargo/core/features.rs

+2
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ pub struct CliUnstable {
356356
pub crate_versions: bool,
357357
pub separate_nightlies: bool,
358358
pub multitarget: bool,
359+
pub rustdoc_map: bool,
359360
}
360361

361362
impl CliUnstable {
@@ -435,6 +436,7 @@ impl CliUnstable {
435436
"crate-versions" => self.crate_versions = parse_empty(k, v)?,
436437
"separate-nightlies" => self.separate_nightlies = parse_empty(k, v)?,
437438
"multitarget" => self.multitarget = parse_empty(k, v)?,
439+
"rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?,
438440
_ => bail!("unknown `-Z` flag specified: {}", k),
439441
}
440442

src/cargo/util/config/mod.rs

+11
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ use serde::Deserialize;
7070
use url::Url;
7171

7272
use self::ConfigValue as CV;
73+
use crate::core::compiler::rustdoc::RustdocExternMap;
7374
use crate::core::shell::Verbosity;
7475
use crate::core::{nightly_features_allowed, CliUnstable, Shell, SourceId, Workspace};
7576
use crate::ops;
@@ -172,6 +173,7 @@ pub struct Config {
172173
net_config: LazyCell<CargoNetConfig>,
173174
build_config: LazyCell<CargoBuildConfig>,
174175
target_cfgs: LazyCell<Vec<(String, TargetCfgConfig)>>,
176+
doc_extern_map: LazyCell<RustdocExternMap>,
175177
}
176178

177179
impl Config {
@@ -241,6 +243,7 @@ impl Config {
241243
net_config: LazyCell::new(),
242244
build_config: LazyCell::new(),
243245
target_cfgs: LazyCell::new(),
246+
doc_extern_map: LazyCell::new(),
244247
}
245248
}
246249

@@ -1159,6 +1162,14 @@ impl Config {
11591162
.try_borrow_with(|| target::load_target_cfgs(self))
11601163
}
11611164

1165+
pub fn doc_extern_map(&self) -> CargoResult<&RustdocExternMap> {
1166+
// Note: This does not support environment variables. The `Unit`
1167+
// fundamentally does not have access to the registry name, so there is
1168+
// nothing to query. Plumbing the name into SourceId is quite challenging.
1169+
self.doc_extern_map
1170+
.try_borrow_with(|| self.get::<RustdocExternMap>("doc.extern-map"))
1171+
}
1172+
11621173
/// Returns the `[target]` table definition for the given target triple.
11631174
pub fn target_cfg_triple(&self, target: &str) -> CargoResult<TargetConfig> {
11641175
target::load_target_triple(self, target)

src/doc/src/reference/unstable.md

+41
Original file line numberDiff line numberDiff line change
@@ -785,3 +785,44 @@ strip = "debuginfo"
785785

786786
Other possible values of `strip` are `none` and `symbols`. The default is
787787
`none`.
788+
789+
### rustdoc-map
790+
* Tracking Issue: TODO
791+
792+
This feature adds configuration settings that are passed to `rustdoc` so that
793+
it can generate links to dependencies whose documentation is hosted elsewhere
794+
when the dependency is not documented. First, add this to `.cargo/config`:
795+
796+
```toml
797+
[doc.extern-map.registries]
798+
crates-io = "https://docs.rs/"
799+
```
800+
801+
Then, when building documentation, use the following flags to cause links
802+
to dependencies to link to [docs.rs](https://docs.rs/):
803+
804+
```
805+
cargo +nightly doc --no-deps -Zrustdoc-map
806+
```
807+
808+
The `registries` table contains a mapping of registry name to the URL to link
809+
to. The URL may have the markers `{pkg_name}` and `{version}` which will get
810+
replaced with the corresponding values. If neither are specified, then Cargo
811+
defaults to appending `{pkg_name}/{version}/` to the end of the URL.
812+
813+
Another config setting is available to redirect standard library links. By
814+
default, rustdoc creates links to <https://doc.rust-lang.org/nightly/>. To
815+
change this behavior, use the `doc.extern-map.std` setting:
816+
817+
```toml
818+
[doc.extern-map]
819+
std = "local"
820+
```
821+
822+
A value of `"local"` means to link to the documentation found in the `rustc`
823+
sysroot. If you are using rustup, this documentation can be installed with
824+
`rustup component add rust-docs`.
825+
826+
The default value is `"remote"`.
827+
828+
The value may also take a URL for a custom location.

tests/testsuite/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ mod run;
9898
mod rustc;
9999
mod rustc_info_cache;
100100
mod rustdoc;
101+
mod rustdoc_extern_html;
101102
mod rustdocflags;
102103
mod rustflags;
103104
mod search;

0 commit comments

Comments
 (0)