Announcing Gild, a formatter for Haskell package descriptions
Haskell package descriptions are known colloquially as *.cabal
files.
Every single Haskell project contains at least one.
In spite of this ubiquity, few tools exist for working with them.
Formatting in particular is anemic.
The first party cabal format
command is essentially broken and has been for some time.
Oleg Grenrus created cabal-fmt
to fill the gap.
That’s a great tool, but after using it for a while I’ve become annoyed with some of its limitations and idiosyncracies.
So I created Gild as an alternative formatter for Haskell package descriptions.
This post explains my motivation and describes how Gild differs from cabal-fmt
.
As a motivating example, consider this unformatted (and valid, but incomplete) package description:
CABAL-VERSION:2.2
name : example
version : 0
library
build-depends: base ^>= 4.19.0, bytestring ^>= 0.12.0,
ghc-options: -Weverything -Wno-implicit-prelude
if impl(ghc>=9.8)
ghc-options: -Wno-missing-role-annotations
Here’s how cabal-fmt
(version 0.1.10) formats that by default:
cabal-version: 2.2
name: example
version: 0
library
build-depends:
, base ^>=4.19.0
, bytestring ^>=0.12.0
ghc-options: -Weverything -Wno-implicit-prelude
if impl(ghc >=9.8)
ghc-options: -Wno-missing-role-annotations
That’s certainly an improvement, but I think it’s got some problems:
-
Fields values, like the name
example
and the version number0
, are aligned. This arguably looks better, but it means that diffs can get messy when new fields (like, say,extra-source-files
) are added because everything has to be lined back up again. -
Similarly, constraints on dependencies are aligned. This has the same problem. Imagine adding a new dependency on
case-insensitive
. Every other line would have to change. -
The GHC options are printed on one line since they fit. This is the same type of problem as the other two. Adding new GHC options will be fine until they spill over onto another line, at which point many lines will change.
Perhaps that’s just three ways of saying that cabal-fmt
’s output isn’t diff friendly.
And to its credit, you can pass --no-tabular
to cabal-fmt
in order to fix the second problem.
But there’s no way around the other two, and Oleg doesn’t seem to want to change it.
By comparison, here’s how Gild (version 1.0.0.0) formats that same package description:
cabal-version: 2.2
name: example
version: 0
library
build-depends:
base ^>=4.19.0,
bytestring ^>=0.12.0,
ghc-options:
-Weverything
-Wno-implicit-prelude
if impl(ghc >= 9.8)
ghc-options: -Wno-missing-role-annotations
The alignment issues are gone. GHC options are put on one line only if there’s exactly one of them. And, as a bonus, trailing commas are used instead of leading ones.
Many other things are the same though.
I think cabal-fmt
makes a lot of good decisions, so Gild follows suite.
For example there are blank spaces around sections (like library
), there are blank spaces after multi-line fields (like build-depends
), and two spaces are used for indentation.
That’s the quick pitch for Gild: like cabal-fmt
but better (in my opinion) and more diff friendly.
However Gild does have one other trick up its sleeve: module discovery.
One annoying problem with package descriptions is that they require you to explicitly list every module (in either exposed-modules
or other-modules
) in your package.
This is extremely tedious for people working on applications, because the modules are almost always just every Haskell file in some directory.
cabal-fmt
has a pragma for this:
-- cabal-fmt: expand src
exposed-modules: ...
This is a nice quality of life improvement, but unfortunately there’s a problem with it.
cabal-fmt
will only ever add modules.
If you delete or rename a module, cabal-fmt
won’t change it for you.
Gild offers a similar feature, but it handles modules being added, removed, or renamed:
-- cabal-gild: discover src
exposed-modules: ...
This works by completely ignoring the contents of the exposed-modules
(or other-modules
) field.
When you format the package description with Gild, it will discover modules in the src
directory and populate the exposed-modules
with whatever it finds.
This is similar to how hpack works, and I think many people consider this to be the killer feature of hpack.
This post has been a short summary of Gild and how it differs from cabal-fmt
.
cabal-fmt
does many other things that I didn’t mention here.
But if you’re looking for a way to format your Haskell package description, I’d recommend using Gild.
Please let me know what you think!
Open an issue on GitHub for any bugs or feature requests.
Thanks!