One unsatisfiable dependency made pkg silently drop five others
A package I published declared a dependency that had never been published. Rather than fail, the solver relaxed the problem, produced a plan quietly missing five *unrelated* dependencies, installed it, and exited 0 — and the breakage surfaced nowhere near the cause.
I maintain an in-house package served from a private repository. It declares about seventy dependencies. One of them named a package that had never actually been published — a wrong name, inherited from a manifest recovered off a running machine rather than written by hand.
I expected that to fail loudly. Instead the package manager printed a single line about it mid-solve, produced a plan, ran it, and exited 0.
The plan was missing five hard dependencies. Not the broken one — five others, with no relationship to it: a template library, a DNS library, an on-disk database binding, a JSON parser, and a text editor.
Why it took so long to see
The missing template library was imported by the daemon that applies configuration on that machine. The daemon died on startup with an ImportError, and everything it drives stopped with it.
But the machine looked healthy. An unrelated resolver kept answering DNS out of its own process. The web interface still rendered its login page, because that is a web server and some static assets. Nothing surfaced an error anywhere a person would casually look. You had to try to change something before the dead service mattered.
I had also verified the release. Or thought I had:
pkg check -s <pkg> # clean
That validates file checksums — the files on disk match what the archive claims. It says nothing whatsoever about whether the declared dependencies were installed.
The check that actually finds this is one letter away:
pkg check -d <pkg>
That validates dependencies of installed packages. It named all five in one command, instantly, after I had spent a long time reading catalogues.
The mechanism
pkg resolves with a SAT solver. When it cannot satisfy every constraint it does not
necessarily refuse. It can relax the problem and return a different satisfiable plan —
and the relaxation is not reported as a failure. Critically, the packages it drops need
have no connection to the constraint that forced the relaxation.
That is what makes it nasty. The error message names one package; the damage lands on five others; and the symptom appears in a service that depends on none of them directly.
I confirmed the mechanism by changing exactly one thing in the next build — removing the bad dependency declaration, nothing else. The five reappeared immediately:
before: 20 python packages installed
after: 49
The check I thought was enough
Before publishing, I had verified that every dependency my package declared existed in the catalogue it was published into. That check passes here — and the release still broke.
Because the unsatisfiable dependency was not mine. It belonged to something I depended on. My one-level check said “all my dependencies resolve”, which was true, while one of them carried a constraint that could not be met.
So the guard has to be transitive, and it has to look at the plan rather than the declaration:
- every dependency you declare exists, at a version not older than you require
- recurse — every dependency’s own dependencies also resolve
- and then, on a real machine, diff what the resolver actually plans to install against what you declared
Only the third catches this class, because the first two describe the problem you gave the solver, not the answer it chose to return.
What I take from it
A package manager exiting 0 is not evidence that it installed what you declared. It is evidence that it found an answer it was willing to run. Those are different claims, and only one of them is the one you cared about.
The corollary is about blast radius. When a dependency graph fails to resolve cleanly, do not assume the fallout lands near the thing that failed. A solver optimises for returning some workable set, not for keeping your failure legible — so treat any unsatisfiable constraint anywhere in the closure as a release blocker, even when the plan it produced looks fine and the machine afterwards looks fine too.