A local experiment for tracing Bundler 4.0's PubGrub resolver. It logs version choices, derived constraints, incompatibilities, conflicts, backtracks, and Bundler's full resolver retries as JSONL.
It patches Bundler internals and is expected to break when those internals change.
bundle plugin install bundler-resolver-logger --path /path/to/bundler-loggerThen wrap any Bundler command:
bundle resolve-log --output tmp/resolve.jsonl -- lock --update--include-versions adds every matching version at each choice point. This can
make the log much larger.
The plugin command starts a child Bundler process with the tracer loaded through
RUBYOPT. This leaves Bundler in charge of parsing and running lock,
install, or update.
You can skip the plugin and load the patch directly:
RUBYLIB=/path/to/bundler-logger/lib \
BUNDLER_RESOLVER_LOG=tmp/resolve.jsonl \
RUBYOPT=-rbundler/resolver_logger/bootstrap \
bundle lock --updateThe patch is in lib/bundler/resolver_logger/monkey_patches.rb. It prepends
modules to:
Bundler::ResolverBundler::PubGrub::VersionSolverBundler::PubGrub::PartialSolutionBundler::Resolver::StrategyBundler::PubGrub::Strategy
The main patched methods are:
- resolver start and retry relaxation
- solver initialization and completion
- package and version selection
- incompatibility creation and propagation
- decisions and derivations
- conflict resolution and backtracking
The patches publish raw Bundler and PubGrub objects through
Bundler::ResolverLogger::Emitter. Recorder subscribes to those events and
writes JSONL. Another subscriber can use the same events for a live
visualization without going through JSON.
Bundler retries and PubGrub backtracks are separate events. A Bundler retry creates a new solver after unlocking a dependency, allowing prereleases, or loading remote specs. A backtrack rewinds the current partial solution.
Each line is one JSON object with:
schema_versionsession_idseqeventtimestampelapsed_ms
Solver, resolution, and incompatibility IDs are stable within a session.
Events:
session_start,session_endresolution_start,resolution_relaxation,resolution_success,resolution_failuresolver_start,solver_initialized,solver_success,solver_failureincompatibilitycandidate,candidate_rejecteddecision,derivationpropagation_conflict,conflict,backtrack
Terms, ranges, assignments, incompatibility causes, and removed decisions are stored as objects rather than Bundler's debug strings. Candidate events also include the strategy score for every currently unsatisfied package.
The schema is currently version 1. Bump SCHEMA_VERSION for changes that would
make existing logs hard to read.
Selected fields from the backtracking example:
{"schema_version":1,"session_id":"example","seq":20,"event":"candidate","package":"database","version":{"display":"2.0.0","class":"Gem::Version","gem_version":"2.0.0"},"candidate_count":2,"decision_level":3,"solver_id":"s1"}
{"schema_version":1,"session_id":"example","seq":22,"event":"decision","package":"database","version":{"display":"2.0.0","class":"Gem::Version","gem_version":"2.0.0"},"decision_level":4,"assignment_index":8,"attempted_solutions":1,"solver_id":"s1"}
{"schema_version":1,"session_id":"example","seq":28,"event":"conflict","incompatibility":{"id":"i7","description":"shared < 3.0.0 depends on leaf = 2.0.0"},"decision_level":4,"solver_id":"s1"}
{"schema_version":1,"session_id":"example","seq":29,"event":"backtrack","from_level":4,"to_level":2,"removed_decisions":[{"package":"leaf","version":{"display":"1.0.0","class":"Gem::Version","gem_version":"1.0.0"}},{"package":"database","version":{"display":"2.0.0","class":"Gem::Version","gem_version":"2.0.0"}}],"solver_id":"s1"}
{"schema_version":1,"session_id":"example","seq":44,"event":"solver_success","attempted_solutions":2,"decisions":[{"package":"database","version":{"display":"1.0.0","class":"Gem::Version","gem_version":"1.0.0"}},{"package":"shared","version":{"display":"3.0.0","class":"Gem::Version","gem_version":"3.0.0"}},{"package":"app","version":{"display":"2.0.0","class":"Gem::Version","gem_version":"2.0.0"}}],"solver_id":"s1"}Full rows also include timestamps, ranges, choice scores, terms, and removed assignments.
ruby examples/pubgrub_backtrack.rbThis builds a small package graph whose first database choice conflicts with a
guard package. PubGrub backtracks from decision level 4 to 2 and succeeds on its
second attempt. The log is written to tmp/pubgrub-backtrack.jsonl.
- Choice logging asks the source for cached version lists again so it can record the strategy score. That adds some overhead.
- Logs are not redacted. They may contain private gem names, paths, source descriptions, requirements, and error backtraces.
- Recorder errors are swallowed so a tracing bug does not change resolution.
Set
BUNDLER_RESOLVER_LOG_DEBUG=1to print them.
Run the tests with:
bundle exec rake