Skip to content

Use off-the-shelf dependency resolver instead of handrolled code#241

Draft
bbannier wants to merge 11 commits into
topic/bbannier/tracking-method-enumfrom
topic/bbannier/pubgrub
Draft

Use off-the-shelf dependency resolver instead of handrolled code#241
bbannier wants to merge 11 commits into
topic/bbannier/tracking-method-enumfrom
topic/bbannier/pubgrub

Conversation

@bbannier

Copy link
Copy Markdown
Member

We were using a hand-rolled dependency solver which had a number of issues, e.g.,

  • it only ever inspected dependencies at the current version of zkg.meta and used that for all versions which probably is not a valid assumption to make in general
  • when working with already installed packages, our algorithm could never suggest upgrades when installing additional packages would introduce dependency incompatibilities
  • error messages were created ad hoc and adding more detail to them was hard

This PR moves us to an off-the-shelf pubgrub-style library instead which can address all of the above shortcomings. While we might not exactly shave off lines of code (we add almost 500 loc), we now make things more explicit. We can also unit test dependency resolution better now.

Warning

All code changes in this branch were generated with Claude Sonnet 4.6. I reviewed all changes and am accountable.

bbannier added 11 commits July 20, 2026 21:51
`semver.Spec` raises `PendingDeprecationWarning` as of semantic-version 2.10
and will be removed in 3.1. `semver.SimpleSpec` is the direct replacement
with an identical interface.
The extraction sets up a clean seam for the next commit, which switches
to reading metadata via `git show` so deps can be fetched at arbitrary
versions rather than only the currently checked-out one.
`PackageInfo` only reflects the currently checked-out version. Reading
deps at a specific tag via `git show` is what lets the nab-resolver
provider fetch accurate constraints for every version it considers, not
just the one on disk.
The (raw_tag, coerced_version) pairing is needed in several places that
follow; keeping it in one place also makes the filtering logic testable.
Add the nab-resolver dependency and the resolver infrastructure in
`zeekpkg/_resolver.py`. `validate_dependencies` is not yet wired to use
it -- keeping the wiring separate lets each commit stay reviewable.
The behaviour is pinned by unit tests in `test_resolver.py` so the
wiring commit can focus purely on the call site.
Without seeding installed and builtin packages into the graph at the start,
the solver sees no version information for them and cannot enforce their
constraints against incoming dependency requests. Seeding upfront also
ensures zeek and zkg appear as nodes the solver can pin.
Captures the expected failure when a newly requested package requires a
newer version of an already-installed one. The baseline reflects
hard-pinning, which the following commit relaxes -- at that point the
test will start showing the upgraded version in the installed list.
Wire `_ZkgProvider` into `validate_dependencies` and replace the BFS
conflict-detection loop with a nab-resolver call.

The `constraints=` parameter is the key semantic difference from the old
approach: installed packages are pinned there rather than by filtering
which versions get registered, so the provider always sees the full
version history and the solver decides what fits.

Three non-obvious cases required hardening before this could work in
practice. Packages without any semver git tags need a fallback version
(we try `metadata_version`, installed state, `versions[-1]`, then `0.0.0`)
or the solver has nothing to pick and rejects every request. Directory-
backed packages have a `metadata_file` path but no git repository, so
`git.Repo()` would crash without an `InvalidGitRepositoryError` guard.
And nab-resolver formats unsatisfied ranges as `(-inf, X) | (X, +inf)`,
which we intercept in `narrow_for_display` to produce readable output.
Hard-pinning all installed packages blocked the common case where a new
package requires a newer version of something already installed. Directory-
backed packages and zeek/zkg stay hard-pinned because zkg has no mechanism
to upgrade them.
`manager.py` imported nab-resolver types directly. Move `_run_solver`
(resolver construction, DFS topo sort) into `_resolver.py` and
re-export `Range` from there so nab-resolver stays an implementation
detail of the resolver module. `manager.py` now only passes in
already-constructed `Range` values and a `lookup_dep` callable.
nab-resolver's error reporter renders constraints via two code paths:
_narrow_positive (which calls narrow_for_display) and direct interpolation
of Incompatibility.constraint_range in the CONSTRAINT-cause path. The
previous narrow_for_display approach only covered the first path, leaving
CONSTRAINT-cause lines with raw (-inf, X) sentinel notation.

_FmtRange overrides __str__ to produce operator-prefixed output and
overrides all Range operators to preserve the subclass through
composition. _constraint_to_range now returns _FmtRange, so every range
we construct formats itself at every render site. narrow_for_display
becomes a one-line fallback that wraps any library-internal Range the
resolver might surface directly.
@bbannier bbannier self-assigned this Jul 21, 2026
@bbannier bbannier changed the title Replace deprecated semver.Spec with semver.SimpleSpec in package.py Use off-the-shelf dependency resolver instead of handrolled code Jul 21, 2026
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.

1 participant