🧰 Making the most of Cabal

Posted on June 6, 2020

Multiple GHC versions

You might have multiple stack.yaml files for different GHC versions. ghcide and haskell-language-server do this with corresponding stack-8.x.y.yaml files that allow them to be built with a myriad of different GHCs, by using specific snapshots. If you're using Cabal though, you manage your GHC installations yourself. Your installed ghcs (and ghc-pkgs) probably have their versions suffixed to them, where ghc is just a symlink to a specific version of choice.

$ ls -1 /usr/local/bin/ghc*
/usr/local/bin/ghc
/usr/local/bin/ghc-8.10.1
/usr/local/bin/ghc-8.6.5
/usr/local/bin/ghc-8.8.3
/usr/local/bin/ghc-pkg
/usr/local/bin/ghc-pkg-8.10.1
/usr/local/bin/ghc-pkg-8.6.5
/usr/local/bin/ghc-pkg-8.8.3

You can tell Cabal to build your project with a specific GHC1 with the --with-compiler flag, or -w for short:

$ cabal build -wghc-8.8.3

If you want Cabal to remember this so you don't need to pass the flag every time, put it in your cabal.project

packages: .
with-compiler: ghc-8.8.3

"But my project is in source control and built by other people. How can I get Cabal to use a specific GHC version without forcing everyone else to build it with the same GHC version?", you ask. That's exactly what cabal.project.local is for. It should be added to your .gitignore, and is intended for your individual local changes. Either make one by hand, or use cabal configure to generate one.

$ cabal configure -wghc-8.8.3
...
$ cat cabal.project.local
with-compiler: ghc-8.8.3

Snapshots

Snapshots are Stack's flagship feature which ensures that all packages are buildable with each other at any given time. You can kind of get the same thing in Cabal with --index-state:

$ cabal configure --index-state=2019-11-24T17:23:36Z
...
$ cat cabal.project.local
index-state: 2019-11-24T17:23:36Z

If you ever find yourself reminiscing about the good old days when your package constraints were solvable, put one of these into your cabal.project[.local] and Cabal will use the index of Hackage at that point in time. Or the closest one available, if there's no exact match for it. It's not exactly the same thing as a Stack snapshot, since not all packages are guaranteed to build with each other. But you won't be surprised with sudden breakages whenever a dependency has a new version published.

If you already had a cabal.project.local when you ran cabal.configure, you might have also noticed there's now a cabal.project.local~: It's a backup of the old one before you configured, just in case.

Freezing

You can go a step further and instead of persisting the state of Hackage, you can persist the exact versions of each package that Cabal's solver picked out.

$ cabal freeze
Wrote freeze file: /Users/luke/foo/cabal.project.freeze

The freeze file is actually just another cabal.project, but with the version of every package locked into place in a list of constraints. Feel free to check this into source control. It's the Cabal equivalent of a package-lock.json

$ cat cabal.project.freeze
constraints: any.Cabal ==3.0.1.0,
             any.QuickCheck ==2.14,
             QuickCheck +templatehaskell,
             any.StateVar ==1.2,
             any.aeson ==1.4.7.1,
...

jlombera pointed out that you can in fact, pretty much replicate Stackage in Cabal by freezing with a specific Stackage LTS, by downloading a config file provided:

curl https://www.stackage.org/lts-15.15/cabal.config > cabal.project.freeze

Local repositories

You've probably been pulling in all your packages from Hackage, which is the default package repository. However you can roll your own if you need a place to store your private packages. If you have the URL to it, you can tell cabal to search for packages in it by editing your ~/.cabal/config:

repository hackage.haskell.org
  url: http://hackage.haskell.org/
repository luxurious-private-repo
  url: http://pkgs.lukelau.me/

If you don't need to share your packages with anyone else, you can use a local folder of source distributions (sdists) as a repository. First create your sdists2

$ cd ~/foo
$ tar -czf foo-0.1.0.0.tar.gz !(dist-newstyle)
$ cd ~/bar # or if you have a git repository
$ git archive HEAD -o foo-0.1.0.0.tar.gz
$ cd ~/baz # or like a normal person
$ cabal sdist
Wrote tarball sdist to
/Users/luke/baz/dist-newstyle/sdist/baz-0.1.0.0.tar.gz

Then place them into a folder

$ ls ~/local-repo
bar-0.1.0.0.tar.gz foo-0.1.0.0.tar.gz

Then add the repository to your config file

repository my-local-repository
  url: file+noindex:///Users/luke/local-repo

Now you can start pulling them in as dependencies. Unlike adding a folder to the cabal.project packages field, this acts as a repository so you can store multiple sdists of a package with different versions.

Source repository packages

Often times if you're waiting for a dependency to be fixed upstream, you might find yourself adding a git submodule to tide yourself over until a new version is uploaded to Hackage. Cabal can help you avoid submodules by pulling in packages from remote version control systems. Just specify them inside your cabal.project:

source-repository-package
    type: git
    location: https://github.com/haskell/haskell-ide-engine.git
    branch: quick-fix
    subdir: hie-plugin-api

It supports quite a few version control systems: Mercurial, Darcs and Bazaar just to name a few.

Vendoring

Thanks to Faucelme for mentioning that there are many other ways to vendor packages – that is tell Cabal to use a local version of a package in place of the version on Hackage. You can simply add the package to your cabal.project. A quick way to make changes is to use cabal get to fetch and unpack a package you want to tweak:

