How I switched from Stack to Cabal
Tags: haskell, functional, beginners, tooling
Published: 2024-05-06 01:39:17

📹 Hate reading articles? Check out [the complementary video](https://youtu.be/gkIHWM4cQig), which covers the same content.

---

Have you ever struggled to choose between [stack](https://docs.haskellstack.org/en/stable/) and [cabal](https://cabal.readthedocs.io/en/3.4/getting-started.html) 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](https://www.haskell.org/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s1is0ofls1wqdrcuyzaf.jpg)


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](https://cabal.readthedocs.io/en/stable/cabal-project-description-file.html#cfg-field-with-compiler) 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](https://github.com/sol/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](https://docs.haskellstack.org/en/stable/) pr [cabal](https://cabal.readthedocs.io/en/3.4/getting-started.html) for my next Haskell projects? I don’t know.

--- 

{% embed https://youtu.be/gkIHWM4cQig
 %}