Skip to content

Commit 2b4273d

Browse files
committed
Simplify checking for dependency cycles
1 parent f911dfd commit 2b4273d

File tree

3 files changed

+47
-38
lines changed

3 files changed

+47
-38
lines changed

src/cargo/core/resolver/mod.rs

Lines changed: 26 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,69 +1004,57 @@ fn find_candidate(
10041004
}
10051005

10061006
fn check_cycles(resolve: &Resolve) -> CargoResult<()> {
1007-
// Create a simple graph representation alternative of `resolve` which has
1008-
// only the edges we care about. Note that `BTree*` is used to produce
1009-
// deterministic error messages here. Also note that the main reason for
1010-
// this copy of the resolve graph is to avoid edges between a crate and its
1011-
// dev-dependency since that doesn't count for cycles.
1012-
let mut graph = BTreeMap::new();
1013-
for id in resolve.iter() {
1014-
let map = graph.entry(id).or_insert_with(BTreeMap::new);
1015-
for (dep_id, listings) in resolve.deps_not_replaced(id) {
1016-
let transitive_dep = listings.iter().find(|d| d.is_transitive());
1017-
1018-
if let Some(transitive_dep) = transitive_dep.cloned() {
1019-
map.insert(dep_id, transitive_dep.clone());
1020-
resolve
1021-
.replacement(dep_id)
1022-
.map(|p| map.insert(p, transitive_dep));
1023-
}
1024-
}
1025-
}
1026-
1027-
// After we have the `graph` that we care about, perform a simple cycle
1028-
// check by visiting all nodes. We visit each node at most once and we keep
1007+
// Perform a simple cycle check by visiting all nodes.
1008+
// We visit each node at most once and we keep
10291009
// track of the path through the graph as we walk it. If we walk onto the
10301010
// same node twice that's a cycle.
1031-
let mut checked = HashSet::new();
1032-
let mut path = Vec::new();
1033-
let mut visited = HashSet::new();
1034-
for pkg in graph.keys() {
1035-
if !checked.contains(pkg) {
1036-
visit(&graph, *pkg, &mut visited, &mut path, &mut checked)?
1011+
let mut checked = HashSet::with_capacity(resolve.len());
1012+
let mut path = Vec::with_capacity(4);
1013+
let mut visited = HashSet::with_capacity(4);
1014+
for pkg in resolve.iter() {
1015+
if !checked.contains(&pkg) {
1016+
visit(&resolve, pkg, &mut visited, &mut path, &mut checked)?
10371017
}
10381018
}
10391019
return Ok(());
10401020

10411021
fn visit(
1042-
graph: &BTreeMap<PackageId, BTreeMap<PackageId, Dependency>>,
1022+
resolve: &Resolve,
10431023
id: PackageId,
10441024
visited: &mut HashSet<PackageId>,
10451025
path: &mut Vec<PackageId>,
10461026
checked: &mut HashSet<PackageId>,
10471027
) -> CargoResult<()> {
1048-
path.push(id);
10491028
if !visited.insert(id) {
1050-
let iter = path.iter().rev().skip(1).scan(id, |child, parent| {
1051-
let dep = graph.get(parent).and_then(|adjacent| adjacent.get(child));
1029+
// We found a cycle and need to construct an error. Performance is no longer top priority.
1030+
let iter = path.iter().rev().scan(id, |child, parent| {
1031+
let dep = resolve.transitive_deps_not_replaced(*parent).find_map(
1032+
|(dep_id, transitive_dep)| {
1033+
(*child == dep_id || Some(*child) == resolve.replacement(dep_id))
1034+
.then_some(transitive_dep)
1035+
},
1036+
);
10521037
*child = *parent;
10531038
Some((parent, dep))
10541039
});
10551040
let iter = std::iter::once((&id, None)).chain(iter);
1041+
let describe_path = errors::describe_path(iter);
10561042
anyhow::bail!(
1057-
"cyclic package dependency: package `{}` depends on itself. Cycle:\n{}",
1058-
id,
1059-
errors::describe_path(iter),
1043+
"cyclic package dependency: package `{id}` depends on itself. Cycle:\n{describe_path}"
10601044
);
10611045
}
10621046

10631047
if checked.insert(id) {
1064-
for dep in graph[&id].keys() {
1065-
visit(graph, *dep, visited, path, checked)?;
1048+
path.push(id);
1049+
for (dep_id, _transitive_dep) in resolve.transitive_deps_not_replaced(id) {
1050+
visit(resolve, dep_id, visited, path, checked)?;
1051+
if let Some(replace_id) = resolve.replacement(dep_id) {
1052+
visit(resolve, replace_id, visited, path, checked)?;
1053+
}
10661054
}
1055+
path.pop();
10671056
}
10681057

1069-
path.pop();
10701058
visited.remove(&id);
10711059
Ok(())
10721060
}

src/cargo/core/resolver/resolve.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,10 @@ unable to verify that `{0}` is the same as when the lockfile was generated
324324
self.graph.iter().cloned()
325325
}
326326

327+
pub fn len(&self) -> usize {
328+
self.graph.len()
329+
}
330+
327331
pub fn deps(&self, pkg: PackageId) -> impl Iterator<Item = (PackageId, &HashSet<Dependency>)> {
328332
self.deps_not_replaced(pkg)
329333
.map(move |(id, deps)| (self.replacement(id).unwrap_or(id), deps))
@@ -336,6 +340,19 @@ unable to verify that `{0}` is the same as when the lockfile was generated
336340
self.graph.edges(&pkg).map(|(id, deps)| (*id, deps))
337341
}
338342

343+
// Only edges that are transitive, filtering out edges between a crate and its dev-dependency
344+
// since that doesn't count for cycles.
345+
pub fn transitive_deps_not_replaced(
346+
&self,
347+
pkg: PackageId,
348+
) -> impl Iterator<Item = (PackageId, &Dependency)> {
349+
self.graph.edges(&pkg).filter_map(|(id, deps)| {
350+
deps.iter()
351+
.find(|d| d.is_transitive())
352+
.map(|transitive_dep| (*id, transitive_dep))
353+
})
354+
}
355+
339356
pub fn replacement(&self, pkg: PackageId) -> Option<PackageId> {
340357
self.replacements.get(&pkg).cloned()
341358
}

src/cargo/util/graph.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ impl<N: Eq + Ord + Clone, E: Default + Clone> Graph<N, E> {
6969
self.nodes.keys()
7070
}
7171

72+
pub fn len(&self) -> usize {
73+
self.nodes.len()
74+
}
75+
7276
/// Checks if there is a path from `from` to `to`.
7377
pub fn is_path_from_to<'a>(&'a self, from: &'a N, to: &'a N) -> bool {
7478
let mut stack = vec![from];

0 commit comments

Comments
 (0)