Skip to content

Commit 00fe8a5

Browse files
committed
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
2 parents 6030f57 + f2d32ac commit 00fe8a5

File tree

10 files changed

+650
-11
lines changed

10 files changed

+650
-11
lines changed

ci/azure-install-rust.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ steps:
55
rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed"
66
rustup update --no-self-update $TOOLCHAIN
77
if [[ "$TOOLCHAIN" == "nightly"* ]]; then
8-
rustup component add --toolchain=$TOOLCHAIN rustc-dev llvm-tools-preview
8+
rustup component add --toolchain=$TOOLCHAIN rustc-dev llvm-tools-preview rust-docs
99
fi
1010
rustup default $TOOLCHAIN
1111
displayName: Install rust

src/cargo/core/compiler/fingerprint.rs

Lines changed: 21 additions & 1 deletion
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 172 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 24 additions & 8 deletions
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

@@ -1008,12 +1011,16 @@ impl Config {
10081011
/// Gets the index for a registry.
10091012
pub fn get_registry_index(&self, registry: &str) -> CargoResult<Url> {
10101013
validate_package_name(registry, "registry name", "")?;
1011-
Ok(
1012-
match self.get_string(&format!("registries.{}.index", registry))? {
1013-
Some(index) => self.resolve_registry_index(index)?,
1014-
None => bail!("No index found for registry: `{}`", registry),
1015-
},
1016-
)
1014+
if let Some(index) = self.get_string(&format!("registries.{}.index", registry))? {
1015+
self.resolve_registry_index(&index).chain_err(|| {
1016+
format!(
1017+
"invalid index URL for registry `{}` defined in {}",
1018+
registry, index.definition
1019+
)
1020+
})
1021+
} else {
1022+
bail!("no index found for registry: `{}`", registry);
1023+
}
10171024
}
10181025

10191026
/// Returns an error if `registry.index` is set.
@@ -1027,7 +1034,8 @@ impl Config {
10271034
Ok(())
10281035
}
10291036

1030-
fn resolve_registry_index(&self, index: Value<String>) -> CargoResult<Url> {
1037+
fn resolve_registry_index(&self, index: &Value<String>) -> CargoResult<Url> {
1038+
// This handles relative file: URLs, relative to the config definition.
10311039
let base = index
10321040
.definition
10331041
.root(self)
@@ -1036,7 +1044,7 @@ impl Config {
10361044
let _parsed = index.val.into_url()?;
10371045
let url = index.val.into_url_with_base(Some(&*base))?;
10381046
if url.password().is_some() {
1039-
bail!("Registry URLs may not contain passwords");
1047+
bail!("registry URLs may not contain passwords");
10401048
}
10411049
Ok(url)
10421050
}
@@ -1154,6 +1162,14 @@ impl Config {
11541162
.try_borrow_with(|| target::load_target_cfgs(self))
11551163
}
11561164

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+
11571173
/// Returns the `[target]` table definition for the given target triple.
11581174
pub fn target_cfg_triple(&self, target: &str) -> CargoResult<TargetConfig> {
11591175
target::load_target_triple(self, target)

0 commit comments

Comments
 (0)