Skip to content

Commit 6658e1a

Browse files
committed
Auto merge of #12977 - Eh2406:bug_12941, r=epage
If the only path is a loop then counted as the shortest path. This is a fix for #12941 This graph data structure is used to store dependency DAGs. Where each edge represents a dependency from a package to the package that fulfilled the dependency. Different parts of the resolver store this data in opposite directions, sometimes packages point at the things that depend on them other times packages point to the parents that required them. Error messages often need to report on why a package is in the graph, either by walking up toward parents or down toward children depending on how this graph is stored. #12678 unified the two different walking implementations, and replace them with a breadth first search so as to find the shortest path. This code ignored when edge pointed at a package that had already been reached, because that generally describes a longer path to an existing package. Unfortunately, when I said this was a DAG that was a simplification. There can be cycles introduced as dev-dependencies. The existing code would reasonably ignore the cycles figuring that if we continue searching we would eventually find the root package (a package that nothing depended on). Missing the possibility that the root package created the cycle. Now we search through the entire graph looking for a root package. If we do not find a root package we report the path to the last package we processed.
2 parents 0fcc90d + d19273c commit 6658e1a

File tree

2 files changed

+56
-9
lines changed

2 files changed

+56
-9
lines changed

Diff for: src/cargo/util/graph.rs

+23-9
Original file line numberDiff line numberDiff line change
@@ -128,30 +128,32 @@ impl<'s, N: Eq + Ord + Clone + 's, E: Default + Clone + 's> Graph<N, E> {
128128
{
129129
let mut back_link = BTreeMap::new();
130130
let mut queue = VecDeque::from([pkg]);
131-
let mut bottom = None;
131+
let mut last = pkg;
132132

133133
while let Some(p) = queue.pop_front() {
134-
bottom = Some(p);
134+
last = p;
135+
let mut out_edges = true;
135136
for (child, edge) in fn_edge(&self, p) {
136-
bottom = None;
137+
out_edges = false;
137138
back_link.entry(child).or_insert_with(|| {
138139
queue.push_back(child);
139140
(p, edge)
140141
});
141142
}
142-
if bottom.is_some() {
143+
if out_edges {
143144
break;
144145
}
145146
}
146147

147148
let mut result = Vec::new();
148-
let mut next =
149-
bottom.expect("the only path was a cycle, no dependency graph has this shape");
149+
let mut next = last;
150150
while let Some((p, e)) = back_link.remove(&next) {
151151
result.push((next, Some(e)));
152152
next = p;
153153
}
154-
result.push((next, None));
154+
if result.iter().all(|(n, _)| n != &next) {
155+
result.push((next, None));
156+
}
155157
result.reverse();
156158
#[cfg(debug_assertions)]
157159
{
@@ -165,8 +167,12 @@ impl<'s, N: Eq + Ord + Clone + 's, E: Default + Clone + 's> Graph<N, E> {
165167
));
166168
}
167169
let last = result.last().unwrap().0;
168-
// fixme: this may sometimes be wrong when there are cycles.
169-
if !fn_edge(&self, last).next().is_none() {
170+
let set: Vec<_> = result.iter().map(|(k, _)| k).collect();
171+
if !fn_edge(&self, last)
172+
.filter(|(e, _)| !set.contains(&e))
173+
.next()
174+
.is_none()
175+
{
170176
self.print_for_test();
171177
unreachable!("The last element in the path should not have outgoing edges");
172178
}
@@ -188,6 +194,14 @@ fn path_to_case() {
188194
);
189195
}
190196

197+
#[test]
198+
fn path_to_self() {
199+
// Extracted from #12941
200+
let mut new: Graph<i32, ()> = Graph::new();
201+
new.link(0, 0);
202+
assert_eq!(new.path_to_bottom(&0), vec![(&0, Some(&()))]);
203+
}
204+
191205
impl<N: Eq + Ord + Clone, E: Default + Clone> Default for Graph<N, E> {
192206
fn default() -> Graph<N, E> {
193207
Graph::new()

Diff for: tests/testsuite/test.rs

+33
Original file line numberDiff line numberDiff line change
@@ -3555,6 +3555,39 @@ fn cyclic_dev() {
35553555
p.cargo("test --workspace").run();
35563556
}
35573557

3558+
#[cargo_test]
3559+
fn cyclical_dep_with_missing_feature() {
3560+
// Checks for error handling when a cyclical dev-dependency specify a
3561+
// feature that doesn't exist.
3562+
let p = project()
3563+
.file(
3564+
"Cargo.toml",
3565+
r#"
3566+
[package]
3567+
name = "foo"
3568+
version = "0.1.0"
3569+
3570+
[dev-dependencies]
3571+
foo = { path = ".", features = ["missing"] }
3572+
"#,
3573+
)
3574+
.file("src/lib.rs", "")
3575+
.build();
3576+
p.cargo("check")
3577+
.with_status(101)
3578+
.with_stderr(
3579+
"error: failed to select a version for `foo`.
3580+
... required by package `foo v0.1.0 ([..]/foo)`
3581+
versions that meet the requirements `*` are: 0.1.0
3582+
3583+
the package `foo` depends on `foo`, with features: `missing` but `foo` does not have these features.
3584+
3585+
3586+
failed to select a version for `foo` which could resolve this conflict",
3587+
)
3588+
.run();
3589+
}
3590+
35583591
#[cargo_test]
35593592
fn publish_a_crate_without_tests() {
35603593
Package::new("testless", "0.1.0")

0 commit comments

Comments
 (0)