6 min read
On this page

cabal vs Stack

The cabal-vs-Stack split was the defining ecosystem schism of Haskell from 2015 through about 2022. It was a practical disaster: tutorials assumed one or the other, packages worked in one and not the other, build systems on different machines diverged in ways nobody could reproduce. By 2026 the picture has settled, both tools are good, both are widely used, the choice is mostly a matter of preference and project conventions.

A short history helps. Cabal was the original build system. It had the famous "cabal hell" problem, install package A, then package B, find that B's dependencies have downgraded packages you need for A, and now nothing builds. Stack appeared in 2015 as a response, with Stackage curated snapshots that pinned a known-good set of package versions, eliminating most cabal hell.

Cabal then absorbed the lesson. The v2- commands (introduced around 2017, made default in cabal 3.0) gave cabal-install a per-project sandboxed build model, similar in effect to Stack's snapshots though achieved differently. As of cabal 3.10+ (and certainly by 2026), the v2- prefix is gone, those commands are just cabal build, cabal run, cabal test. The legacy global-install behavior is no longer the default.

What each one does

Cabal-install is the package manager and build driver. You write a .cabal file describing your package (or a package.yaml if you use hpack, which generates the cabal file). You run cabal build, cabal run, cabal test. Dependencies are resolved by a constraint solver against Hackage.

$ cabal init
$ cabal build
$ cabal test
$ cabal run myapp -- arg1 arg2

Cabal uses a per-project store (dist-newstyle/) and a shared global package store (~/.cabal/store/). Identical builds across projects share artifacts.

Stack is a higher-level tool. It uses cabal under the hood but adds:

  • LTS resolvers: a curated set of package versions known to build together (Stackage). You pin to e.g. lts-22.30 and Stack fetches that exact set.
  • A managed GHC: Stack downloads and manages GHC versions per-project. No need for ghcup or system GHC.
  • A stack.yaml config that's higher-level than cabal's freeze files.
$ stack new myapp
$ stack build
$ stack test
$ stack exec myapp -- arg1 arg2

Stack uses .stack-work/ per project and a shared ~/.stack/ for snapshots and tools.

How they differ in practice

Dependency selection:

  • Cabal runs a solver against Hackage's full version space. You can ask for any constraints you want; if a solution exists, it'll find one. The cost is that the solution might be one nobody has tested before.
  • Stack uses a snapshot. Every package in the snapshot has been built and tested together. You don't run a solver, you just use what's in the snapshot. Adding a package not in the snapshot requires an extra-deps entry.

Ergonomics:

  • Stack's stack new gives you a project that builds out of the box, GHC included. Onboarding a new developer is a one-liner.
  • Cabal's cabal init produces a similar project, but you also need GHC installed (typically via ghcup). The ghcup plus cabal-install combo is the standard 2026 setup if you go this route.

Compile times:

  • Both tools cache builds. With identical inputs, both are equally fast.
  • Cabal's solver can take noticeable time for large projects; Stack skips that step entirely.

CI:

  • Stack with a pinned LTS gives you the most reproducible CI by default. Pin the resolver, your CI is bit-for-bit reproducible across months.
  • Cabal can be reproducible too, with cabal freeze and a cabal.project.freeze file, but it requires more discipline.

A simple project: cabal version

-- myapp.cabal
cabal-version: 3.0
name:          myapp
version:       0.1.0.0
build-type:    Simple

library
  exposed-modules:  Lib
  build-depends:    base ^>= 4.18
                  , text
                  , aeson
  hs-source-dirs:   src
  default-language: Haskell2010

executable myapp
  main-is:          Main.hs
  build-depends:    base, myapp, text
  hs-source-dirs:   app
  default-language: Haskell2010

test-suite myapp-test
  type:             exitcode-stdio-1.0
  main-is:          Spec.hs
  build-depends:    base, myapp, hspec
  hs-source-dirs:   test
  default-language: Haskell2010
