Skip to content

Fix cargo resolver pruning of optional/feature-gated dependencies#191

Open
evandowning wants to merge 1 commit into
masterfrom
worktree-fix-issue-189-cargo-optional-deps
Open

Fix cargo resolver pruning of optional/feature-gated dependencies#191
evandowning wants to merge 1 commit into
masterfrom
worktree-fix-issue-189-cargo-optional-deps

Conversation

@evandowning

Copy link
Copy Markdown
Collaborator

Fixes #189.

Problem

The cargo resolver marked every declared dependency of a package as already-resolved. But cargo metadata does not add optional/feature-gated dependencies to its resolved package set when the active feature set doesn't enable them. Those deps therefore matched no package in the cache, and resolution.py pruned them and their entire subtrees.

Net effect: for cargo projects, it-depends' core "resolve all possible dependency versions" behavior collapsed to "the single locked dependency tree" (e.g. rayon, backtrace, afl and ~30 other crates reachable only via optional edges were dropped for the siderophile fixture).

Changes

  1. resolve_from_source — collect all packages first, then only set_resolved(dep) when a matching package was actually added to the cache. Inactive optional deps fall through to normal resolution instead of being marked resolved-to-nothing and pruned. This also unifies the previously-separate handling of the SourcePackage's direct deps and transitive deps.

  2. get_dependencies — wrap the cargo metadata subprocess call in except subprocess.CalledProcessError, mirroring pip.py's johnnydep handling (Add exception handling #125). Previously a single crate whose temp-project cargo metadata failed (exit 101) raised CalledProcessError and aborted the entire resolution; the old pruning shortcut had inadvertently masked this by never resolving those crates.

Tests

Added test_resolve_from_source_only_marks_resolved_deps, which mocks get_dependencies (no cargo binary required) and verifies that a dep with a matching resolved package is marked resolved while optional deps with no matching package are left unresolved.

Open question (draft)

Fix (1) restores full "all possible versions" expansion, which is large — the issue reports ~963 crates for siderophile vs. 163 in the 2021 baseline and 129 on current master. Per the issue, whether the intended cargo semantics are the full possible set, the locked tree, or a middle ground (e.g. union of all features at locked versions) should be decided deliberately. Opening as a draft so that semantics decision can be settled before merge.

Note: cargo is not installed in this environment, so the end-to-end siderophile reproduction and the re-enabled smoke test (#181) were not run here — CI should exercise those.

🤖 Generated with Claude Code

The cargo resolver marked every declared dependency of a package as
already-resolved, including optional/feature-gated deps that `cargo
metadata` does not activate under the default feature set. Those deps
resolved to nothing and were pruned, collapsing it-depends' "resolve all
possible versions" behavior to the single locked tree for cargo projects.

Only mark a dependency resolved when `cargo metadata` actually added a
matching package to the cache, so inactive optional deps fall through to
normal resolution instead of being pruned.

Also wrap the `cargo metadata` subprocess call in `get_dependencies` with
CalledProcessError handling, mirroring pip.py (#125), so one unresolvable
crate no longer aborts the entire resolution.

Fixes #189

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@evandowning evandowning self-assigned this Jul 1, 2026
@evandowning evandowning added the integration Runs integration tests label Jul 1, 2026
@evandowning evandowning requested a review from ESultanik July 1, 2026 12:44
@evandowning

Copy link
Copy Markdown
Collaborator Author

Ran the siderophile reproduction + re-enabled smoke test

The PR note said cargo wasn't installed here so the end-to-end siderophile reproduction and the re-enabled smoke test (#181) weren't run. I ran them with the rust toolchain (cargo 1.96.1) installed, applying this PR plus #181's re-enabled smoke test on top of master.

Reproduction — confirms the fix works

pytest test/test_smoke.py::TestSmoke::test_cargo --runintegration (10m48s, resolves trailofbits/siderophile@7bca0f5):

Smoke test — fails on one invariant, but it's baseline drift, not a regression

The only failure is:

AssertionError: resolution missing expected package(s) ['cargo:cc']

Root cause, verified from the resolved graph:

  • In the 163-package baseline, cc was reachable only via afl@0.10.1 → cc.
  • The full expansion now selects newer afl (0.15.24, 0.16.0), which dropped the cc build-dependency (afl now deps libc, rustc_version, xdg).
  • The remaining paths to cc (flate2 → cloudflare-zlib-sys, bitstream-io → core2) hit the fully-yanked crates above, so those subtrees legitimately error out.

So cc genuinely isn't in the current resolution. The baseline siderophile.expected.json predates both this fix and the current crates.io state — this is exactly the drift the "open question" here and #181 anticipate. The gap needs the semantics decision plus a re-baseline via test/rebuild_expected_output.py, not a code change. I did not rebuild the baseline, since that's the decision this draft defers.

🤖 Generated with Claude Code

@evandowning evandowning marked this pull request as ready for review July 1, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration Runs integration tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cargo resolver prunes optional/feature-gated dependencies (collapses "all possible versions" to the locked tree)

1 participant