Haskell · Cabal · build tooling
Cabal 2.0: the Release that Reshaped Haskell Builds
Cabal 2.0 shipped in September 2017 alongside GHC 8.2, and in hindsight it is the release that set the direction of Haskell tooling for the next decade. It is the moment three things became real at once: nix-style local builds, Backpack, and a modern .cabal format with a saner version syntax. Here is what each one was, and where it stands today.
1. Nix-style local builds (new-build)
The headline feature. Earlier, sandboxes isolated dependencies per project but rebuilt everything from scratch and weren't reproducible. Cabal 2.0 promoted cabal new-build: a content-addressed model where each unique combination of package + version + flags + dependencies is built once, stored under a hash in a shared global store, and reused by every project whose build plan needs it.
$ cabal new-build # 2017 spelling
$ cabal build # 2026: this IS new-build — it became the default in Cabal 3.0 2026 status: the new-* prefix is gone. cabal build, cabal run, cabal repl and cabal test are all nix-style by default. The v2- aliases still exist for scripts; the old v1- commands are deprecated. What Cabal 2.0 introduced as opt-in is now simply how Cabal works.
2. Backpack — mixin packages and module signatures
Backpack added genuine parametric modularity to Haskell: a library can declare a module signature (an interface) and be type-checked against it without committing to a concrete implementation; a consumer then "mixes in" the implementation it wants. The classic example is a library written against an abstract Str signature that can be instantiated with String, strict Text or ByteString without code duplication.
-- in a .cabal file
library
signatures: Str
exposed-modules: Generic.Algorithm
library impl-text
mixins: indef-lib (Generic.Algorithm as Text.Algorithm)
requires (Str as Data.Text) 2026 status: Backpack works and is still supported, but it stayed a specialist tool rather than a mainstream pattern — type classes and ordinary parameterisation cover most needs, and tooling/IDE support for signatures remained thin. Reach for it when you genuinely need to compile the same code against several concrete types; otherwise you will rarely meet it.
3. A modern .cabal format
Setting cabal-version: 2.0 unlocked format improvements that are now everyday Haskell:
- The caret operator
^>=.build-depends: aeson ^>= 2.1means ">= 2.1 && < 2.2" — a compact, intention-revealing way to follow the PVP upper bound. It is everywhere in modern packages. autogen-modules, so generated modules likePaths_yourpkgare declared properly and shipped in the sdist.- Foreign libraries (the
foreign-librarystanza), letting Haskell code be built as a shared object for consumption by other languages. - Per-component
build-depends, so a package's library, tests and benchmarks can each have their own precise dependency sets.
2026 status: all standard. The recommendation now is to set cabal-version: 3.0 or later (it implies everything 2.0 gave you, plus common stanzas from 2.2, the ** wildcard for data files, and more). Use 2.0 only if you must support a very old toolchain.
4. The dependency solver got better
Cabal 2.0 shipped solver improvements that made it both faster and more likely to find a valid install plan in a deep dependency graph, with clearer explanations when no plan exists. The solver has continued to improve since, but 2.0 is where "the solver usually just works" started to be true for non-trivial projects.
Why it mattered
Before 2.0, the Haskell build story was "install globally and pray", patched by sandboxes. After 2.0, it became "declare what you want, get a reproducible, shared, isolated build" — the same mental model as Nix, Cargo or modern package managers elsewhere. Every cabal build you run in 2026 is the direct descendant of what 2.0 made the default path.
If you're upgrading an old project
# Bump the format and modernise bounds:
cabal-version: 3.0 # was 1.x or 2.0
# Replace hand-written ranges with caret bounds where it fits:
build-depends: base ^>= 4.18
, text ^>= 2.0
# Drop any v1-/sandbox era commands from CI; use:
cabal build --enable-tests
cabal test
cabal freeze # commit cabal.project.freeze for reproducibility