$ cabal build all
$ cabal test all

A cabal.project file at the repo root controls multi-package builds and project-level options:

packages: .
          ./services/api
          ./services/worker

For freezing dependencies:

$ cabal freeze

This writes cabal.project.freeze with exact versions. Commit it and CI uses the same resolution.

A simple project: Stack version

# stack.yaml
resolver: lts-22.30
packages:
  - .
extra-deps: []
# package.yaml (with hpack)
name: myapp
version: 0.1.0.0

dependencies:
  - base
  - text
  - aeson

library:
  source-dirs: src

executables:
  myapp:
    main: Main.hs
    source-dirs: app
    dependencies:
      - myapp

tests:
  myapp-test:
    main: Spec.hs
    source-dirs: test
    dependencies:
      - myapp
      - hspec
$ stack build
$ stack test

The resolver pins everything; CI is reproducible without any extra freeze step.

When to use which

Use Stack when:

  • You want the simplest possible onboarding (clone, stack build, done)
  • You want managed GHC versions per-project
  • You want maximum reproducibility with minimal discipline
  • You're working on something where dependency stability matters more than having the latest versions

Use cabal when:

  • You want the latest versions of dependencies (Stackage LTS is conservative)
  • You're publishing a library to Hackage (cabal is the canonical tool there)
  • You want to use newer GHC features faster (Stack snapshots lag GHC releases)
  • You're working in a project that's already cabal-based

For most production application projects in 2026, either works fine. The decision is usually settled by what the team is already using.

For library authors, cabal is increasingly the primary tool. Hackage uses cabal files; CI for libraries typically tests against multiple GHC versions which cabal-install handles cleanly.

For commercial Haskell shops, the picture varies. IOG (Cardano) uses cabal with extensive Nix integration. Mercury historically used Stack and has been moving toward cabal. Standard Chartered uses cabal. Tweag uses cabal-with-Nix. Galois uses both depending on the project.

What about Nix?

Nix is the third option, kind of. With haskell.nix (IOHK's Haskell-on-Nix tooling) or the older nixpkgs Haskell infrastructure, you can build Haskell projects with Nix and get truly reproducible builds, including the system libraries. This is overkill for most projects but invaluable for some, IOG's Cardano build is reproducible to the bit because of Nix.

In practice, Nix layers on top of either cabal or Stack rather than replacing them. You write a cabal.project file, then a flake.nix that drives the build with Nix.

Common Pitfalls

Mixing cabal and Stack in the same project. They each have their own cache and lock files. Pick one, document it in the README, and stay consistent.

Using cabal v1 commands. The v1- commands (the old global-install model) still exist for legacy compatibility but produce broken installs. Always use cabal build, never cabal v1-build. If a tutorial says cabal install foo (without --lib or a project context), it's probably outdated.

Stack's slow first build. The first stack build downloads GHC and the snapshot's package set, which can take 20+ minutes on a fresh machine. Subsequent builds are fast. CI caches need to preserve ~/.stack/.

Cabal's solver giving up. For complex projects, the solver sometimes can't find a valid resolution. The error message is usually helpful but not always. cabal build --allow-newer is sometimes the workaround, with the caveat that you're now running an untested combination.

Forgetting cabal update. Cabal's index is local; if you haven't updated in months, you won't see new package versions. Run cabal update periodically.

Key Takeaways

The cabal-vs-Stack war is over; both are viable tools in 2026. The split exists for historical reasons; nothing fundamental separates them anymore.

Stack with LTS resolvers is the easiest onboarding path. Pinned snapshots, managed GHC, reproducible by default.

Cabal-install (the v2-now-default flavor) is the canonical tool for library publishing and works well for applications too. It needs a bit more discipline (cabal freeze for reproducibility) but is otherwise on par with Stack.

For a fresh project, pick whichever your team prefers and commit to it. The decision is reversible but switching costs aren't trivial. Document the choice in your README.