Skip to content

Commit 77a0379

Browse files
committed
Auto merge of #9827 - weihanglo:issue-6199, r=Eh2406
Improve resolver message to include dependency requirements Resolves #6199. Thanks for previous efforts: #5452, #6374, #6665, which are great but somehow outdated, so I tweak them and create this PR. This will also be obsolete if we ship pubgrub-rs with cargo in the future 😃 But before that happens, IMO these changes are still helpful. --- This PR changes the resolver error message from https://github.com/rust-lang/cargo/blob/216f915c46b8ada2323423d049314ba18247ef95/tests/testsuite/build.rs#L1104-L1106 to https://github.com/rust-lang/cargo/blob/0afd40b4de17a5c45145a0762beb4ef001720fe1/tests/testsuite/build.rs#L1104-L1106 Also provide different message for different source kinds, such like: https://github.com/rust-lang/cargo/blob/0afd40b4de17a5c45145a0762beb4ef001720fe1/tests/testsuite/build.rs#L2810-L2812 ## TODO? From #5452 (comment), there shall be at least one task left behind: > 3. Special case pind by a lock file and not a `"=1.1.2"` in a dependency. Also add a "note: try cargo update" to the end. In this PR, `validate_links` also faces this issue that a dependency requirement is locked into a precise version `=0.1.0`. https://github.com/rust-lang/cargo/blob/a5f8bc94f5d38539dd127f735ea4d3a515c230fd/tests/testsuite/build_script.rs#L1002-L1004 I am uncertain about how to resolve this. Besides the function`validate_links`, is this problem really a thing that may happen? If not, since `validate_links` only handles old validation logic, it may be ok to drop the commit a5f8bc9 and leave it as is.
2 parents 851defd + a5f8bc9 commit 77a0379

File tree

12 files changed

+134
-75
lines changed

12 files changed

+134
-75
lines changed

