Cookies Psst! Do you accept cookies?

We use cookies to enhance and personalise your experience.
Please accept our cookies. Checkout our Cookie Policy for more information.

How I switched from Stack to Cabal

Have you ever struggled to choose between stack and cabal for your Haskell projects?

I haven’t, because all the years ago I started with stack and never dared to switch to cabal.

💡 Technically, I did use it in the enterprise setting, but let’s say it’s irrelevant.

Stack has been my personal go-to build tool, basically for 3 reasons:

  • managing GHC versions;
  • managing dependency versions;
  • managing modules.

From what I’ve heard in the last couple of years, those 3 do not justify not using cabal. So, I wanted to give it a try.

🤔 Note that at the moment, I don’t worry about any optimizations and disk usage — I just want to make my life simpler and try out things.

Managing GHC versions

One nice thing about stack is that it by default manages ghc versions.

But also ghcup does that and I already use ghcup to manage stack itself, the haskell language server (hls), and even the compatible ghc version. Recently I’ve been hooking up stack to make it use ghcup-installed ghc versions. So, it feels simpler to just use ghcup.

ghcup

Yeah, it’s also nice that out of the box stack uses the correct ghc version when switching between projects — something that cabal doesn’t do. And it’s fine. Well, at least in my case — because I rarely actively use more than 1 or 2 GHC versions and don’t have to worry about it that often.

Regarding reproducible builds, you can still use the with-compiler option in the cabal.project file to pin down the ghc version.

with-compiler: ghc-9.4.5

💡 Note: You can also pin the base version.

Managing dependency versions

Another thing that stack manages is dependency versions. Instead of relying on dependency resolutions or picking versions by hand, you can use stackage snapshots.

These days, you can also use stackage snapshots with cabal — by using import in cabal.project:

packages: .
import: https://www.stackage.org/lts-21.7/cabal.config

💡 The cabal.project file comes in place of the stack.yaml file (for project configuration and options).

You can stop there, but you can also try a different workflow. Ask yourself what you want. Do you want reproducible builds? Do you want to make sure that libraries work together? Something else?

You can use cabal freeze to pin down the dependencies, which ensures (more) reproducible builds.

💡 It results in a cabal.project.freeze file **that we need to commit (or share).

I’ve used freeze- and lock-like workflows in a couple of previous jobs, and most of the time, it is pretty convenient, except for the days when I was the lucky one and had to deal with occasional problematic dependency bump.

If you don’t care about reproducibility, you don’t have to freeze anything. In either case, modern cabal is capable of constructing proper build plans. And if you worry about getting the “right” dependency versions, remember that stackage still exists and assists you even if you don’t use it directly — maintainers still want to make it into the snapshots and make sure that their packages work with others.

💡 As a side note: the in-between step or solution can be directly freezing a stackage snapshot:

curl https://www.stackage.org/lts-21.7/cabal.config > cabal.project.freeze

Managing modules

Okay, the actual reason I’ve been avoiding cabal is this: I hate enumerating the modules in cabal files by hand. And I didn’t mind using an extra tool just for that.

Then the other day I had an epiphany: hpack takes care of that boilerplate — not stack!

So, I can cut out the middle man — one middle man, but keep the other one…

Running hpack generates the .cabal file based on the package.yaml. You can run it manually, but it’s easy to forget, which makes it a good candidate for automation.

There are other alternatives for dealing with this and other boilerplate, but it’s good enough for me. If you’re less lazy and want to cut out hpack as well: you can generate the cabal file, patch up the version bounds, and then delete the hpack file.

Takeaways

To reiterate, if you want to switch from stack to cabal, you can generate the .cabal file from hpack’s package.yaml (and then either keep it or delete it) and swap stack.yaml for cabal.project.

💡 Depending on your prev. workflow, you might need to remove the .cabal file from gitignore.

So, will I choose stack pr cabal for my next Haskell projects? I don’t know.

But let me tell you about nix…

Last Stories

What's your thoughts?

Please Register or Login to your account to be able to submit your comment.