$ cabal get aeson
Unpacking to aeson-1.5.1.0/
$ cat cabal.project
packages:
  . -- the package in the current directory
  ../aeson-1.5.1.0 -- the local copy of aeson

Or if the tweaked package is hosted somewhere, you can even specify a HTTP URL!

Scripts

runghc/runhaskell is great for scripting in a pinch.

#!/usr/bin/env runghc
main = putStrLn "hey"
$ chmod u+x Script.hs
$ ./Script.hs
"hey"

But the fun pretty much stops as soon as you need to use an external package.

#!/usr/bin/env runghc
{-# LANGUAGE TypeApplications #-}
import Data.Aeson -- this isn't in the core libraries!
import Data.Text.Lazy (pack)
import Data.Text.Lazy.Encoding (encodeUtf8)
main = getLine >>= print . decode @[Int] . encodeUtf8 . pack
$ echo [1,2,3] | ./Script.hs

Script.hs:3:1: error:
    Could not find module ‘Data.Aeson’
    Perhaps you meant Data.Version (from base-4.14.0.0)
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
3 | import Data.Aeson 
  | ^^^^^^^^^^^^^^^^^

You could try installing the library locally and then running it:

$ cabal install aeson --lib
$ echo [1,2,3] | ./Script.hs 
Just [1,2,3]

But if you want to then share the script others will also have to make sure aeson is installed locally. And where does the installed library even end up? And how do you uninstall it? It gets installed into the Cabal store and then registered in Cabal's GHC package database, under the name shortened name sn

~/.cabal/store/ghc-8.10.1/sn-1.5.1.0-ec3f28f3/
~/.cabal/store/ghc-8.10.1/package.db/sn-1.5.1.0-ec3f28f3.conf

and then exposed through the default GHC environment

$ cat ~/.ghc/x86_64-darwin-8.10.1/environments/default
clear-package-db
global-package-db
package-db /Users/luke/.cabal/store/ghc-8.10.1/package.db
package-id ghc-8.10.1
package-id bytestring-0.10.10.0
...
package-id sn-1.5.1.0-ec3f28f3

If you want to make your script portable, avoid cluttering your environment and if this is all just a bit over your head, then you can just turn it into a Cabal script:

#!/usr/bin/env cabal
{- cabal:
   build-depends: base, text, aeson ^>= 1.5
-}
{-# LANGUAGE TypeApplications #-}
import Data.Aeson
import Data.Text.Lazy (pack)
import Data.Text.Lazy.Encoding (encodeUtf8)
main = getLine >>= print . decode @[Int] . encodeUtf8 . pack

Just put your dependencies at the top and Cabal will run it through a miniature package.

$ echo [1,2,3] | ./Script.hs 
Resolving dependencies...
Build profile: -w ghc-8.10.1 -O1
In order, the following will be built (use -v for more details):
 - fake-package-0 (exe:script) (first run)
Configuring executable 'script' for fake-package-0..
Preprocessing executable 'script' for fake-package-0..
Building executable 'script' for fake-package-0..
...
Just [1,2,3]

Haddocks

It's often handy to see the haddocks of the project you're working on. No need to wait before you upload it Hackage though. You can build it locally with cabal haddock

$ cabal haddock
Build profile: -w ghc-8.10.1 -O1
In order, the following will be built (use -v for more details):
 - foo-0.1.0.0 (lib) (configuration changed)
Configuring library for foo-0.1.0.0..
Preprocessing library for foo-0.1.0.0..
Running Haddock on library for foo-0.1.0.0..
Haddock coverage:
 100% (  2 /  2) in 'MyLib'
Documentation created:
/Users/luke/foo/dist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/index.html
# browse to your hearts content
$ open /Users/luke/foo/dist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/index.html

Open up the index.html in your browser of choice and you're good to go. It also shows the output of running haddock so you can see where you've missed any documentation.

If you're like me you might often take advantage of the quickjump functionality in Haddock. You can press the 's' key when browsing a Haddock package to bring up a search box, from which you can jump to definitions throughout. It's enabled on the haddock built on Hackage, and you can get it locally too with the --haddock-quickjump flag. Just be aware that you'll need to properly serve the pages with a HTTP server to get around the same-origin security restrictions.

$ cabal haddock --haddock-quickjump
# if you have python installed, this is a quick way to serve pages
$ python3 -mhttp.server -ddist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/

Quickjump in Haddock

Hoogle

Why not take the search experience to the next level by generating a hoogle database with --haddock-hoogle?

$ cabal haddock --haddock-hoogle
...
Documentation created:
/Users/luke/foo/dist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/foo.txt
$ hoogle generate --local=dist-newstyle/build/x86_64-osx/ghc-8.10.1/foo-0.1.0.0/doc/html/foo/
Starting generate
[1/1] foo... 0.00s

Reordering items... 0.00s
Writing tags... 0.00s
Writing names... 0.00s
Writing types... 0.00s
Took 0.09s

Now you can search for functions by type from the command line:

$ hoogle "IO ()"
MyLib someFunc :: IO ()
  1. If you're feeling daring you can try passing UHC, LHC or any other Haskell compiler.
  2. Don't roll your own sdists by hand, you're bound to forget one of the other files listed inside the your .cabal file like LICENSE or CHANGELOG.md. I'm just showing these for fun!