A reference for dependency resolution algorithms and strategies across different package managers.
48
6 commits
updated Dec 11, 2025
A reference for dependency resolution algorithms and strategies across different package managers.
Background: Package manager and Dependency hell (Wikipedia). For academic treatment, see Dependency Solving Is Still Hard, but We Are Getting Better at It (Abate et al., 2020).
Package managers generally fall into a few algorithmic families.
Some registries only list one version per package in their index (APT, DNF, Pacman, Homebrew, Alpine), which simplifies resolution since there's no version selection. Others (npm, PyPI, RubyGems, crates.io, Maven Central) list all historical versions, requiring the resolver to choose among candidates.
Language package registries rarely remove old versions or packages. System package repositories are more curated, partly to avoid conflicts and unresolvable dependency trees.
| Algorithm Family | Description | Package Managers |
|---|---|---|
| SAT Solving | Translates dependencies to boolean satisfiability | Composer, DNF/Zypper/Conda (libsolv), Eclipse P2 (Sat4j), opam (via CUDF) |
| ASP | Answer Set Programming for optimization | Spack |
| PubGrub | Conflict-driven clause learning with good error messages | Dart pub, Poetry, uv, SwiftPM |
| Molinillo | Backtracking with forward checking | Bundler, CocoaPods, RubyGems |
| Backtracking | Try versions, backtrack on conflict | pip, Cargo, Cabal, ansible-galaxy (collections) |
| Minimal Version Selection | Always use minimum satisfying version | Go modules |
| Deduplication with nesting | Deduplicate where possible, nest on conflict | npm, Yarn, pnpm, Bun |
| Version Mediation | Pick based on graph position or declaration order | Maven, Gradle, NuGet |
| Scoring/Priority | Assigns scores to packages, resolves by priority | APT/aptitude |
| Ad-hoc | Custom graph traversal without formal solver | cpanm |
| Bundled | Dependencies included at build time, no runtime resolution | Snap |
npm (v7+) uses Arborist for dependency resolution, building a logical graph of dependencies overlaid on a physical tree of folders.
Algorithm: Maximally naive deduplication with nested fallback
How it works:
Trade-offs:
References:
Algorithm: Similar to npm (deduplication with nested fallback)
How it works:
yarn.lock ensures deterministic installs across machinesTrade-offs:
References:
Yarn Berry has its own resolver implementation (complete rewrite from Yarn Classic).
Algorithm: Own implementation; deduplication with nested fallback
How it works:
@yarnpkg/core (source).pnp.cjs lookup file instead of node_modulesTrade-offs:
node_modules tree to writenode_modules to existReferences:
pnpm has its own resolver implementation with similar semantics to npm.
Algorithm: Own implementation; deduplication with isolated installation
How it works:
@pnpm/resolve-dependencies and related packages)node_modules/ with only declared dependenciesTrade-offs:
References:
Bun has its own resolver implementation written in Zig.
Algorithm: Own implementation; npm-compatible semantics
How it works:
bun.lockb) for fast parsingTrade-offs:
node_modules layoutReferences:
pip (v20.3+) uses a backtracking resolver based on the resolvelib library.
Algorithm: Backtracking with on-demand metadata fetching
How it works:
Trade-offs:
References:
Poetry uses a PubGrub-based resolver (Mixology library).
Algorithm: PubGrub (conflict-driven clause learning)
How it works:
Trade-offs:
Limitations:
References:
uv (from Astral) uses pubgrub-rs, a Rust implementation of PubGrub.
Algorithm: PubGrub with performance optimizations
How it works:
Trade-offs:
References:
Conda now uses libsolv (via libmamba) as its default solver. Mamba is a faster reimplementation of conda using the same solver.
Algorithm: SAT solving via libsolv
How it works:
History: Conda's original solver used pycosat (PicoSAT wrapper). libmamba became the default in conda 23.10 (2023) due to significant performance improvements.
Trade-offs:
References:
Bundler and RubyGems use Molinillo, a backtracking resolver with forward checking.
Algorithm: Molinillo (backtracking with forward checking and conflict-driven backjumping)
How it works:
Trade-offs:
References:
Cargo uses a backtracking resolver that tries the highest compatible version first.
Algorithm: Backtracking with semver-aware version selection
How it works:
ActivationsKey to track: only one semver-compatible version allowedlinks field ensures native libraries linked only onceVersion interpretation:
1.2.3 means >=1.2.3, <2.0.00.2.3 means >=0.2.3, <0.3.00.0.3 means >=0.0.3, <0.0.4Trade-offs:
References:
cpanm and CPAN.pm use ad-hoc dependency resolution that is neither correct nor complete according to academic analysis.
Algorithm: Ad-hoc depth-first traversal
How it works:
>=, <=, >, <, ==, !=) and can be combined with commas@INC wins)--scandeps option outputs the dependency tree without installingVersioning: Free-form strings rather than semantic versioning. Version comparison uses Perl's version.pm rules.
Trade-offs:
conflicts relationship exists in the spec but is rarely used and discouragedCarton adds lockfile support (cpanfile.snapshot) on top of cpanm for reproducible installs, but does not change the underlying resolution algorithm.
References:
Go modules use Minimal Version Selection (MVS).
Algorithm: Minimal Version Selection
How it works:
go.sum records cryptographic checksums for verificationTrade-offs:
go get -u to upgradeReferences:
Maven uses "nearest definition first" with depth-based mediation.
Algorithm: Nearest definition wins (breadth-first, first declaration breaks ties)
How it works:
<dependencyManagement> section takes precedence over mediationTrade-offs:
Workarounds:
maven-enforcer-plugin with requireUpperBoundDeps rule<dependencyManagement> to pin versionsReferences:
Gradle defaults to "newest version wins" with configurable conflict resolution.
Algorithm: Highest version wins (configurable)
How it works:
failOnVersionConflict() is enabledConfiguration options:
failOnVersionConflict(): Fail build on any conflictforce 'group:artifact:version': Force specific versionpreferProjectModules(): Prefer local project over binaryconstraints {}: Suggest versions without requiringTrade-offs:
References:
Composer uses a SAT solver ported from openSUSE's libzypp.
Algorithm: SAT solving (DPLL/CDCL)
How it works:
Clause generation:
A requires B: (-A|B1|B2|...)A conflicts with B: (-A|-B)Trade-offs:
Performance note: Experiments showed native SAT solver (Plingeling) solving same formula 633x faster than Composer's PHP implementation.
References:
NuGet defaults to lowest matching version for dependencies.
Algorithm: Lowest applicable version (configurable)
How it works:
>= 2.1, picks 2.1 if available, or next lowest6.0.*) select highest matching versionConfiguration:
-DependencyVersion switch: Lowest (default), HighestPatch, HighestMinor, Highestpackages.config projects, not PackageReferenceRationale: Lowest version is most likely to be compatible since that's what the package author tested with.
Trade-offs:
References:
pub uses PubGrub, an algorithm designed specifically for Dart.
Algorithm: PubGrub (conflict-driven clause learning)
How it works:
PubGrub was designed to provide clear error messages explaining why resolution failed.
Trade-offs:
References:
Algorithm: Highest version, single version per package
How it works:
Options:
:override option forces a dependency version to be used everywhereTrade-offs:
References:
Cabal uses a modular solver with configurable backtracking.
Algorithm: Modular solver with backjumping
How it works:
--reorder-goals heuristic can speed up some resolutions--count-conflicts prefers goals involved in many conflicts (default)Configuration:
max-backjumps: Maximum backtrack steps (-1 for unlimited, default 2000)--reorder-goals: Try to order goals more efficiently--count-conflicts: Prioritize conflict-heavy packagesTrade-offs:
References:
opam is notable for being one of the few package managers to fully embrace external CUDF solvers, as advocated by the Mancoosi research project.
Algorithm: External CUDF solvers (mccs built-in, aspcud, packup, or custom)
How it works:
User preferences: opam allows custom solver criteria. For example:
opam install merlin --criteria="-changed,-removed"
This minimizes changes to other installed packages.
Trade-offs:
References:
CocoaPods uses Molinillo, the same resolver as Bundler.
Algorithm: Molinillo (backtracking with forward checking)
See Bundler section for algorithm details.
References:
Swift Package Manager uses PubGrub for dependency resolution.
Algorithm: PubGrub
How it works:
Package.resolved records resolved versionsTrade-offs:
References:
APT uses a scoring-based resolver with immediate dependency resolution.
Algorithm: Scoring with immediate resolution
How it works:
Trade-offs:
References:
DNF uses libsolv, a SAT-based dependency resolver from openSUSE.
Algorithm: SAT solving via libsolv
How it works:
History: DNF replaced YUM in Fedora 22+ and RHEL 8+. YUM's ad-hoc dependency checking was slow and unpredictable; libsolv provides modern SAT-based resolution.
Trade-offs:
References:
Pacman uses libalpm for package management. Limited public documentation on the resolution algorithm internals.
How it works:
Trade-offs:
References:
Homebrew has a simpler model than most package managers: one version of each formula at a time.
Algorithm: Single version per formula, topological sort for install order
How it works:
Trade-offs:
References:
Nix avoids traditional resolution by having each package explicitly specify exact versions of its dependencies.
Algorithm: No resolution needed - dependencies are explicit
How it works:
Trade-offs:
References:
Snaps bundle their dependencies rather than resolving them at install time.
Algorithm: No runtime resolution - dependencies bundled at build time
How it works:
Trade-offs:
References:
Algorithm: Unknown (limited documentation on internals)
How it works:
References:
Spack is a package manager for HPC that uses Answer Set Programming (ASP) for dependency resolution.
Algorithm: Answer Set Programming (ASP) via Clingo
How it works:
Why ASP over SAT: SAT finds any satisfying solution; ASP finds an optimal solution according to user-defined criteria.
Trade-offs:
References:
| Package Manager | Algorithm | Default Version | Lockfile | Multiple Versions |
|---|---|---|---|---|
| npm | Dedup + nesting | Highest | Yes | Yes (nested) |
| Yarn Classic | Dedup + nesting | Highest | Yes | Yes (nested) |
| Yarn Berry | Dedup + nesting | Highest | Yes | Yes (isolated) |
| pnpm | Dedup + nesting | Highest | Yes | Yes (isolated) |
| Bun | Dedup + nesting | Highest | Yes | Yes (nested) |
| pip | Backtracking | Highest | No* | No |
| Poetry | PubGrub | Highest | Yes | No |
| uv | PubGrub | Highest | Yes | No |
| Conda/Mamba | SAT (libsolv) | Highest | Yes | No |
| Bundler | Molinillo | Highest | Yes | No |
| Cargo | Backtracking | Highest | Yes | Yes (major) |
| cpanm/Carton | Ad-hoc (depth-first) | Highest | Yes (Carton) | No |
| Go | MVS | Lowest | No | No |
| Maven | Nearest | Nearest | No | No |
| Gradle | Newest | Highest | Yes | No |
| Composer | SAT | Highest | Yes | No |
| NuGet | Lowest | Lowest | Yes | No |
| pub | PubGrub | Highest | Yes | No |
| Mix/Hex | Latest | Highest | Yes | No |
| Cabal | Modular | Highest | Yes | No |
| opam | CUDF (external) | Highest | Yes | No |
| CocoaPods | Molinillo | Highest | Yes | No |
| SwiftPM | PubGrub | Highest | Yes | No |
| APT | Scoring | Highest | No | No |
| DNF | SAT (libsolv) | Highest | No | No |
| Pacman | Unknown | Latest | No | No |
| Homebrew | Formula-based | Latest | No | No |
| Nix | Explicit (no resolution) | Specified | Yes (flakes) | Yes (by design) |
| Snap | Bundled (no resolution) | N/A | No | N/A |
| Alpine APK | Unknown | Latest | No | No |
| Spack | ASP (Clingo) | Optimized | Yes | Yes |
[!tip] * pip cannot install from standard lock files;
pip freeze > requirements.txtwith pinned versions serves a similar purpose; additionally,constraint.txtfiles can be used to restrict dependency resolution; pip v25.3 is able to produce the tool-agnostic ecosystem standardpylock.tomllock file accepted through PEP 751 at the beginning of 2025 — but cannot use it during installation (as of Dec 2025). Other installers in the Python ecosystem are able to install frompylock.tomlfiles.
Select the highest version that satisfies all constraints. Used by most modern package managers.
Pros: Gets latest features and security fixes Cons: More likely to introduce breaking changes
Select the lowest version that satisfies all constraints.
Pros: More stable, uses what was tested Cons: May miss security patches
Select version based on proximity in dependency graph.
Pros: Gives control to direct dependencies Cons: Order-dependent, can be surprising
Try to find versions that satisfy multiple dependents; nest different versions when conflicts arise.
Pros: Handles conflicts without failing, disk efficient when versions align Cons: Phantom dependencies possible (except pnpm/Yarn PnP), multiple copies when conflicts exist
Each package only sees its declared dependencies.
Pros: No phantom dependencies Cons: Some packages may break if they rely on hoisted dependencies
A phantom dependency is when package A can require('B') even though A does not list B in its dependencies, simply because B was installed for another package and hoisted to a common ancestor.
Affected by: npm, Yarn Classic Prevented by: pnpm, Yarn PnP (strict mode)
This causes problems when:
A 2020 academic survey (Dependency Solving Is Still Hard) evaluated package managers on two properties:
Their findings:
| Package Manager | Solver Type | Correct | Complete | User Preferences |
|---|---|---|---|---|
| npm | ad-hoc | ? | ? | No |
| opam | CUDF (external) | Yes | Yes | Yes |
| pip | ad-hoc | Yes | Yes | No |
| NuGet | ad-hoc | Yes | Yes | No |
| Maven | ad-hoc | Yes | Yes | With plugins |
| RubyGems | ad-hoc | ? | ? | ? |
| Cargo | ad-hoc | Yes | Yes | No |
| CPAN | ad-hoc | No | No | No |
| Cabal | ? | No | No | No |
| Debian (apt) | CUDF (external) | Yes | Yes | Yes |
| RedHat (dnf) | libzypp SAT | Yes | Yes | ? |
| Eclipse P2 | Sat4j | Yes | Yes | Yes |
The paper notes that SAT-based solvers (libsolv, Sat4j) and CUDF-based external solvers are generally both correct and complete, while ad-hoc implementations vary.
General dependency resolution with arbitrary version constraints is NP-hard (reducible to SAT) when you must select exactly one version of each package. However, not all package managers face this complexity:
For a comprehensive bibliography, see Package Management Papers.
A reference for dependency resolution algorithms and strategies across different package managers.
48
6 commits
updated Dec 11, 2025
A reference for dependency resolution algorithms and strategies across different package managers.
Background: Package manager and Dependency hell (Wikipedia). For academic treatment, see Dependency Solving Is Still Hard, but We Are Getting Better at It (Abate et al., 2020).
Package managers generally fall into a few algorithmic families.
Some registries only list one version per package in their index (APT, DNF, Pacman, Homebrew, Alpine), which simplifies resolution since there's no version selection. Others (npm, PyPI, RubyGems, crates.io, Maven Central) list all historical versions, requiring the resolver to choose among candidates.
Language package registries rarely remove old versions or packages. System package repositories are more curated, partly to avoid conflicts and unresolvable dependency trees.
| Algorithm Family | Description | Package Managers |
|---|---|---|
| SAT Solving | Translates dependencies to boolean satisfiability | Composer, DNF/Zypper/Conda (libsolv), Eclipse P2 (Sat4j), opam (via CUDF) |
| ASP | Answer Set Programming for optimization | Spack |
| PubGrub | Conflict-driven clause learning with good error messages | Dart pub, Poetry, uv, SwiftPM |
| Molinillo | Backtracking with forward checking | Bundler, CocoaPods, RubyGems |
| Backtracking | Try versions, backtrack on conflict | pip, Cargo, Cabal, ansible-galaxy (collections) |
| Minimal Version Selection | Always use minimum satisfying version | Go modules |
| Deduplication with nesting | Deduplicate where possible, nest on conflict | npm, Yarn, pnpm, Bun |
| Version Mediation | Pick based on graph position or declaration order | Maven, Gradle, NuGet |
| Scoring/Priority | Assigns scores to packages, resolves by priority | APT/aptitude |
| Ad-hoc | Custom graph traversal without formal solver | cpanm |
| Bundled | Dependencies included at build time, no runtime resolution | Snap |
npm (v7+) uses Arborist for dependency resolution, building a logical graph of dependencies overlaid on a physical tree of folders.
Algorithm: Maximally naive deduplication with nested fallback
How it works:
Trade-offs:
References:
Algorithm: Similar to npm (deduplication with nested fallback)
How it works:
yarn.lock ensures deterministic installs across machinesTrade-offs:
References:
Yarn Berry has its own resolver implementation (complete rewrite from Yarn Classic).
Algorithm: Own implementation; deduplication with nested fallback
How it works:
@yarnpkg/core (source).pnp.cjs lookup file instead of node_modulesTrade-offs:
node_modules tree to writenode_modules to existReferences:
pnpm has its own resolver implementation with similar semantics to npm.
Algorithm: Own implementation; deduplication with isolated installation
How it works:
@pnpm/resolve-dependencies and related packages)node_modules/ with only declared dependenciesTrade-offs:
References:
Bun has its own resolver implementation written in Zig.
Algorithm: Own implementation; npm-compatible semantics
How it works:
bun.lockb) for fast parsingTrade-offs:
node_modules layoutReferences:
pip (v20.3+) uses a backtracking resolver based on the resolvelib library.
Algorithm: Backtracking with on-demand metadata fetching
How it works:
Trade-offs:
References:
Poetry uses a PubGrub-based resolver (Mixology library).
Algorithm: PubGrub (conflict-driven clause learning)
How it works:
Trade-offs:
Limitations:
References:
uv (from Astral) uses pubgrub-rs, a Rust implementation of PubGrub.
Algorithm: PubGrub with performance optimizations
How it works:
Trade-offs:
References:
Conda now uses libsolv (via libmamba) as its default solver. Mamba is a faster reimplementation of conda using the same solver.
Algorithm: SAT solving via libsolv
How it works:
History: Conda's original solver used pycosat (PicoSAT wrapper). libmamba became the default in conda 23.10 (2023) due to significant performance improvements.
Trade-offs:
References:
Bundler and RubyGems use Molinillo, a backtracking resolver with forward checking.
Algorithm: Molinillo (backtracking with forward checking and conflict-driven backjumping)
How it works:
Trade-offs:
References:
Cargo uses a backtracking resolver that tries the highest compatible version first.
Algorithm: Backtracking with semver-aware version selection
How it works:
ActivationsKey to track: only one semver-compatible version allowedlinks field ensures native libraries linked only onceVersion interpretation:
1.2.3 means >=1.2.3, <2.0.00.2.3 means >=0.2.3, <0.3.00.0.3 means >=0.0.3, <0.0.4Trade-offs:
References:
cpanm and CPAN.pm use ad-hoc dependency resolution that is neither correct nor complete according to academic analysis.
Algorithm: Ad-hoc depth-first traversal
How it works:
>=, <=, >, <, ==, !=) and can be combined with commas@INC wins)--scandeps option outputs the dependency tree without installingVersioning: Free-form strings rather than semantic versioning. Version comparison uses Perl's version.pm rules.
Trade-offs:
conflicts relationship exists in the spec but is rarely used and discouragedCarton adds lockfile support (cpanfile.snapshot) on top of cpanm for reproducible installs, but does not change the underlying resolution algorithm.
References:
Go modules use Minimal Version Selection (MVS).
Algorithm: Minimal Version Selection
How it works:
go.sum records cryptographic checksums for verificationTrade-offs:
go get -u to upgradeReferences:
Maven uses "nearest definition first" with depth-based mediation.
Algorithm: Nearest definition wins (breadth-first, first declaration breaks ties)
How it works:
<dependencyManagement> section takes precedence over mediationTrade-offs:
Workarounds:
maven-enforcer-plugin with requireUpperBoundDeps rule<dependencyManagement> to pin versionsReferences:
Gradle defaults to "newest version wins" with configurable conflict resolution.
Algorithm: Highest version wins (configurable)
How it works:
failOnVersionConflict() is enabledConfiguration options:
failOnVersionConflict(): Fail build on any conflictforce 'group:artifact:version': Force specific versionpreferProjectModules(): Prefer local project over binaryconstraints {}: Suggest versions without requiringTrade-offs:
References:
Composer uses a SAT solver ported from openSUSE's libzypp.
Algorithm: SAT solving (DPLL/CDCL)
How it works:
Clause generation:
A requires B: (-A|B1|B2|...)A conflicts with B: (-A|-B)Trade-offs:
Performance note: Experiments showed native SAT solver (Plingeling) solving same formula 633x faster than Composer's PHP implementation.
References:
NuGet defaults to lowest matching version for dependencies.
Algorithm: Lowest applicable version (configurable)
How it works:
>= 2.1, picks 2.1 if available, or next lowest6.0.*) select highest matching versionConfiguration:
-DependencyVersion switch: Lowest (default), HighestPatch, HighestMinor, Highestpackages.config projects, not PackageReferenceRationale: Lowest version is most likely to be compatible since that's what the package author tested with.
Trade-offs:
References:
pub uses PubGrub, an algorithm designed specifically for Dart.
Algorithm: PubGrub (conflict-driven clause learning)
How it works:
PubGrub was designed to provide clear error messages explaining why resolution failed.
Trade-offs:
References:
Algorithm: Highest version, single version per package
How it works:
Options:
:override option forces a dependency version to be used everywhereTrade-offs:
References:
Cabal uses a modular solver with configurable backtracking.
Algorithm: Modular solver with backjumping
How it works:
--reorder-goals heuristic can speed up some resolutions--count-conflicts prefers goals involved in many conflicts (default)Configuration:
max-backjumps: Maximum backtrack steps (-1 for unlimited, default 2000)--reorder-goals: Try to order goals more efficiently--count-conflicts: Prioritize conflict-heavy packagesTrade-offs:
References:
opam is notable for being one of the few package managers to fully embrace external CUDF solvers, as advocated by the Mancoosi research project.
Algorithm: External CUDF solvers (mccs built-in, aspcud, packup, or custom)
How it works:
User preferences: opam allows custom solver criteria. For example:
opam install merlin --criteria="-changed,-removed"
This minimizes changes to other installed packages.
Trade-offs:
References:
CocoaPods uses Molinillo, the same resolver as Bundler.
Algorithm: Molinillo (backtracking with forward checking)
See Bundler section for algorithm details.
References:
Swift Package Manager uses PubGrub for dependency resolution.
Algorithm: PubGrub
How it works:
Package.resolved records resolved versionsTrade-offs:
References:
APT uses a scoring-based resolver with immediate dependency resolution.
Algorithm: Scoring with immediate resolution
How it works:
Trade-offs:
References:
DNF uses libsolv, a SAT-based dependency resolver from openSUSE.
Algorithm: SAT solving via libsolv
How it works:
History: DNF replaced YUM in Fedora 22+ and RHEL 8+. YUM's ad-hoc dependency checking was slow and unpredictable; libsolv provides modern SAT-based resolution.
Trade-offs:
References:
Pacman uses libalpm for package management. Limited public documentation on the resolution algorithm internals.
How it works:
Trade-offs:
References:
Homebrew has a simpler model than most package managers: one version of each formula at a time.
Algorithm: Single version per formula, topological sort for install order
How it works:
Trade-offs:
References:
Nix avoids traditional resolution by having each package explicitly specify exact versions of its dependencies.
Algorithm: No resolution needed - dependencies are explicit
How it works:
Trade-offs:
References:
Snaps bundle their dependencies rather than resolving them at install time.
Algorithm: No runtime resolution - dependencies bundled at build time
How it works:
Trade-offs:
References:
Algorithm: Unknown (limited documentation on internals)
How it works:
References:
Spack is a package manager for HPC that uses Answer Set Programming (ASP) for dependency resolution.
Algorithm: Answer Set Programming (ASP) via Clingo
How it works:
Why ASP over SAT: SAT finds any satisfying solution; ASP finds an optimal solution according to user-defined criteria.
Trade-offs:
References:
| Package Manager | Algorithm | Default Version | Lockfile | Multiple Versions |
|---|---|---|---|---|
| npm | Dedup + nesting | Highest | Yes | Yes (nested) |
| Yarn Classic | Dedup + nesting | Highest | Yes | Yes (nested) |
| Yarn Berry | Dedup + nesting | Highest | Yes | Yes (isolated) |
| pnpm | Dedup + nesting | Highest | Yes | Yes (isolated) |
| Bun | Dedup + nesting | Highest | Yes | Yes (nested) |
| pip | Backtracking | Highest | No* | No |
| Poetry | PubGrub | Highest | Yes | No |
| uv | PubGrub | Highest | Yes | No |
| Conda/Mamba | SAT (libsolv) | Highest | Yes | No |
| Bundler | Molinillo | Highest | Yes | No |
| Cargo | Backtracking | Highest | Yes | Yes (major) |
| cpanm/Carton | Ad-hoc (depth-first) | Highest | Yes (Carton) | No |
| Go | MVS | Lowest | No | No |
| Maven | Nearest | Nearest | No | No |
| Gradle | Newest | Highest | Yes | No |
| Composer | SAT | Highest | Yes | No |
| NuGet | Lowest | Lowest | Yes | No |
| pub | PubGrub | Highest | Yes | No |
| Mix/Hex | Latest | Highest | Yes | No |
| Cabal | Modular | Highest | Yes | No |
| opam | CUDF (external) | Highest | Yes | No |
| CocoaPods | Molinillo | Highest | Yes | No |
| SwiftPM | PubGrub | Highest | Yes | No |
| APT | Scoring | Highest | No | No |
| DNF | SAT (libsolv) | Highest | No | No |
| Pacman | Unknown | Latest | No | No |
| Homebrew | Formula-based | Latest | No | No |
| Nix | Explicit (no resolution) | Specified | Yes (flakes) | Yes (by design) |
| Snap | Bundled (no resolution) | N/A | No | N/A |
| Alpine APK | Unknown | Latest | No | No |
| Spack | ASP (Clingo) | Optimized | Yes | Yes |
[!tip] * pip cannot install from standard lock files;
pip freeze > requirements.txtwith pinned versions serves a similar purpose; additionally,constraint.txtfiles can be used to restrict dependency resolution; pip v25.3 is able to produce the tool-agnostic ecosystem standardpylock.tomllock file accepted through PEP 751 at the beginning of 2025 — but cannot use it during installation (as of Dec 2025). Other installers in the Python ecosystem are able to install frompylock.tomlfiles.
Select the highest version that satisfies all constraints. Used by most modern package managers.
Pros: Gets latest features and security fixes Cons: More likely to introduce breaking changes
Select the lowest version that satisfies all constraints.
Pros: More stable, uses what was tested Cons: May miss security patches
Select version based on proximity in dependency graph.
Pros: Gives control to direct dependencies Cons: Order-dependent, can be surprising
Try to find versions that satisfy multiple dependents; nest different versions when conflicts arise.
Pros: Handles conflicts without failing, disk efficient when versions align Cons: Phantom dependencies possible (except pnpm/Yarn PnP), multiple copies when conflicts exist
Each package only sees its declared dependencies.
Pros: No phantom dependencies Cons: Some packages may break if they rely on hoisted dependencies
A phantom dependency is when package A can require('B') even though A does not list B in its dependencies, simply because B was installed for another package and hoisted to a common ancestor.
Affected by: npm, Yarn Classic Prevented by: pnpm, Yarn PnP (strict mode)
This causes problems when:
A 2020 academic survey (Dependency Solving Is Still Hard) evaluated package managers on two properties:
Their findings:
| Package Manager | Solver Type | Correct | Complete | User Preferences |
|---|---|---|---|---|
| npm | ad-hoc | ? | ? | No |
| opam | CUDF (external) | Yes | Yes | Yes |
| pip | ad-hoc | Yes | Yes | No |
| NuGet | ad-hoc | Yes | Yes | No |
| Maven | ad-hoc | Yes | Yes | With plugins |
| RubyGems | ad-hoc | ? | ? | ? |
| Cargo | ad-hoc | Yes | Yes | No |
| CPAN | ad-hoc | No | No | No |
| Cabal | ? | No | No | No |
| Debian (apt) | CUDF (external) | Yes | Yes | Yes |
| RedHat (dnf) | libzypp SAT | Yes | Yes | ? |
| Eclipse P2 | Sat4j | Yes | Yes | Yes |
The paper notes that SAT-based solvers (libsolv, Sat4j) and CUDF-based external solvers are generally both correct and complete, while ad-hoc implementations vary.
General dependency resolution with arbitrary version constraints is NP-hard (reducible to SAT) when you must select exactly one version of each package. However, not all package managers face this complexity:
For a comprehensive bibliography, see Package Management Papers.