# cabal-matrix
`cabal-matrix` is a matrix builder for cabal. It lets you specify in a flexible
way a list of configurations in which something can be built, such as compiler
versions, or dependency version restrictions. It will then run the builds in
parallel (locally), and present the results in a TUI table where the specific
outcomes can be more closely examined. This is useful for inventing and
correcting dependency bounds for your package, as well as finding dependency
issues in other packages and fixing them.
This README is a tutorial that walks through some example use cases. A reference
of CLI options can be obtained by running `cabal-matrix --help`.
# Installation
```sh
cabal install cabal-matrix
```
# Building Your Package
Suppose you have a package `foo` that you're developing as part of a cabal
project (with a `cabal.project` file in current directory), such that your
package can be built by running:
```sh
cabal build foo
```
You can then build your package with `cabal-matrix` using:
```sh
cabal-matrix -j1 foo --default
```
This will open a TUI with a single cell representing the single build you're
doing. You can use `<Tab>` and then `<Space>` to view and follow the output of
the build, and `q` or `<Esc>` to exit.
# Inventing Dependency Bounds
Suppose your package build-depends on `bytestring`, but you don't have any
version bounds in the cabal file, because you're not sure which to put. To
figure this out, we can simply try building our package with each version of
`bytestring` and see if it works or not! This can be achieved by running:
```sh
cabal-matrix -j1 foo --package bytestring=">= 0"
```
This may take a while to complete, and you can try using a higher value of `-j`
to run multiple builds in parallel, but in the end your table will look like
this (you can use arrow keys to scroll around):
```
bytestring│0.9 0.9.0.1 0.9.0.2 0.9.0.3 0.9.0.4 0.9.1.0 0.9
──────────┼────────────────────────────────────────────────────────────────────▶
│
──────────┼────────────────────────────────────────────────────────────────────▶
│no plan no plan no plan no plan no plan no plan no
│
│
│
│
<Esc>/Q: quit │ X: axes │ ◀▲▼▶: select cell │ <Tab>: focus cells/cols/rows
```
- A `build ok` result in a particular column means that your package built
successfully against that version of `bytestring`, meaning your package de
facto supports it.
- A `no plan` result is considered a tentative success. It means cabal could not
come up with a build plan, because dependency bounds have prevented this
configuration from being usable.
- A `build fail` result means that we tried building but there was an error.
You can select the cell with arrow keys, and use `<Space>` to view the build
output to see what the error was.
- A `deps fail` result means there was a problem building the dependencies.
This could indicate a bug in `bytestring`, or in a package that is between
`foo` and `bytestring` in the dependency graph. Or it could be that your build
environment is not working properly.
The goal of setting version bounds is turning `build fail`s into `no plan`s:
you can choose the `bytestring` version range to be one that includes all the
`build ok`s, and excludes all `build fail`s.
You could also change your package to avoid whatever it was that caused the
`build fail`s in the first place, and thus widen the possible version range.
# Validating Dependency Bounds
Once you add bounds in the cabal file, or if you had pre-existing bounds, you
can verify that these bounds are not too loose. Suppose your bounds were
`build-depends: bytestring >=0.11.4 && <0.13`. You can then run:
```sh
cabal-matrix -j1 foo --package bytestring=">=0.11.4 && <0.13"
```
You could even run the same command as before (`bytestring=">= 0"`), but this
will save time (and table space!) by not focusing on configurations that are
definitely outside the possible range.
In the result, you should expect to see only `build ok`s and `no plan`s. As
before, `build fail` suggests that your bounds aren't tight enough, and
`deps fail` suggests an issue upstream (but you should verify).
Note however that `no plan` is only a tentative success. It could be caused by
bounds created by you, or by constraints not created by you. In the latter case
you run the risk of those constraints disappearing, and uncovering a
`build fail` underneath. A very common way this can happen is with compiler
versions, for example if you're using GHC 9.12.2, then any configuration
involving `bytestring-0.11.4.0` will be `no-plan` because `bytestring` itself
doesn't support that compiler. If I switch to GHC 9.8.4, `bytestring-0.11.4.0`
becomes buildable.
If you intend to support multiple compilers in your package, and you have e.g.
`ghc-9.8.4` and `ghc-9.12.2` on your `PATH`, you can test this scenario using:
```sh
cabal-matrix -j1 foo \
--compiler ghc-9.8.4,ghc-9.12.2 \
--times \
--package bytestring=">=0.11.4 && <0.13"
```
This will create a build matrix that looks like this:
```
COMPILER│ghc-9.8.4 ghc-9.12.2
──────────┼─────────────────────────────────────────────────────────────────────
bytestring│
──────────┼─────────────────────────────────────────────────────────────────────
0.11.4.0 │build ok no plan
0.11.5.0 │build ok no plan
0.11.5.1 │build ok no plan
0.11.5.2 │build ok no plan
0.11.5.3 ▼build ok build ok
<Esc>/Q: quit │ X: axes │ ◀▲▼▶: select cell │ <Tab>: focus cells/cols/rows
```
If you like the view transposed, you can press `X` and configure "COMPILER" to
use the Vertical axis and "bytestring" to use the Horizontal axis instead.
Note that in the invocation above, `--compiler ...` produces a list of
compilers, and `--package bytestring=...` produces a list of `bytestring`
versions, and the `--times` operator between them combines the two lists using a
cartesian product.
If your package also depends on e.g. `text`, you may want to similarly check
which versions of `text` it actually builds with, with e.g.
```sh
cabal-matrix -j1 foo \
--compiler ghc-9.8.4,ghc-9.12.2 \
--times \
--package bytestring="==0.11.5.*" \
--times \
--package text="==2.0.*"
```
This will run a build for each of the 2 compilers, for each of the 5 selected
`bytestring` versions, and each of the 3 selected `text` versions for a total
of 2 * 5 * 3 = 30 builds.
For most situations this is wasteful though, as independent packages don't
usually interact in a way that will only cause a problem for a combination of
versions. For dependent packages, such as `text` depending on `bytestring`, the
incompatibility will usually manifest when building `text`, which is why
`text` will have its own dependency bounds for `bytestring`. And if those are
incorrect then that's a problem in `text`.
So instead it is usually sufficient to only constrain one package at a time.
You can do this with `cabal-matrix` like so:
```sh
cabal-matrix -j1 foo \
--compiler ghc-9.8.4,ghc-9.12.2 \
--times \
--[ \
--package bytestring="==0.11.5.*" \
--add \
--package text="==2.0.*" \
--]
```
Note the use of `--add` here instead of `--times` -- this concatenates the two
lists instead of taking their cartesian product. Also note the use of `--[`
`--]` to group the operands together, otherwise operations are executed from
left to right (a.k.a. left associatively).
The above invocation will run 2 * (5 + 3) = 16 builds instead. You can press `X`
and configure "bytestring" and "text" to use the Vertical axis, and then the
table will look like this:
```
COMPILER│ghc-9.8.4 ghc-9.12.2
────────────────┼───────────────────────────────────────────────────────────────
bytestring text │
────────────────┼───────────────────────────────────────────────────────────────
0.11.5.0 │build ok no plan
0.11.5.1 │build ok no plan
0.11.5.2 │build ok no plan
0.11.5.3 │build ok build ok
0.11.5.4 │build ok build ok
2.0 │no plan no plan
2.0.1│build ok no plan
2.0.2│no plan no plan
<Esc>/Q: quit │ X: axes │ ◀▲▼▶: select cell │ <Tab>: focus cells/cols/rows
```
Note that there are unusual circumstances where a simple `--add` will be
insufficient, particularly if you use `#ifdef`s or cabal flags to faciliate a
migration for some breaking change. It's your job to know whether your package
uses such mechanisms, to decide how much they should be tested, and to find a
balance between practicality and the combinatorial explosion of configurations.
# Parallelism
There are two ways to specify parallelism: you can specify `-jN` to tell
`cabal-matrix` to run N cabals in parallel, and you can also use `--option -jM`
to tell `cabal-matrix` to *forward* the option `-jM` to cabal, causing cabal to
use M threads in turn. In combination this may require up to N * M cores.
I recommend the combination `-jN --option -j1`, where `N` is the number of cores
on your machine; because planning isn't threaded, and because `--option -j1`
also causes cabal to log each module being compiled, rather than only entire
packages.
# Building Someone Else's Package
If you run into `deps fail`, or otherwise find a package on Hackage that fails
to build because its dependency bounds are incorrect, you can easily debug this
with `cabal-matrix` too.
You could run e.g.:
```sh
cabal-matrix -j1 text --package text=">=0" --times --package bytestring=">=0"
```
to try every `text` version against every `bytestring` version. However if
you're running this from your project folder, then the constraints coming from
your project's packages will prevent some versions from being available,
possibly masking some `build fail`s as `no plan`s.
Instead you can move out of your project folder into a temporary directory, and
use `--install-lib`:
```sh
cd /tmp
cabal-matrix -j1 --install-lib text \
--package text=">=0" --times --package bytestring=">=0"
```
For a typical yet concrete example, we can time travel to the past using
`--option --index-state=2025-10-01T00:00:00Z`, and try building `tar` against
`directory`:
```sh
cabal-matrix -j1 --install-lib tar \
--option --index-state=2025-10-01T00:00:00Z \
--compiler ghc-9.2.8 \
--times \
--package tar='>=0.6' \
--times \
--package directory='>=1.3'
```
There is a clear cut corner of `build fail`s at `tar >=0.6.4` and
`directory <1.3.8`:
```
COMPILER│ghc-9.2.8 ghc-9.2.8 ghc-9.2.8 ghc-9.2.8 ghc-9.2.8 ghc-9.2.8
tar │0.6.0.0 0.6.1.0 0.6.2.0 0.6.3.0 0.6.4.0 0.7.0.0
─────────┼──────────────────────────────────────────────────────────────────────
directory│
─────────┼──────────────────────────────────────────────────────────────────────
1.3.5.0 ▲no plan no plan no plan no plan no plan no plan
1.3.6.0 │no plan no plan no plan no plan no plan no plan
1.3.6.1 │no plan no plan no plan no plan no plan no plan
1.3.6.2 │build ok build ok build ok build ok build fail build fail
1.3.7.0 │build ok build ok build ok build ok build fail build fail
1.3.7.1 │build ok build ok build ok build ok build fail build fail
1.3.8.0 │build ok build ok build ok build ok build ok build ok
1.3.8.1 │build ok build ok build ok build ok build ok build ok
1.3.8.2 ▼build ok build ok build ok build ok build ok build ok
<Esc>/Q: quit │ X: axes │ ◀▲▼▶: select cell │ <Tab>: focus cells/cols/rows
```
Using arrow keys to navigate to each `build fail` and `<Space>` to view the
output, we see that they are all caused by the same issue of importing
`System.Directory.OsPath`. Double checking with the documentation we see that
the module was added in `directory-1.3.8.0`, and the fact that `tar` uses it
without declaring `build-depends: directory >=1.3.8` is a mistake.
This specific issue has been already reported in
https://github.com/haskell/tar/issues/103 and fixed.
Note that we chose to use a sufficiently old compiler to cover a wide range of
`directory` versions. If we had used GHC 9.8.4, we would have noticed a
`build fail` but our investigation would have been complicated by the fact that
`directory-1.3.8.0` is not buildable, and it's not immediately clear whether the
fix is `directory >=1.3.8` or `directory >=1.3.8.1`. If we had used GHC 9.12.2,
we would not have noticed the problem at all, as `directory <1.3.8` is not
buildable.
In general, some fiddling with GHC versions may be required, even mixing
different GHC versions in the same build, e.g.:
```
--[ --compiler ghc-7.10.3 --times --package directory="<1.3.8" --] \
--add \
--[ --compiler ghc-9.6.7 --times --package directory=">=1.3.8" --]
```
# Loosening Dependency Bounds
If you have dependency bounds, but you're unsure if they're too tight and
can/should be relaxed, you can forward (using `--option`) `--allow-newer` and
`--allow-older` to cabal:
```sh
cabal-matrix -j1 foo --package bytestring=">= 0" \
--option --allow-newer=foo:bytestring --option --allow-older=foo:bytestring
```
This will tell cabal to ignore the `build-depends` constraints that your package
`foo` has declared on `bytestring`.
With these options, some configurations that were previously `no plan`s may
become `build fail`s, meaning the constraint was correct in excluding those.
Other configurations may become `build ok`, meaning the constraint could be
relaxed to include those. Finally, some `no plan`s may remain as such, meaning
the configuration was excluded for some other reason. You can read the `no plan`
output to find out in more detail.