coldwa.st
All guidesProgrammingWebDataToolsDatabasesHaskellConceptsCabal & buildsToolchainCompilerPerformanceEditor & HLS

Haskell · Cabal · package management

How do you add a dependency in Cabal?

By ColdwastUpdated Aug 4, 20267 min read#haskell#cabal#packages
Tall stacks of second-hand books filling a bookshop aisle
Stacks of second-hand books crowding a bookshop aisle, piled higher than the shelves behind them.

The short answer is that you do not add a dependency with a command so much as with a line in a file. A Haskell package declares what it needs in its .cabal file, under build-depends, and everything else follows from that declaration.

That is worth stating plainly, because people arriving from npm or Cargo look for an install command that mutates a manifest, and then wonder why nothing they installed is visible to the compiler.

The field that actually matters: build-depends

Open your .cabal file and find the stanza for the thing you are building - a library, an executable, or a test-suite. Each one carries its own build-depends, and that is the point people miss most often.

library
  exposed-modules:  MyApp.Core
  build-depends:    base ^>=4.18
                  , containers ^>=0.6
                  , text ^>=2.0
  default-language: Haskell2010

Adding a package means adding a line here. The comma-first layout is a convention, not a requirement; it exists so that adding or removing a line touches one line rather than two.

Dependencies are per-stanza. A package your test suite needs does not belong in the library's build-depends, and a package your library needs is not automatically available to your executable unless the executable depends on the library itself. This is stricter than most ecosystems, and it is deliberate: it keeps test-only tooling out of what your users have to build.

What the caret operator really means

The ^>= you see everywhere is not decoration. It is a shorthand tied to the Haskell Package Versioning Policy, the PVP, and understanding it removes most of the guesswork about bounds.

Under the PVP a version reads A.B.C.D, where A.B together form the major version. Breaking changes bump A or B. Additions that cannot break existing code bump C. So ^>=1.2.3 means at least 1.2.3, and below the next major, which expands to >=1.2.3 && <1.3.

The consequence catches people out: in Haskell, 1.2 to 1.3 is a major bump, not a minor one. If you carry a mental model from semantic versioning, where the first number alone is major, you will write bounds that are far looser than you intended.

Identical wooden planks assembled into small structures, each layer resting on the one below
Identical wooden planks assembled into small structures, each layer resting on the one below it.

Why cabal update comes first

Cabal resolves dependencies against a local copy of the Hackage package index. If that copy is stale, a package published last week simply does not exist as far as your machine is concerned, and you get a resolution error that reads as if the package name were wrong.

cabal update
cabal build

Run cabal update when a package you know exists cannot be found, and after any long gap between sessions. It is the first thing to try, and it costs nothing.

Reading a resolution failure without panic

When the solver cannot satisfy every constraint at once, it reports the conflict rather than silently picking something. The message is dense, but it is telling you something specific: two of your dependencies want incompatible ranges of a third.

The honest options are few, and worth knowing before reaching for a flag:

Relax a bound you set yourself. If the too-narrow bound is in your own .cabal file, widening it is legitimate, provided you then build and test rather than assume.

Pick a different version of the package you are adding. Often an older release of the new dependency fits the constraints your project already has.

Use --allow-newer knowingly, and temporarily. It tells the solver to ignore upper bounds that other packages declared. Those bounds were written by maintainers who had a reason, so treat this as a diagnostic that tells you where the real conflict is, not as a fix you commit.

Pinning what you resolved

Resolution picks versions at a moment in time. To make that moment reproducible, freeze it:

cabal freeze

This writes a cabal.project.freeze file recording the exact versions chosen. Commit it when you want a build that behaves the same next month, which is the usual expectation for an application. Libraries are the opposite case: they should stay flexible so that whoever depends on them can resolve freely.

For a project spanning several packages, or one that needs a source dependency not on Hackage, that configuration belongs in cabal.project rather than in the .cabal file. Keeping the two straight is the single clearest way to stay oriented: the .cabal file describes one package, cabal.project describes how to build a set of them together.

The short version

Add a dependency by adding a line to build-depends, in the stanza that actually needs it. Bound it with ^>=, remembering that under the PVP the first two numbers together are the major version. Run cabal update before blaming a package for not existing. Freeze applications, leave libraries loose.

If you are still choosing your tooling, our comparison of Stack and Cabal covers that decision, and the GHCup installation guide sets up the toolchain this assumes you already have.

The build-depends field, the caret operator's expansion and the A.B major-version rule follow the Cabal user guide and the Haskell Package Versioning Policy, checked at the time of writing. Command availability varies with your cabal-install version; run cabal --version and consult the guide for your release before relying on a specific subcommand.