crates/resolver-tests/tests/resolve.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,7 @@ fn cyclic_good_error_message() {
14981498
assert_eq!("\
14991499
cyclic package dependency: package `A v0.0.0 (registry `https://example.com/`)` depends on itself. Cycle:
15001500
package `A v0.0.0 (registry `https://example.com/`)`
1501-
... which is depended on by `C v0.0.0 (registry `https://example.com/`)`
1502-
... which is depended on by `A v0.0.0 (registry `https://example.com/`)`\
1501+
... which satisfies dependency `A = \"*\"` of package `C v0.0.0 (registry `https://example.com/`)`
1502+
... which satisfies dependency `C = \"*\"` of package `A v0.0.0 (registry `https://example.com/`)`\
15031503
", error.to_string());
15041504
}

src/cargo/core/compiler/links.rs

+11-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use super::unit_graph::UnitGraph;
2+
use crate::core::resolver::errors::describe_path;
23
use crate::core::{PackageId, Resolve};
34
use crate::util::errors::CargoResult;
45
use std::collections::{HashMap, HashSet};
5-
use std::fmt::Write;
66

77
/// Validate `links` field does not conflict between packages.
88
pub fn validate_links(resolve: &Resolve, unit_graph: &UnitGraph) -> CargoResult<()> {
@@ -28,17 +28,15 @@ pub fn validate_links(resolve: &Resolve, unit_graph: &UnitGraph) -> CargoResult<
2828
None => continue,
2929
};
3030
if let Some(&prev) = links.get(lib) {
31+
let prev_path = resolve
32+
.path_to_top(&prev)
33+
.into_iter()
34+
.map(|(p, d)| (p, d.and_then(|d| d.iter().next())));
3135
let pkg = unit.pkg.package_id();
32-
33-
let describe_path = |pkgid: PackageId| -> String {
34-
let dep_path = resolve.path_to_top(&pkgid);
35-
let mut dep_path_desc = format!("package `{}`", dep_path[0]);
36-
for dep in dep_path.iter().skip(1) {
37-
write!(dep_path_desc, "\n ... which is depended on by `{}`", dep).unwrap();
38-
}
39-
dep_path_desc
40-
};
41-
36+
let path = resolve
37+
.path_to_top(&pkg)
38+
.into_iter()
39+
.map(|(p, d)| (p, d.and_then(|d| d.iter().next())));
4240
anyhow::bail!(
4341
"multiple packages link to native library `{}`, \
4442
but a native library can be linked only once\n\
@@ -47,9 +45,9 @@ pub fn validate_links(resolve: &Resolve, unit_graph: &UnitGraph) -> CargoResult<
4745
\n\
4846
{}\nalso links to native library `{}`",
4947
lib,
50-
describe_path(prev),
48+
describe_path(prev_path),
5149
lib,
52-
describe_path(pkg),
50+
describe_path(path),
5351
lib
5452
)
5553
}

src/cargo/core/resolver/dep_cache.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//! This module impl that cache in all the gory details
1111
1212
use crate::core::resolver::context::Context;
13-
use crate::core::resolver::errors::describe_path;
13+
use crate::core::resolver::errors::describe_path_in_context;
1414
use crate::core::resolver::types::{ConflictReason, DepInfo, FeaturesSet};
1515
use crate::core::resolver::{
1616
ActivateError, ActivateResult, CliFeatures, RequestedFeatures, ResolveOpts, VersionOrdering,
@@ -216,7 +216,7 @@ impl<'a> RegistryQueryer<'a> {
216216
format!(
217217
"failed to get `{}` as a dependency of {}",
218218
dep.package_name(),
219-
describe_path(&cx.parents.path_to_bottom(&candidate.package_id())),
219+
describe_path_in_context(cx, &candidate.package_id()),
220220
)
221221
})?;
222222
Ok((dep, candidates, features))

src/cargo/core/resolver/errors.rs

+56-16
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ pub(super) fn activation_error(
8282
cx.parents
8383
.path_to_bottom(&parent.package_id())
8484
.into_iter()
85+
.map(|(node, _)| node)
8586
.cloned()
8687
.collect(),
8788
)
@@ -90,9 +91,7 @@ pub(super) fn activation_error(
9091
if !candidates.is_empty() {
9192
let mut msg = format!("failed to select a version for `{}`.", dep.package_name());
9293
msg.push_str("\n ... required by ");
93-
msg.push_str(&describe_path(
94-
&cx.parents.path_to_bottom(&parent.package_id()),
95-
));
94+
msg.push_str(&describe_path_in_context(cx, &parent.package_id()));
9695

9796
msg.push_str("\nversions that meet the requirements `");
9897
msg.push_str(&dep.version_req().to_string());
@@ -128,7 +127,7 @@ pub(super) fn activation_error(
128127
msg.push_str("`, but it conflicts with a previous package which links to `");
129128
msg.push_str(link);
130129
msg.push_str("` as well:\n");
131-
msg.push_str(&describe_path(&cx.parents.path_to_bottom(p)));
130+
msg.push_str(&describe_path_in_context(cx, p));
132131
msg.push_str("\nOnly one package in the dependency graph may specify the same links value. This helps ensure that only one copy of a native library is linked in the final binary. ");
133132
msg.push_str("Try to adjust your dependencies so that only one package uses the links ='");
134133
msg.push_str(&*dep.package_name());
@@ -197,7 +196,7 @@ pub(super) fn activation_error(
197196
for (p, r) in &conflicting_activations {
198197
if let ConflictReason::Semver = r {
199198
msg.push_str("\n\n previously selected ");
200-
msg.push_str(&describe_path(&cx.parents.path_to_bottom(p)));
199+
msg.push_str(&describe_path_in_context(cx, p));
201200
}
202201
}
203202
}
@@ -250,9 +249,7 @@ pub(super) fn activation_error(
250249
registry.describe_source(dep.source_id()),
251250
);
252251
msg.push_str("required by ");
253-
msg.push_str(&describe_path(
254-
&cx.parents.path_to_bottom(&parent.package_id()),
255-
));
252+
msg.push_str(&describe_path_in_context(cx, &parent.package_id()));
256253

257254
// If we have a path dependency with a locked version, then this may
258255
// indicate that we updated a sub-package and forgot to run `cargo
@@ -330,9 +327,7 @@ pub(super) fn activation_error(
330327
}
331328
msg.push_str(&format!("location searched: {}\n", dep.source_id()));
332329
msg.push_str("required by ");
333-
msg.push_str(&describe_path(
334-
&cx.parents.path_to_bottom(&parent.package_id()),
335-
));
330+
msg.push_str(&describe_path_in_context(cx, &parent.package_id()));
336331

337332
msg
338333
};
@@ -351,12 +346,57 @@ pub(super) fn activation_error(
351346
to_resolve_err(anyhow::format_err!("{}", msg))
352347
}
353348

349+
/// Returns String representation of dependency chain for a particular `pkgid`
350+
/// within given context.
351+
pub(super) fn describe_path_in_context(cx: &Context, id: &PackageId) -> String {
352+
let iter = cx
353+
.parents
354+
.path_to_bottom(id)
355+
.into_iter()
356+
.map(|(p, d)| (p, d.and_then(|d| d.iter().next())));
357+
describe_path(iter)
358+
}
359+
354360
/// Returns String representation of dependency chain for a particular `pkgid`.
355-
pub(super) fn describe_path(path: &[&PackageId]) -> String {
361+
///
362+
/// Note that all elements of `path` iterator should have `Some` dependency
363+
/// except the first one. It would look like:
364+
///
365+
/// (pkg0, None)
366+
/// -> (pkg1, dep from pkg1 satisfied by pkg0)
367+
/// -> (pkg2, dep from pkg2 satisfied by pkg1)
368+
/// -> ...
369+
pub(crate) fn describe_path<'a>(
370+
mut path: impl Iterator<Item = (&'a PackageId, Option<&'a Dependency>)>,
371+
) -> String {
356372
use std::fmt::Write;
357-
let mut dep_path_desc = format!("package `{}`", path[0]);
358-
for dep in path[1..].iter() {
359-
write!(dep_path_desc, "\n ... which is depended on by `{}`", dep).unwrap();
373+
374+
if let Some(p) = path.next() {
375+
let mut dep_path_desc = format!("package `{}`", p.0);
376+
for (pkg, dep) in path {
377+
let dep = dep.unwrap();
378+
let source_kind = if dep.source_id().is_path() {
379+
"path "
380+
} else if dep.source_id().is_git() {
381+
"git "
382+
} else {
383+
""
384+
};
385+
let requirement = if source_kind.is_empty() {
386+
format!("{} = \"{}\"", dep.name_in_toml(), dep.version_req())
387+
} else {
388+
dep.name_in_toml().to_string()
389+
};
390+
write!(
391+
dep_path_desc,
392+
"\n ... which satisfies {}dependency `{}` of package `{}`",
393+
source_kind, requirement, pkg
394+
)
395+
.unwrap();
396+
}
397+
398+
return dep_path_desc;
360399
}
361-
dep_path_desc
400+
401+
String::new()
362402
}

src/cargo/core/resolver/mod.rs

+20-12
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
//! that we're implementing something that probably shouldn't be allocating all
4848
//! over the place.
4949
50-
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
50+
use std::collections::{BTreeMap, HashMap, HashSet};
5151
use std::mem;
5252
use std::rc::Rc;
5353
use std::time::{Duration, Instant};
@@ -78,7 +78,7 @@ mod conflict_cache;
7878
mod context;
7979
mod dep_cache;
8080
mod encode;
81-
mod errors;
81+
pub(crate) mod errors;
8282
pub mod features;
8383
mod resolve;
8484
mod types;
@@ -1007,13 +1007,15 @@ fn check_cycles(resolve: &Resolve) -> CargoResult<()> {
10071007
// dev-dependency since that doesn't count for cycles.
10081008
let mut graph = BTreeMap::new();
10091009
for id in resolve.iter() {
1010-
let set = graph.entry(id).or_insert_with(BTreeSet::new);
1011-
for (dep, listings) in resolve.deps_not_replaced(id) {
1012-
let is_transitive = listings.iter().any(|d| d.is_transitive());
1013-
1014-
if is_transitive {
1015-
set.insert(dep);
1016-
set.extend(resolve.replacement(dep));
1010+
let map = graph.entry(id).or_insert_with(BTreeMap::new);
1011+
for (dep_id, listings) in resolve.deps_not_replaced(id) {
1012+
let transitive_dep = listings.iter().find(|d| d.is_transitive());
1013+
1014+
if let Some(transitive_dep) = transitive_dep.cloned() {
1015+
map.insert(dep_id, transitive_dep.clone());
1016+
resolve
1017+
.replacement(dep_id)
1018+
.map(|p| map.insert(p, transitive_dep));
10171019
}
10181020
}
10191021
}
@@ -1033,23 +1035,29 @@ fn check_cycles(resolve: &Resolve) -> CargoResult<()> {
10331035
return Ok(());
10341036

10351037
fn visit(
1036-
graph: &BTreeMap<PackageId, BTreeSet<PackageId>>,
1038+
graph: &BTreeMap<PackageId, BTreeMap<PackageId, Dependency>>,
10371039
id: PackageId,
10381040
visited: &mut HashSet<PackageId>,
10391041
path: &mut Vec<PackageId>,
10401042
checked: &mut HashSet<PackageId>,
10411043
) -> CargoResult<()> {
10421044
path.push(id);
10431045
if !visited.insert(id) {
1046+
let iter = path.iter().rev().skip(1).scan(id, |child, parent| {
1047+
let dep = graph.get(parent).and_then(|adjacent| adjacent.get(child));
1048+
*child = *parent;
1049+
Some((parent, dep))
1050+
});
1051+
let iter = std::iter::once((&id, None)).chain(iter);
10441052
anyhow::bail!(
10451053
"cyclic package dependency: package `{}` depends on itself. Cycle:\n{}",
10461054
id,
1047-
errors::describe_path(&path.iter().rev().collect::<Vec<_>>()),
1055+
errors::describe_path(iter),
10481056
);
10491057
}
10501058

10511059
if checked.insert(id) {
1052-
for dep in graph[&id].iter() {
1060+
for dep in graph[&id].keys() {
10531061
visit(graph, *dep, visited, path, checked)?;
10541062
}
10551063
}

src/cargo/core/resolver/resolve.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ impl Resolve {
117117

118118
/// Resolves one of the paths from the given dependent package up to
119119
/// the root.
120-
pub fn path_to_top<'a>(&'a self, pkg: &'a PackageId) -> Vec<&'a PackageId> {
120+
pub fn path_to_top<'a>(
121+
&'a self,
122+
pkg: &'a PackageId,
123+
) -> Vec<(&'a PackageId, Option<&'a HashSet<Dependency>>)> {
121124
self.graph.path_to_top(pkg)
122125
}
123126

src/cargo/util/graph.rs

+21-11
Original file line numberDiff line numberDiff line change
@@ -89,40 +89,50 @@ impl<N: Eq + Ord + Clone, E: Default + Clone> Graph<N, E> {
8989

9090
/// Resolves one of the paths from the given dependent package down to
9191
/// a leaf.
92-
pub fn path_to_bottom<'a>(&'a self, mut pkg: &'a N) -> Vec<&'a N> {
93-
let mut result = vec![pkg];
92+
///
93+
/// Each element contains a node along with an edge except the first one.
94+
/// The representation would look like:
95+
///
96+
/// (Node0,) -> (Node1, Edge01) -> (Node2, Edge12)...
97+
pub fn path_to_bottom<'a>(&'a self, mut pkg: &'a N) -> Vec<(&'a N, Option<&'a E>)> {
98+
let mut result = vec![(pkg, None)];
9499
while let Some(p) = self.nodes.get(pkg).and_then(|p| {
95100
p.iter()
96101
// Note that we can have "cycles" introduced through dev-dependency
97102
// edges, so make sure we don't loop infinitely.
98-
.find(|(node, _)| !result.contains(node))
99-
.map(|(p, _)| p)
103+
.find(|&(node, _)| result.iter().all(|p| p.0 != node))
104+
.map(|(node, edge)| (node, Some(edge)))
100105
}) {
101106
result.push(p);
102-
pkg = p;
107+
pkg = p.0;
103108
}
104109
result
105110
}
106111

107112
/// Resolves one of the paths from the given dependent package up to
108113
/// the root.
109-
pub fn path_to_top<'a>(&'a self, mut pkg: &'a N) -> Vec<&'a N> {
114+
///
115+
/// Each element contains a node along with an edge except the first one.
116+
/// The representation would look like:
117+
///
118+
/// (Node0,) -> (Node1, Edge01) -> (Node2, Edge12)...
119+
pub fn path_to_top<'a>(&'a self, mut pkg: &'a N) -> Vec<(&'a N, Option<&'a E>)> {
110120
// Note that this implementation isn't the most robust per se, we'll
111121
// likely have to tweak this over time. For now though it works for what
112122
// it's used for!
113-
let mut result = vec![pkg];
114-
let first_pkg_depending_on = |pkg: &N, res: &[&N]| {
123+
let mut result = vec![(pkg, None)];
124+
let first_pkg_depending_on = |pkg, res: &[(&N, Option<&E>)]| {
115125
self.nodes
116126
.iter()
117127
.filter(|(_, adjacent)| adjacent.contains_key(pkg))
118128
// Note that we can have "cycles" introduced through dev-dependency
119129
// edges, so make sure we don't loop infinitely.
120-
.find(|(node, _)| !res.contains(node))
121-
.map(|(p, _)| p)
130+
.find(|&(node, _)| !res.iter().any(|p| p.0 == node))
131+
.map(|(p, adjacent)| (p, adjacent.get(pkg)))
122132
};
123133
while let Some(p) = first_pkg_depending_on(pkg, &result) {
124134
result.push(p);
125-
pkg = p;
135+
pkg = p.0;
126136
}
127137
result
128138
}

0 commit comments

Comments
 (0)