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

  1. Break the cycle: move the shared code into a third crate that both depend on, instead of A↔B.
  2. For dev-dependency cycles, convert one direction to a build-dependency or extract tests into a separate crate.
  3. Audit `[patch]`/`[replace]` entries to ensure a replacement doesn't reintroduce a dependency on the original package.
  4. 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

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


AI-assisted analysis of rust-lang/cargo@0e07a15537 (2026-08-06). Data as JSON: /data/errors/56f4663aa56c8ab6.json. Report an issue: GitHub.