rust-lang/cargo · error · anyhow::Error
cyclic package dependency: package `{id}` depends on itself.
Error message
cyclic package dependency: package `{id}` depends on itself. Cycle:
{describe_path} What it means
Detected during post-resolution cycle checking (`visit` traversal in `mod.rs`): a package transitively depends on itself through the resolved graph (including via replacements). Cargo performs a DFS keeping a `visited` set and a `path` stack; revisiting a node already in `visited` means a cycle, which is unsupported because dependency resolution assumes a DAG.
Source
Thrown at src/resolver/mod.rs:1042
visited: &mut HashSet<PackageId>,
path: &mut Vec<PackageId>,
checked: &mut HashSet<PackageId>,
) -> CargoResult<()> {
if !visited.insert(id) {
// We found a cycle and need to construct an error. Performance is no longer top priority.
let iter = path.iter().rev().scan(id, |child, parent| {
let dep = resolve.transitive_deps_not_replaced(*parent).find_map(
|(dep_id, transitive_dep)| {
(*child == dep_id || Some(*child) == resolve.replacement(dep_id))
.then_some(transitive_dep)
},
);
*child = *parent;
Some((parent, dep))
});
let iter = std::iter::once((&id, None)).chain(iter);
let describe_path = errors::describe_path(iter);
anyhow::bail!(
"cyclic package dependency: package `{id}` depends on itself. Cycle:\n{describe_path}"
);
}
if checked.insert(id) {
path.push(id);
for (dep_id, _transitive_dep) in resolve.transitive_deps_not_replaced(id) {
visit(resolve, dep_id, visited, path, checked)?;
if let Some(replace_id) = resolve.replacement(dep_id) {
visit(resolve, replace_id, visited, path, checked)?;
}
}
path.pop();
}
visited.remove(&id);
Ok(())
}View on GitHub (pinned to 0e07a15537)
Solutions
- Break the cycle: move the shared code into a third crate that both depend on, instead of A↔B.
- For dev-dependency cycles, convert one direction to a build-dependency or extract tests into a separate crate.
- Audit `[patch]`/`[replace]` entries to ensure a replacement doesn't reintroduce a dependency on the original package.
- Run `cargo tree -e features` or inspect the reported `describe_path` to see the exact cycle members.
Example fix
# before: crate-a depends on crate-b (path), crate-b depends on crate-a (path) # after: extract shared module into crate-c, both a and b depend on c
Defensive patterns
Strategy: validation
Validate before calling
# Detect cycles before resolving with cargo tree: cargo tree --cycles 2>/dev/null # exits non-zero / lists cycles if any # Static check in CI: forbid circular path deps # review [dependencies] path = entries for back-references
Prevention
- Keep workspace dependency graphs acyclic; extract shared code into a leaf crate.
- Run `cargo tree` regularly to spot unexpected edges.
- Review `[patch]`/`[replace]` to ensure replacements don't re-introduce the original as a transitive dep.
When it happens
Trigger: A dependency graph where A → B → A (directly or transitively), including through `[replace]`/`[patch]` substitutions that close a loop. The `!visited.insert(id)` check fires when `visit` re-enters an in-progress node.
Common situations: Path/git dependencies that accidentally point back at each other (workspace crates with circular path deps); `[patch]` or `[replace]` that substitutes a package with one depending on the original; dev-dependency cycles between two crates in a workspace; renaming/re-exporting crates that create a loop.
Related errors
- dependency `{}` in package `{}` requires a `{}` artifact to
- cannot add `{}` as a dependency to itself
- dependency ({}) specified without providing a local path, Gi
- cannot override workspace dependency with `--default-feature
- cannot override workspace dependency with `--registry`, eith
AI-assisted analysis of rust-lang/cargo@0e07a15537 (2026-08-06).
Data as JSON: /data/errors/56f4663aa56c8ab6.json.
Report an issue: GitHub.