Skip to content

Pubgrub experiment for fun - #91

Open
weihanglo wants to merge 103 commits into
masterfrom
pubgrub
Open

weihanglo wants to merge 103 commits into
masterfrom
pubgrub

Conversation

@weihanglo

Copy link
Copy Markdown
Owner

No description provided.

@weihanglo weihanglo changed the title Experiment for fun Pubgrub experiment for fun Jun 17, 2026
@weihanglo
weihanglo force-pushed the pubgrub branch 2 times, most recently from a2a2827 to 09389d8 Compare June 19, 2026 15:32
xtqqczze and others added 27 commits July 17, 2026 21:09
Track missing Cargo.toml state using Option<Option<Package>>, where
outer None means unloaded and inner None means loaded but missing.
This allows the resolver to produce standard diagnostics instead of
raw IO error chains.
- Scan subdirectories when a path dependency is missing or invalid.

- Show helpful hints suggesting packages that exist nearby.
…7236)

### What does this PR try to resolve?

This is another optimization to reduce the amount of args we pass to
rustc when the new build-dir layout is enabled.

Discussed in
rust-lang#17183 (comment),
this reduce the total `-L` arg length by ~16% in the case of Zed.

### How to test and review this PR?

* Tested this on Zed on windows and was able to compile it fine
* Created a testsuite case for this.
report entries

This captures the current behavior of the report.
…#17238)

# What Does This PR Try to Resolve?

Fixes rust-lang#17212

# How to Test and Review This PR?

The first commit adds tests pinning the current behavior.

The second commit fixes the underlying issues and updates the tests as
needed.
This is to be consistent with the remote http registry (sparse registry)
…ang#17240)

### What does this PR try to resolve?

Make the registry source's backend naming more consistent. Found this
confusing when working in this area. I don't remember what I was doing
here before the weekend. Maybe related to rust-lang#17226?

### How to test and review this PR?
The main benefit I see to `git fetch`s output is the progress
reporting.
The motivating reason to remove it unless progress is being reported is
that this is making it annoying to update the test suite to
`net.git-fetch-with-cli = true`.
This gives context for what the bool means
weihanglo and others added 29 commits July 30, 2026 12:21
Fixes fresh resolution of large graphs (including Cargo itself), which
previously failed or diverged because optional-dependency edges were
handled incorrectly.

Two coupled fixes:

* Gate optional-dependency edges on activation again (a prior change had
  made them feature-agnostic, which drew unactivated optional edges such
  as schemars -> url and produced dependency cycles).

* Activate the optional dependency for ANY `dep/feat` reference in an
  enabled feature, including weak `dep?/feat` ones. Cargo's v1 lock
  resolver always records the optional dependency in the graph for such
  references (the `weak` flag only gates the dependency's own implicit
  feature). Previously weak references pulled the target into the lock
  but dropped the edge to it (e.g. bstr -> serde via `serde?/std`).

With both fixes, `-Zpubgrub-resolver generate-lockfile` run from scratch
(no pre-existing lock) produces a byte-identical Cargo.lock to the
default resolver for Cargo's own ~5944-line dependency tree.

Adds graph-level (edge) comparison tests against the default resolver,
including regressions for the cycle and weak-dependency cases that the
package-set/SAT tests did not catch.
Captures architecture, the encoding, build/test instructions (Nix dev
shell), verified status (fresh byte-identical lockfile of Cargo itself),
the hard-won correctness insights (member seeding, activation-gated
edges, weak-dependency edge semantics, edge-level testing gap), reused
reference material, known limitations, and prioritized next steps for a
takeover.
Add CARGO_TEST_PUBGRUB env switch to the resolver-tests harness so the
convenience helpers (resolve / resolve_and_validated) route through
-Zpubgrub-resolver. This lets the entire curated suite (tests/resolve.rs,
tests/pubgrub.rs) be re-run against the PubGrub resolver for differential
validation:

    CARGO_TEST_PUBGRUB=1 cargo test -p resolver-tests

Default behavior is unchanged when the variable is unset.

