Understanding how hx manages dependencies.
Overview
hx manages dependencies through:
- Declaration — Dependencies declared in
.cabalfiles - Resolution — Finding compatible versions
- Locking — Pinning versions in
hx.lock - Installation — Downloading and building packages
Dependency Declaration
In .cabal Files
Dependencies are declared with version constraints:
build-depends:
, base ^>=4.18
, text >=2.0 && <3
, aeson ^>=2.1
, containersVersion Constraint Syntax
| Syntax | Meaning |
|---|---|
pkg | Any version |
pkg ==1.2.3 | Exact version |
pkg >=1.0 | Minimum version |
pkg <2.0 | Maximum version |
pkg >=1.0 && <2.0 | Range |
pkg ^>=1.2 | Major version (>=1.2 && <1.3) |
pkg ^>=1.2.3 | Minor version (>=1.2.3 && <1.3) |
Adding Dependencies
Use hx add:
hx add text
hx add "aeson ^>=2.1"Resolution
How Resolution Works
When you run hx build or hx lock:
- Read constraints from all
.cabalfiles - Query package index (Hackage)
- Run solver to find compatible versions
- Verify all constraints are satisfied
The Solver
hx uses Cabal’s dependency solver:
- Finds newest compatible versions
- Respects all constraints
- Minimizes dependency footprint
- Handles conflicts
Solver Failures
When resolution fails:
error: Could not resolve dependencies:
aeson-2.0 requires text >=1.0 && <2
my-project requires text >=2.0
fix: Adjust version constraints or update dependenciesSolutions:
- Update package with conflict:
hx update aeson - Relax constraints in
.cabal - Use
--allow-newer(escape hatch)
Locking
Purpose
The lockfile ensures reproducible builds:
- Same versions on all machines
- Same versions over time
- Explicit updates
Creating a Lockfile
hx lockThis creates hx.lock:
version = 1
generated = "2024-01-15T10:30:00Z"
[index]
hackage = "2024-01-15T00:00:00Z"
[[package]]
name = "aeson"
version = "2.1.2.1"
sha256 = "abc123..."
[[package]]
name = "text"
version = "2.0.2"
sha256 = "def456..."Updating the Lockfile
# Update all dependencies
hx lock --update
# Update specific packages
hx update aeson
hx lockUsing the Lockfile
# Install locked versions
hx sync
# Build uses locked versions automatically
hx buildPackage Sources
Hackage
Default package source:
[[package]]
name = "aeson"
version = "2.1.2.1"
source = "hackage"Git Repositories
For packages not on Hackage:
In cabal.project:
source-repository-package
type: git
location: https://github.com/user/package
tag: v1.0.0In hx.lock:
[[package]]
name = "custom-package"
version = "1.0.0"
source = "git"
git = "https://github.com/user/package"
commit = "abc123..."Local Packages
For local development:
In cabal.project:
packages:
.
../local-depIndex State
Pin the Hackage index for reproducibility:
# hx.toml
[dependencies]
index-state = "2024-01-15T00:00:00Z"This ensures:
- Same packages available everywhere
- No surprises from new uploads
- Reproducible resolution
Dependency Types
Regular Dependencies
Required for library and executables:
library
build-depends:
, base ^>=4.18
, text ^>=2.0Test Dependencies
Only for test suites:
test-suite my-tests
build-depends:
, base ^>=4.18
, my-lib
, hspec ^>=2.11 -- test-only
, QuickCheck ^>=2.14Add with --dev:
hx add --dev hspec QuickCheckBenchmark Dependencies
Only for benchmarks:
benchmark my-bench
build-depends:
, base
, my-lib
, criterion ^>=1.6Build Tools
Tools used during build:
build-tool-depends:
, alex
, happyTransitive Dependencies
Dependencies have their own dependencies:
my-project
└── aeson (direct)
├── text (transitive)
├── containers (transitive)
└── vector (transitive)
└── primitive (transitive)The lockfile includes ALL versions:
[[package]]
name = "aeson"
version = "2.1.2.1"
[[package]]
name = "text"
version = "2.0.2"
[[package]]
name = "vector"
version = "0.13.0.0"Version Bounds
Why Bounds Matter
Upper bounds prevent breakage:
-- Good: won't break with aeson-3.0
build-depends: aeson ^>=2.1
-- Risky: will break when aeson API changes
build-depends: aeson >=2.0PVP (Package Versioning Policy)
Haskell follows PVP:
A.B.C.D
│ │ │ └─ Patch (no API change)
│ │ └─── Minor (backwards compatible)
│ └───── Major (breaking change)
└─────── Major (breaking change)Use ^>= for automatic PVP bounds:
-- Allows 2.1.*, blocks 2.2.*
build-depends: aeson ^>=2.1Conflict Resolution
Understanding Conflicts
Conflicts occur when packages require incompatible versions:
Package A requires text >=1.0 && <2.0
Package B requires text >=2.0Resolution Strategies
Update conflicting package
hx update package-aUse newer versions
hx lock --updateRelax constraints (in your code)
-- If you control the package build-depends: text >=1.0 && <3.0Use allow-newer (escape hatch)
-- cabal.project allow-newer: package-a:text