š¹Ā Hate reading articles? Check out the complementary video, which covers the same content.
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.
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.