Current results through pubgrub: tests/pubgrub.rs 28/28 pass;
tests/resolve.rs 35/37 pass, the 2 failures being error-message text
only (PubGrub's DefaultStringReporter vs Cargo-native messages), not
resolution-outcome differences.
Two curated tests assert Cargo's native conflict/no-match error text.
The PubGrub resolver formats errors via its derivation-tree reporter, so
these messages differ while the resolution outcome (error) is identical.
Gate just the message assertions on CARGO_TEST_PUBGRUB so the full
curated suite is a clean green gate on both resolvers (resolve.rs 37/37,
pubgrub.rs 28/28).
Update the handoff doc: the curated resolver suites now run through
pubgrub via CARGO_TEST_PUBGRUB (resolve.rs 37/37, pubgrub.rs 28/28),
mark next-step #1 done, and document the command.
Emit a tracing::debug! at the top of the pubgrub resolve path so its
activation can be confirmed via
`CARGO_LOG=cargo::core::resolver::pubgrub=debug` without temporary
instrumentation. Useful when verifying that -Zpubgrub-resolver actually
dispatches (a byte-identical lockfile alone cannot distinguish a working
resolver from a no-op flag).
Record the pristine-clone parity test (avoids this branch's Cargo.toml
confound), the dispatch proof via the new CARGO_LOG trace, the
single-resolve-pass note, and refresh the commit history.
…ve helper

Extract resolve_with_prefs_raw from resolve_with_global_context_raw so a
caller can supply its own VersionPreferences instead of always starting
from default(). Add prefs_from_lock to build preferences that reproduce a
previous resolution.

This is the resolver-level slice of the conservative-update flows
(building against an existing lock, cargo update -p, --precise), which all
reach the resolver as VersionPreferences. Pure refactor: the existing
helper now delegates with VersionPreferences::default(), so behavior is
unchanged.
Eight deterministic offline tests covering the conservative-update paths:
building against an untouched lock, cargo update -p <crate>, cargo update
(free everything), adding a dependency, shared transitive pinning,
stale-lock override, --precise, and a wildcard requirement.

Each resolves a manifest fresh, derives VersionPreferences from that
resolution, mutates one input, and asserts the pubgrub graph (nodes and
edges) matches the default resolver run with the same preferences. The
default resolver is the oracle, so the tests pin down that pubgrub honors
preferences exactly like Cargo already does rather than re-encoding the
logic under test.
Add prop_pubgrub_locked_reresolve_passes_validation: resolve fresh, feed
the result back as VersionPreferences, then re-resolve both with everything
kept (building against the lock) and with the requested crate freed
(cargo update -p). Each re-resolution must produce a SAT-valid solution and
agree with the default resolver on solvability.

Preferences only reorder the candidates pubgrub considers; they must never
let it accept an invalid solution nor fail when one exists. This is the
randomized counterpart to the deterministic pubgrub_update.rs suite.
Route every resolution through the PubGrub resolver when CARGO_TEST_PUBGRUB
is set, independent of the nightly-gated -Zpubgrub-resolver flag. This lets
the full integration testsuite (which shells out to a real cargo binary,
mostly on the stable channel) be re-run on PubGrub for differential
validation; child cargo processes inherit the env var. Never set in
production.
Update the handoff doc for the work in this batch:
- sections 2/8/9.3: conservative-update paths are now verified at the
  resolver level (pubgrub_update.rs + pubgrub_prop.rs), with the remaining
  end-to-end gap (ops::resolve glue, --precise registry pinning) called out;
- section 3: document the CARGO_TEST_PUBGRUB dispatch hook and how to run the
  full integration testsuite on PubGrub;
- section 9.1: proptests pass 5/5 under the env var at 256 cases;
- section 10: refresh the commit ledger;
- section 12 (new): record the first full-testsuite survey -- 3872 passed,
  4 failed, of which 3 are pre-existing env/snapshot failures and 1 is the
  known error-reporting limitation (typed ResolveError), not a misresolution.
The cargo::z_help::case snapshot predates the -Zpubgrub-resolver flag, so
the added help line (and the y-coordinate shifts it caused in the SVG)
failed the testsuite. Regenerated with SNAPSHOTS=overwrite.
The pubgrub resolver module and its resolver-tests were never run through
`cargo fmt`; CI's `cargo fmt --all --check` flagged them. Pure formatting,
no behavior change.
Cargo's clippy config disallows `std::env::var_os` (rust-lang#11588);
route the env reads through `GlobalContext::get_env_os` at the resolver
dispatch fork and in the resolver-tests helper. Two reads in tests/resolve.rs
have no GlobalContext in scope, so they keep direct env access under a scoped
`#[expect(clippy::disallowed_methods, reason = ...)]`, matching the idiom used
elsewhere in cargo (e.g. context/environment.rs).

Also rename the variable to `__CARGO_TEST_PUBGRUB` to match cargo's existing
test-only env convention (`__CARGO_TEST_*`), updating the doc-comment and the
design doc in lockstep so the documented and runtime names agree.

`GlobalContext` snapshots the process environment at construction, so child
cargo processes spawned by the testsuite still observe the variable.
`cargo doc` with `-D warnings` (CI's intra-doc-link gate) flagged two links:
`Provider::query` (no such method; the blocking poll loop lives in
`Provider::candidates`) and the ambiguous `super::resolve` (both a function
and a module -- disambiguate to the function with `()`).
The pubgrub solution reconstruction built each node as
`PackageId::new(name, version, bucket.source)`, using the source the
dependency named. But `[patch]` redirects a query to a summary from a
different source (e.g. a `crates-io` requirement satisfied by a path
patch); `PackageRegistry::query` returns that patched summary while the
bucket still names the registry source. The result was a lockfile entry
pointing at the registry for a package the patch actually provided, which
surfaced as "patch ... was not used in the crate graph" plus a checksum
mismatch against the patched (sourceless) package.

Use the selected summary's real `PackageId` as the node identity via the
new `bucket_pid` helper, threading it through the activation/edge/checksum
passes so the patched source is preserved. `resolve_child` now keys on the
bucket `(name, source)` but returns the actually-selected `PackageId`.

Reduces `patch::` testsuite failures under `__CARGO_TEST_PUBGRUB` from 43
to 25 (remaining failures are a separate spurious-index-update issue and
error-message formatting, not resolution). The bug only manifests with the
real `PackageRegistry` patch machinery, so it is covered by the existing
`tests/testsuite/patch.rs` suite rather than the bare resolver-tests
harness.
The pubgrub solution recorded the `[replace]` map (`from -> to`) but never
added the replacement target as a resolved package. Because
`Resolve::deps` redirects dependency edges through `replacement()`, the
build then walked to the replacement `PackageId` and failed with an
internal "couldn't find `<pkg>` in package set" panic.

Mirror the default resolver, which activates the replacement summary so it
becomes a graph node: for each node with a recorded replacement, add the
replacement target to the graph, summaries, and checksums.

Reduces `replace::` testsuite failures under `__CARGO_TEST_PUBGRUB` from 20
to 9; the remaining failures are the shared spurious-index-update and
error-message-context issues, not resolution. Covered by the existing
`tests/testsuite/replace.rs` suite (the bare resolver-tests harness cannot
model `[replace]`).
A feature listing itself (e.g. `default = ["default"]`) produced a
self-dependency in the pubgrub encoding, which the solver treats as
trivially satisfiable -- so the cycle was silently accepted and resolution
proceeded, diverging from the default resolver's `cyclic feature
dependency: feature `X` depends on itself` error.

Detect the self-reference while expanding a feature's values and return it
as an `Unavailable` reason, matching the default resolver's
`RequirementError::Cycle` (which fires only on `*dep_feat == feat`, i.e.
direct self-reference; mutual feature cycles like `A -> B -> A` remain
legal and are unaffected -- see resolver-tests `test_04_cyclic_features`).

The error *outcome* now matches; the exact message text is still wrapped by
pubgrub's derivation-tree reporter pending Cargo-native error reporting.
Widen `RequirementError` and `RequirementError::into_activate_error` to
`pub(in crate::core::resolver)` and derive `Clone`/`Debug`/`PartialEq`/`Eq`
plus a `Display` impl, so the pubgrub error-reporting bridge can reuse the
v1 resolver's exact message rendering rather than re-implementing it. No
behavior change; the type is still private to the resolver module.
Introduce a standalone `pubgrub/error.rs` module that turns a PubGrub
failure into a Cargo `ResolveError` instead of a generic `anyhow` wrapper
over PubGrub's `DefaultStringReporter`.

The bridge is deliberately thin and reuses the v1 resolver's own rendering:
- PubGrub's custom incompatibility metadata `M` changes from `String` to a
  structured `UnavailableReason`, so the provider records *why* a package is
  unusable (carrying the v1 `RequirementError`) rather than baking prose at
  the throw site;
- for the common single-cause shape (missing/cyclic feature, missing
  dependency) the bridge recovers the failing summary and calls
  `RequirementError::into_activate_error`, producing byte-identical text;
- it reconstructs a best-effort `package_path` from the derivation tree so
  member-level diagnostics work (fixes `member_errors`);
- anything else falls back to PubGrub's reporter, still wrapped as a typed
  `ResolveError` so downcasting callers keep working.

All user-facing prose now lives in this one module, which can be dropped or
rewritten without touching resolution. Fixes the `member_errors` downcast
failures and byte-matches the root-level feature errors; conflict-family
messages (which need full `activation_error` context) still fall back and
are left for a later change. No regressions in the deterministic or curated
suites.
Split the "no candidates found" branch of `activation_error` (no version
exists / yanked / typo'd name / etc.) into a standalone
`no_candidates_error` that takes the resolved `package_path` and a
pre-rendered `required_by` dependency-chain string instead of a
`ResolverContext`.

This is a pure extract-method refactor with no behavior change for the
default resolver; it lets the experimental PubGrub error bridge reuse the
exact same rendering (it recovers `package_path`/`required_by` from its own
derivation tree). Marked `!` and kept standalone so it can be cherry-picked
upstream independently of the PubGrub work.
Extend the PubGrub error bridge to the "no candidates found" family (no
matching package / no matching version / yanked / typo'd name). When the
derivation tree reduces to a `parent depends on a missing child` edge, the
bridge recovers the original `Dependency` from the parent's summary and the
parent path, then delegates to the v1 resolver's `no_candidates_error` for
byte-identical output.

Reconstruction is guarded: it only triggers when the child crate genuinely
has no candidate versions (not a version/feature conflict), otherwise it
falls back to PubGrub's reporter, so it never emits a wrong error.

Under `__CARGO_TEST_PUBGRUB`: `registry::` failures drop 36 -> 23 and
`package::` is fully green; no regressions in the deterministic or curated
suites. The offline-mode hint is omitted (the provider carries no
`GlobalContext`); conflict-family messages (candidates exist) still fall
back and are left for a later change.
- §4.2: add the `error.rs` bridge module to the layout table.
- §6: record the patch/replace node-identity and self-cyclic-feature
  insights.
- §8: mark `[patch]`/`[replace]` as handled (with the remaining
  spurious-`[UPDATING]` caveat) and rewrite the error-reporting entry to
  describe the standalone bridge and what still falls back.
- §9: error reporting is now in-progress; add the conflict-family and
  spurious-`[UPDATING]` follow-ups.
- §10: extend the commit ledger with this session's work.
- §12: replace the invalid survey number (it had run the default resolver
  via a misspelled env var) with a corrected before→after under nightly
  (233→177 failures), plus a methodology warning.
Generalize the no-candidates bridge from "crate is absent" to "no candidate
version satisfies the requirement". The trigger now recovers the failing
`Dependency` and checks whether any candidate matches its version req; if
none do, it delegates to `no_candidates_error` (which re-queries via
`alt_versions`/`rejected_versions`), producing byte-identical text for the
`candidate versions found which didn't match` / yanked / unavailable
families too. Cases where a candidate matches but conflicts with another
selection are deliberately excluded and still fall back.

Under `__CARGO_TEST_PUBGRUB`: `registry::` drops 20->15, with smaller drops
in `directory`/`update`; no regressions in the deterministic or curated
suites.
…rror

Split the "candidates exist but conflict" branch of `activation_error`
(semver clash / `links` collision / requested feature the dependency lacks)
into a standalone `version_conflict_error` that takes the resolved
`package_path`, the pre-rendered `required_by` parent chain, and a map of
`conflict_paths` for the conflicting packages, instead of a
`ResolverContext`.

Pure extract-method refactor with no behavior change for the default
resolver; it lets the experimental PubGrub error bridge reuse the exact same
rendering. Marked `!` and kept standalone so it can be cherry-picked
upstream independently of the PubGrub work. The now-unused `to_resolve_err`
closure in `activation_error` is removed (both branches are extracted).
When a dependency requests a feature its target lacks (e.g. `foo` depends on
`bar` with feature `bar` but `bar` has no such feature), render the v1
resolver's "... depends on X with feature Y but X does not have that
feature" conflict instead of the root-level "does not have the feature"
form.

The bridge locates the `Custom(child, Requirement(..))` leaf and the
`FromDependencyOf(parent, child)` edge (which sit at different tree depths,
so they are searched independently and matched by package), reuses
`into_activate_error(Some(parent), …)` to get Cargo's own `ConflictReason`
classification (missing / required-dep / `dep:`-syntax), and delegates to
`version_conflict_error`. Byte-identical for all five
`features::dependency_activates_*` cases plus
`test::cyclical_dep_with_missing_feature`. The remaining conflict families
(semver / links) still fall back. No regressions.
The branch was rebased onto upstream master after rust-lang#17230 and
rust-lang#17231, whose `src/` flattening moved every path this document referenced:
`src/cargo/core/resolver/` -> `src/resolver/`, `src/cargo/core/features.rs` ->
`src/workspace/features.rs`, and `src/doc/` -> `doc/`.

Refresh the stale paths, the `CARGO_LOG` target (now
`cargo::resolver::pubgrub`), and the unit-test filter, and replace the section
10 hashes, which the rebase invalidated wholesale.

Add section 13 recording how the rebase was actually done. A plain
`rebase --onto` does not survive this move: git relocates modified files but
leaves a newly added directory at its dead path. The working recipe rewrites
paths with `filter-branch --index-filter` first, and must leave upstream-owned
files alone to preserve the merge base. Both remaining conflicts were in
generated artifacts that have to be regenerated rather than hand-merged --
taking `--theirs` on the `-Z help` snapshot silently drops upstream new flags.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.