Announcing Gild, a formatter for Haskell package descriptions

by Taylor Fausak on

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 number 0, 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!