mirror of
https://github.com/rust-lang/cargo.git
synced 2025-09-28 11:20:36 +00:00
Remove some doc collisions.
There are some cases where `cargo doc` will try to document two things with the same crate_name. This attempts to automatically remove some of those duplicates based on some rules: - Prefers dependencies for the target over dependencies for the host (such as proc-macros). - Prefers the "newest" version if it comes from the same source. There are still plenty of situations where there can be collisions, but I'm uncertain on the best way to handle those.
This commit is contained in:
parent
d4b3a1d43c
commit
a58b0c589a
@ -36,10 +36,11 @@ use crate::core::profiles::{Profiles, UnitFor};
|
||||
use crate::core::resolver::features::{self, FeaturesFor, RequestedFeatures};
|
||||
use crate::core::resolver::{HasDevUnits, Resolve, ResolveOpts};
|
||||
use crate::core::{FeatureValue, Package, PackageSet, Shell, Summary, Target};
|
||||
use crate::core::{PackageId, PackageIdSpec, TargetKind, Workspace};
|
||||
use crate::core::{PackageId, PackageIdSpec, SourceId, TargetKind, Workspace};
|
||||
use crate::ops;
|
||||
use crate::ops::resolve::WorkspaceResolve;
|
||||
use crate::util::config::Config;
|
||||
use crate::util::interning::InternedString;
|
||||
use crate::util::restricted_names::is_glob_pattern;
|
||||
use crate::util::{closest_msg, profile, CargoResult, StableHasher};
|
||||
|
||||
@ -503,6 +504,12 @@ pub fn create_bcx<'a, 'cfg>(
|
||||
interner,
|
||||
)?;
|
||||
|
||||
// TODO: In theory, Cargo should also dedupe the roots, but I'm uncertain
|
||||
// what heuristics to use in that case.
|
||||
if build_config.mode == (CompileMode::Doc { deps: true }) {
|
||||
remove_duplicate_doc(build_config, &mut unit_graph);
|
||||
}
|
||||
|
||||
if build_config
|
||||
.requested_kinds
|
||||
.iter()
|
||||
@ -1455,3 +1462,108 @@ fn opt_patterns_and_names(
|
||||
}
|
||||
Ok((opt_patterns, opt_names))
|
||||
}
|
||||
|
||||
/// Removes duplicate CompileMode::Doc units that would cause problems with
|
||||
/// filename collisions.
|
||||
///
|
||||
/// Rustdoc only separates units by crate name in the file directory
|
||||
/// structure. If any two units with the same crate name exist, this would
|
||||
/// cause a filename collision, causing different rustdoc invocations to stomp
|
||||
/// on one another's files.
|
||||
///
|
||||
/// Unfortunately this does not remove all duplicates, as some of them are
|
||||
/// either user error, or difficult to remove. Cases that I can think of:
|
||||
///
|
||||
/// - Same target name in different packages. See the `collision_doc` test.
|
||||
/// - Different sources. See `collision_doc_sources` test.
|
||||
///
|
||||
/// Ideally this would not be necessary.
|
||||
fn remove_duplicate_doc(build_config: &BuildConfig, unit_graph: &mut UnitGraph) {
|
||||
// NOTE: There is some risk that this can introduce problems because it
|
||||
// may create orphans in the unit graph (parts of the tree get detached
|
||||
// from the roots). I currently can't think of any ways this will cause a
|
||||
// problem because all other parts of Cargo traverse the graph starting
|
||||
// from the roots. Perhaps this should scan for detached units and remove
|
||||
// them too?
|
||||
//
|
||||
// First, create a mapping of crate_name -> Unit so we can see where the
|
||||
// duplicates are.
|
||||
let mut all_docs: HashMap<String, Vec<Unit>> = HashMap::new();
|
||||
for unit in unit_graph.keys() {
|
||||
if unit.mode.is_doc() {
|
||||
all_docs
|
||||
.entry(unit.target.crate_name())
|
||||
.or_default()
|
||||
.push(unit.clone());
|
||||
}
|
||||
}
|
||||
let mut remove = |units: Vec<Unit>, reason: &str| {
|
||||
for unit in &units {
|
||||
log::debug!(
|
||||
"removing duplicate doc due to {} for package {} target `{}`",
|
||||
reason,
|
||||
unit.pkg,
|
||||
unit.target.name()
|
||||
);
|
||||
unit_graph.remove(unit);
|
||||
}
|
||||
for unit_deps in unit_graph.values_mut() {
|
||||
unit_deps.retain(|unit_dep| !units.iter().any(|unit| *unit == unit_dep.unit));
|
||||
}
|
||||
};
|
||||
// Iterate over the duplicates and try to remove them from unit_graph.
|
||||
for (_crate_name, mut units) in all_docs {
|
||||
if units.len() == 1 {
|
||||
continue;
|
||||
}
|
||||
// Prefer target over host if --target was not specified.
|
||||
if build_config
|
||||
.requested_kinds
|
||||
.iter()
|
||||
.all(CompileKind::is_host)
|
||||
{
|
||||
let (to_remove, remaining_units): (Vec<Unit>, Vec<Unit>) =
|
||||
units.into_iter().partition(|unit| unit.kind.is_host());
|
||||
// Note these duplicates may not be real duplicates, since they
|
||||
// might get merged in rebuild_unit_graph_shared. Either way, it
|
||||
// shouldn't hurt to remove them early (although the report in the
|
||||
// log might be confusing).
|
||||
remove(to_remove, "host/target merger");
|
||||
units = remaining_units;
|
||||
if units.len() == 1 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Prefer newer versions over older.
|
||||
let mut source_map: HashMap<(InternedString, SourceId, CompileKind), Vec<Unit>> =
|
||||
HashMap::new();
|
||||
for unit in units {
|
||||
let pkg_id = unit.pkg.package_id();
|
||||
// Note, this does not detect duplicates from different sources.
|
||||
source_map
|
||||
.entry((pkg_id.name(), pkg_id.source_id(), unit.kind))
|
||||
.or_default()
|
||||
.push(unit);
|
||||
}
|
||||
let mut remaining_units = Vec::new();
|
||||
for (_key, mut units) in source_map {
|
||||
if units.len() > 1 {
|
||||
units.sort_by(|a, b| a.pkg.version().partial_cmp(b.pkg.version()).unwrap());
|
||||
// Remove any entries with version < newest.
|
||||
let newest_version = units.last().unwrap().pkg.version().clone();
|
||||
let (to_remove, keep_units): (Vec<Unit>, Vec<Unit>) = units
|
||||
.into_iter()
|
||||
.partition(|unit| unit.pkg.version() < &newest_version);
|
||||
remove(to_remove, "older version");
|
||||
remaining_units.extend(keep_units);
|
||||
} else {
|
||||
remaining_units.extend(units);
|
||||
}
|
||||
}
|
||||
if remaining_units.len() == 1 {
|
||||
continue;
|
||||
}
|
||||
// Are there other heuristics to remove duplicates that would make
|
||||
// sense? Maybe prefer path sources over all others?
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
use cargo_test_support::basic_manifest;
|
||||
use cargo_test_support::project;
|
||||
use cargo_test_support::registry::Package;
|
||||
use std::env;
|
||||
|
||||
#[cargo_test]
|
||||
@ -160,3 +161,273 @@ the same path; see <https://github.com/rust-lang/cargo/issues/6313>.
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn collision_doc_multiple_versions() {
|
||||
// Multiple versions of the same package.
|
||||
Package::new("old-dep", "1.0.0").publish();
|
||||
Package::new("bar", "1.0.0").dep("old-dep", "1.0").publish();
|
||||
// Note that this removes "old-dep". Just checking what happens when there
|
||||
// are orphans.
|
||||
Package::new("bar", "2.0.0").publish();
|
||||
let p = project()
|
||||
.file(
|
||||
"Cargo.toml",
|
||||
r#"
|
||||
[package]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
bar = "1.0"
|
||||
bar2 = { package="bar", version="2.0" }
|
||||
"#,
|
||||
)
|
||||
.file("src/lib.rs", "")
|
||||
.build();
|
||||
|
||||
// Should only document bar 2.0, should not document old-dep.
|
||||
p.cargo("doc")
|
||||
.with_stderr_unordered(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[DOWNLOADING] crates ...
|
||||
[DOWNLOADED] bar v2.0.0 [..]
|
||||
[DOWNLOADED] bar v1.0.0 [..]
|
||||
[DOWNLOADED] old-dep v1.0.0 [..]
|
||||
[CHECKING] old-dep v1.0.0
|
||||
[CHECKING] bar v2.0.0
|
||||
[CHECKING] bar v1.0.0
|
||||
[DOCUMENTING] bar v2.0.0
|
||||
[FINISHED] [..]
|
||||
[DOCUMENTING] foo v0.1.0 [..]
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn collision_doc_host_target_feature_split() {
|
||||
// Same dependency built twice due to different features.
|
||||
//
|
||||
// foo v0.1.0
|
||||
// ├── common v1.0.0
|
||||
// │ └── common-dep v1.0.0
|
||||
// └── pm v0.1.0 (proc-macro)
|
||||
// └── common v1.0.0
|
||||
// └── common-dep v1.0.0
|
||||
// [build-dependencies]
|
||||
// └── common-dep v1.0.0
|
||||
//
|
||||
// Here `common` and `common-dep` are built twice. `common-dep` has
|
||||
// different features for host versus target.
|
||||
Package::new("common-dep", "1.0.0")
|
||||
.feature("bdep-feat", &[])
|
||||
.file(
|
||||
"src/lib.rs",
|
||||
r#"
|
||||
/// Some doc
|
||||
pub fn f() {}
|
||||
|
||||
/// Another doc
|
||||
#[cfg(feature = "bdep-feat")]
|
||||
pub fn bdep_func() {}
|
||||
"#,
|
||||
)
|
||||
.publish();
|
||||
Package::new("common", "1.0.0")
|
||||
.dep("common-dep", "1.0")
|
||||
.file(
|
||||
"src/lib.rs",
|
||||
r#"
|
||||
/// Some doc
|
||||
pub fn f() {}
|
||||
"#,
|
||||
)
|
||||
.publish();
|
||||
let p = project()
|
||||
.file(
|
||||
"Cargo.toml",
|
||||
r#"
|
||||
[package]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
pm = { path = "pm" }
|
||||
common = "1.0"
|
||||
|
||||
[build-dependencies]
|
||||
common-dep = { version = "1.0", features = ["bdep-feat"] }
|
||||
"#,
|
||||
)
|
||||
.file(
|
||||
"src/lib.rs",
|
||||
r#"
|
||||
/// Some doc
|
||||
pub fn f() {}
|
||||
"#,
|
||||
)
|
||||
.file("build.rs", "fn main() {}")
|
||||
.file(
|
||||
"pm/Cargo.toml",
|
||||
r#"
|
||||
[package]
|
||||
name = "pm"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
common = "1.0"
|
||||
"#,
|
||||
)
|
||||
.file(
|
||||
"pm/src/lib.rs",
|
||||
r#"
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
/// Some doc
|
||||
#[proc_macro]
|
||||
pub fn pm(_input: TokenStream) -> TokenStream {
|
||||
"".parse().unwrap()
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// No warnings, no duplicates, common and common-dep only documented once.
|
||||
p.cargo("doc")
|
||||
// Cannot check full output due to https://github.com/rust-lang/cargo/issues/9076
|
||||
.with_stderr_does_not_contain("[WARNING][..]")
|
||||
.run();
|
||||
|
||||
assert!(p.build_dir().join("doc/common_dep/fn.f.html").exists());
|
||||
assert!(!p
|
||||
.build_dir()
|
||||
.join("doc/common_dep/fn.bdep_func.html")
|
||||
.exists());
|
||||
assert!(p.build_dir().join("doc/common/fn.f.html").exists());
|
||||
assert!(p.build_dir().join("doc/pm/macro.pm.html").exists());
|
||||
assert!(p.build_dir().join("doc/foo/fn.f.html").exists());
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn collision_doc_profile_split() {
|
||||
// Same dependency built twice due to different profile settings.
|
||||
Package::new("common", "1.0.0").publish();
|
||||
let p = project()
|
||||
.file(
|
||||
"Cargo.toml",
|
||||
r#"
|
||||
[package]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
pm = { path = "pm" }
|
||||
common = "1.0"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 2
|
||||
"#,
|
||||
)
|
||||
.file("src/lib.rs", "")
|
||||
.file(
|
||||
"pm/Cargo.toml",
|
||||
r#"
|
||||
[package]
|
||||
name = "pm"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
common = "1.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
"#,
|
||||
)
|
||||
.file("pm/src/lib.rs", "")
|
||||
.build();
|
||||
|
||||
// Just to verify that common is normally built twice.
|
||||
p.cargo("build -v")
|
||||
.with_stderr(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[DOWNLOADING] crates ...
|
||||
[DOWNLOADED] common v1.0.0 [..]
|
||||
[COMPILING] common v1.0.0
|
||||
[RUNNING] `rustc --crate-name common [..]
|
||||
[RUNNING] `rustc --crate-name common [..]
|
||||
[COMPILING] pm v0.1.0 [..]
|
||||
[RUNNING] `rustc --crate-name pm [..]
|
||||
[COMPILING] foo v0.1.0 [..]
|
||||
[RUNNING] `rustc --crate-name foo [..]
|
||||
[FINISHED] [..]
|
||||
",
|
||||
)
|
||||
.run();
|
||||
|
||||
// Should only document common once, no warnings.
|
||||
p.cargo("doc")
|
||||
.with_stderr_unordered(
|
||||
"\
|
||||
[CHECKING] common v1.0.0
|
||||
[DOCUMENTING] common v1.0.0
|
||||
[DOCUMENTING] pm v0.1.0 [..]
|
||||
[DOCUMENTING] foo v0.1.0 [..]
|
||||
[FINISHED] [..]
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn collision_doc_sources() {
|
||||
// Different sources with the same package.
|
||||
Package::new("bar", "1.0.0").publish();
|
||||
let p = project()
|
||||
.file(
|
||||
"Cargo.toml",
|
||||
r#"
|
||||
[package]
|
||||
name = "foo"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
bar = "1.0"
|
||||
bar2 = { path = "bar", package = "bar" }
|
||||
"#,
|
||||
)
|
||||
.file("src/lib.rs", "")
|
||||
.file("bar/Cargo.toml", &basic_manifest("bar", "1.0.0"))
|
||||
.file("bar/src/lib.rs", "")
|
||||
.build();
|
||||
|
||||
p.cargo("doc")
|
||||
.with_stderr_unordered(
|
||||
"\
|
||||
[UPDATING] [..]
|
||||
[DOWNLOADING] crates ...
|
||||
[DOWNLOADED] bar v1.0.0 [..]
|
||||
[WARNING] output filename collision.
|
||||
The lib target `bar` in package `bar v1.0.0` has the same output filename as \
|
||||
the lib target `bar` in package `bar v1.0.0 ([..]/foo/bar)`.
|
||||
Colliding filename is: [..]/foo/target/doc/bar/index.html
|
||||
The targets should have unique names.
|
||||
This is a known bug where multiple crates with the same name use
|
||||
the same path; see <https://github.com/rust-lang/cargo/issues/6313>.
|
||||
[CHECKING] bar v1.0.0 [..]
|
||||
[DOCUMENTING] bar v1.0.0 [..]
|
||||
[DOCUMENTING] bar v1.0.0
|
||||
[CHECKING] bar v1.0.0
|
||||
[DOCUMENTING] foo v0.1.0 [..]
|
||||
[FINISHED] [..]
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user