Skip to content

Commit 4b4dc0a

Browse files
committed
Fix a bug in Cargo's cyclic dep graph detection
Cargo's cyclic dependency graph detection turns out to have had a bug for quite a long time as surfaced by #9073. The bug in Cargo has to do with how dev-dependencies are handled. Cycles are "allowed" through dev-dependencies because the dev-dependency can depend on the original crate. Our cyclic graph detection, however, was too eagerly flagging a package as known to not have a cycle before we had processed everything about it. The fix here was basically to just simplify the graph traversal. Instead of traversing the raw `Resolve` this instead creates an alternate in-memory graph which has the actual edges we care about for cycle detection (e.g. every edge that wasn't induced via a dev-dependency). With this simplified graph we then apply the exact same algorithm, but this time it should be less buggy because we're not trying to do funky things about switching sets about what's visited halfway through a traversal. Closes #9073
1 parent a73e5b7 commit 4b4dc0a

File tree

2 files changed

+101
-31
lines changed

2 files changed

+101
-31
lines changed

src/cargo/core/resolver/mod.rs

Lines changed: 29 additions & 31 deletions
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, HashMap, HashSet};
50+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
5151
use std::mem;
5252
use std::rc::Rc;
5353
use std::time::{Duration, Instant};
@@ -1001,28 +1001,46 @@ fn find_candidate(
10011001
}
10021002

10031003
fn check_cycles(resolve: &Resolve) -> CargoResult<()> {
1004-
// Sort packages to produce user friendly deterministic errors.
1005-
let mut all_packages: Vec<_> = resolve.iter().collect();
1006-
all_packages.sort_unstable();
1004+
// Create a simple graph representation alternative of `resolve` which has
1005+
// only the edges we care about. Note that `BTree*` is used to produce
1006+
// deterministic error messages here. Also note that the main reason for
1007+
// this copy of the resolve graph is to avoid edges between a crate and its
1008+
// dev-dependency since that doesn't count for cycles.
1009+
let mut graph = BTreeMap::new();
1010+
for id in resolve.iter() {
1011+
let set = graph.entry(id).or_insert_with(BTreeSet::new);
1012+
for (dep, listings) in resolve.deps_not_replaced(id) {
1013+
let is_transitive = listings.iter().any(|d| d.is_transitive());
1014+
1015+
if is_transitive {
1016+
set.insert(dep);
1017+
set.extend(resolve.replacement(dep));
1018+
}
1019+
}
1020+
}
1021+
1022+
// After we have the `graph` that we care about, perform a simple cycle
1023+
// check by visiting all nodes. We visit each node at most once and we keep
1024+
// track of the path through the graph as we walk it. If we walk onto the
1025+
// same node twice that's a cycle.
10071026
let mut checked = HashSet::new();
10081027
let mut path = Vec::new();
10091028
let mut visited = HashSet::new();
1010-
for pkg in all_packages {
1011-
if !checked.contains(&pkg) {
1012-
visit(resolve, pkg, &mut visited, &mut path, &mut checked)?
1029+
for pkg in graph.keys() {
1030+
if !checked.contains(pkg) {
1031+
visit(&graph, *pkg, &mut visited, &mut path, &mut checked)?
10131032
}
10141033
}
10151034
return Ok(());
10161035

10171036
fn visit(
1018-
resolve: &Resolve,
1037+
graph: &BTreeMap<PackageId, BTreeSet<PackageId>>,
10191038
id: PackageId,
10201039
visited: &mut HashSet<PackageId>,
10211040
path: &mut Vec<PackageId>,
10221041
checked: &mut HashSet<PackageId>,
10231042
) -> CargoResult<()> {
10241043
path.push(id);
1025-
// See if we visited ourselves
10261044
if !visited.insert(id) {
10271045
anyhow::bail!(
10281046
"cyclic package dependency: package `{}` depends on itself. Cycle:\n{}",
@@ -1031,32 +1049,12 @@ fn check_cycles(resolve: &Resolve) -> CargoResult<()> {
10311049
);
10321050
}
10331051

1034-
// If we've already checked this node no need to recurse again as we'll
1035-
// just conclude the same thing as last time, so we only execute the
1036-
// recursive step if we successfully insert into `checked`.
1037-
//
1038-
// Note that if we hit an intransitive dependency then we clear out the
1039-
// visitation list as we can't induce a cycle through transitive
1040-
// dependencies.
10411052
if checked.insert(id) {
1042-
let mut empty_set = HashSet::new();
1043-
let mut empty_vec = Vec::new();
1044-
for (dep, listings) in resolve.deps_not_replaced(id) {
1045-
let is_transitive = listings.iter().any(|d| d.is_transitive());
1046-
let (visited, path) = if is_transitive {
1047-
(&mut *visited, &mut *path)
1048-
} else {
1049-
(&mut empty_set, &mut empty_vec)
1050-
};
1051-
visit(resolve, dep, visited, path, checked)?;
1052-
1053-
if let Some(id) = resolve.replacement(dep) {
1054-
visit(resolve, id, visited, path, checked)?;
1055-
}
1053+
for dep in graph[&id].iter() {
1054+
visit(graph, *dep, visited, path, checked)?;
10561055
}
10571056
}
10581057

1059-
// Ok, we're done, no longer visiting our node any more
10601058
path.pop();
10611059
visited.remove(&id);
10621060
Ok(())

tests/testsuite/path.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,3 +1063,75 @@ Caused by:
10631063
)
10641064
.run();
10651065
}
1066+
1067+
#[cargo_test]
1068+
fn catch_tricky_cycle() {
1069+
let p = project()
1070+
.file(
1071+
"Cargo.toml",
1072+
r#"
1073+
[package]
1074+
name = "message"
1075+
version = "0.1.0"
1076+
1077+
[dev-dependencies]
1078+
test = { path = "test" }
1079+
"#,
1080+
)
1081+
.file("src/lib.rs", "")
1082+
.file(
1083+
"tangle/Cargo.toml",
1084+
r#"
1085+
[package]
1086+
name = "tangle"
1087+
version = "0.1.0"
1088+
1089+
[dependencies]
1090+
message = { path = ".." }
1091+
snapshot = { path = "../snapshot" }
1092+
"#,
1093+
)
1094+
.file("tangle/src/lib.rs", "")
1095+
.file(
1096+
"snapshot/Cargo.toml",
1097+
r#"
1098+
[package]
1099+
name = "snapshot"
1100+
version = "0.1.0"
1101+
1102+
[dependencies]
1103+
ledger = { path = "../ledger" }
1104+
"#,
1105+
)
1106+
.file("snapshot/src/lib.rs", "")
1107+
.file(
1108+
"ledger/Cargo.toml",
1109+
r#"
1110+
[package]
1111+
name = "ledger"
1112+
version = "0.1.0"
1113+
1114+
[dependencies]
1115+
tangle = { path = "../tangle" }
1116+
"#,
1117+
)
1118+
.file("ledger/src/lib.rs", "")
1119+
.file(
1120+
"test/Cargo.toml",
1121+
r#"
1122+
[package]
1123+
name = "test"
1124+
version = "0.1.0"
1125+
1126+
[dependencies]
1127+
snapshot = { path = "../snapshot" }
1128+
"#,
1129+
)
1130+
.file("test/src/lib.rs", "")
1131+
.build();
1132+
1133+
p.cargo("test")
1134+
.with_stderr_contains("[..]cyclic package dependency[..]")
1135+
.with_status(101)
1136+
.run();
1137+
}

0 commit comments

Comments
 (0)