Today I am going to share a simple Travis CI configuration for Haskell projects with you. The .travis.yml
file presented in this blog post allows you to painlessly test your repository on the continuous integration service under multiple GHC versions and with both build tools — cabal-install and stack. Note that the suggested settings do not include complex configuration steps that could possibly be required for some projects. However, they work amazingly well for most Haskell libraries and applications where a basic setup is enough!
The Config
I’ll cut to the chase, here is the .travis.yml
file that you can copy-paste to each Haskell project and enjoy (hopefully) the green CI status:
sudo: true
language: haskell
git:
depth: 5
cabal: "2.4"
cache:
directories:
- "$HOME/.cabal/store"
- "$HOME/.stack"
- "$TRAVIS_BUILD_DIR/.stack-work"
matrix:
include:
# Cabal
- ghc: 8.2.2
- ghc: 8.4.4
- ghc: 8.6.5
# Stack
- ghc: 8.6.5
env: STACK_YAML="$TRAVIS_BUILD_DIR/stack.yaml"
install:
- |
if [ -z "$STACK_YAML" ]; then
ghc --version
cabal --version
cabal new-update
cabal new-build --enable-tests --enable-benchmarks
else
# install stack
curl -sSL https://get.haskellstack.org/ | sh
# build project with stack
stack --version
stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks
fi
script:
- |
if [ -z "$STACK_YAML" ]; then
cabal new-test --enable-tests
else
stack test --system-ghc
fi
notifications:
email: false
Customization: HLint
I want to point out some nice things about this config — it doesn’t
mention the project name anywhere and it doesn’t rely on any
additional shell scripts. Which means that you have the ability to
paste it in any Haskell project and it should just work out of the
box. Though, it’s not the only reason why I recommend it. This config
requires almost no maintenance, but at the same time, it is easily
extensible. For example, if you want to check your code with
HLint on every CI run, you
just need to add the following line at the end of the script
section:
- curl -sSL https://raw.github.com/ndmitchell/hlint/master/misc/run.sh | sh -s .
This command downloads the latest HLint version and runs the hlint
executable on your project.
How to set up Travis CI
A quick recap on how to make Travis CI work for your project:
- Enable Travis CI for your GitHub account if you haven’t done it before.
- Enable Travis CI for your specific repository.
- Copy-paste the given
.travis.yml
to your project.
That’s all! Feel free to contact me if you have any problem setting up Travis CI for your Haskell projects with this config. I have done tons of “Fix CI”
commits and learned about a lot of weird errors before figuring out the proper settings.
NOTE: If you build your project with
stack
, it is a good idea to put a fully configuredstack.yaml
file to the repository. There you can specify all the settings required forstack
to build your project, but for basic cases, it should be enough to have only the resolver in there:: lts-13.26 resolver
Explanation of the configuration commands
Here is a short description of the commands used in the script:
sudo: true
allows us to use more powerful CI environment and increases the speed of building.- When you use
language: haskell
, the GHC and cabal versions for Ubuntu are taken from Herbert V. Riedel’s ppa. - Setting
git depth
to 5 decreases the time for cloning the GitHub repo, by pruning the amount of history (commits) of the repo that is fetched. STACK_YAML
environment variable is used to distinguish between Cabal and Stack builds. If you want to test multiple GHC versions withstack
you only need to createstack-VERSION.yaml
file specific to GHC version and add the corresponding number of items to thematrix
.new-
commands are used for Cabal since they provide a modern way to build Haskell projects withcabal-install
in nix-style local builds.--system-ghc
flag is important forstack
builds. Since you get the GHC version from the CI,stack
doesn’t need to download them separately. That’s why, do remember to add this flag to everystack
command you’re using.- Email notifications are disabled because they quickly become annoying. But the
notifications
section allows you to add a Slack integration, which can be handy.
Reasoning behind such a CI config
As you can see from the config, it contains several opinionated decisions, which can appear redundant or suboptimal. Here I am going to reveal the reasoning behind them.
Why both cabal and stack?
You may ask, why would I need to test my project with two build tools? The answer is to be friendlier to the rest of the Haskell community. Yes, there are several build tools for Haskell with cabal
and stack
being the most popular ones. But, unfortunately, if the project builds with one of the tools, it doesn’t guarantee that it would also build with the other (though it works in most cases).
- If your project builds with
stack
it may still fail to build withcabal
. For example, if you don’t specify library bounds in thebuild-depends
section, cabal solver might not find a valid plan to build your package with. - If your project builds with
cabal
it may still fail to build withstack
. For example, some of your dependencies might not be in the Stackage snapshot, so you would need to add them into theextra-deps
section instack.yaml
.
If you test your Haskell package on CI with both Cabal and Stack you provide a better user experience for users of both the build tools. As a maintainer of multiple open-source libraries and applications, I try to make the life of the contributors and users of my packages easier by providing support for both cabal
and stack
. The fewer steps they need to do in order to make my project work in their current environment, the more chances they don’t give up halfway through fighting with the tooling. Also, if your Haskell package builds with cabal
and stack
, you can add it to both Hackage and Stackage.
Why latest 3 major GHC versions?
It is a huge temptation to use only the latest GHC version for your package with all brand new cool features. But even if migration to a newer GHC version is usually a painless process, it takes time for the whole Haskell ecosystem to catch up. And users of your Haskell package may still use a two year old GHC version. This usually happens in big applications where you cannot switch to a newer GHC version unless each of your dependencies supports it.
At the same time, it is highly desired to be able to bump up the version bounds only for some specific libraries (because of applied bug-fixes and performance improvements in the newer versions). But if newer versions require you to also switch to a newer compiler, you might not be able to do that because the migration of your application to a newer GHC version can be blocked by other libraries.
You can see why it is nice when libraries support older compiler versions as well. However, maintaining support for very old GHC versions might be a thankless and time-consuming thing to do. Also, the more GHC versions you test against on the CI, the more time you need to wait until CI passes. That’s why the latest 3 seem reasonable enough.
NOTE: you may notice from the configuration that the latest 3 GHC versions are tested only for
cabal-install
whilestack
builds only most recent version. This is done merely for convenience since in Stackage snapshots GHC versions are tightly coupled with dependency versions.
Conclusion
You can see now that it’s actually not that hard to run Travis CI for Haskell repositories! And the given configuration is easily extensible with more commands, like adding HLint, checking on a newer compiler version, building Haddock documentation, running benchmarks, etc. Dealing with CI errors requires time and patience. But at least with this config you don’t need to have multiple build tools and multiple GHC versions installed on your machine, you can delegate all this dirty work to the CI.
Appendix
In some situations it’s not possible to run CI for your Haskell project with both build tools easily. So below I’m providing separate configurations for each build tool.
Cabal-only configuration
sudo: true
language: haskell
git:
depth: 5
cabal: "2.4"
cache:
directories:
- "$HOME/.cabal/store"
matrix:
include:
- ghc: 8.2.2
- ghc: 8.4.4
- ghc: 8.6.5
install:
- ghc --version
- cabal --version
- cabal new-update
- cabal new-build --enable-tests --enable-benchmarks
script:
- cabal new-test --enable-tests
notifications:
email: false
Stack-only configuration
sudo: true
language: haskell
git:
depth: 5
cache:
directories:
- "$HOME/.stack"
- "$TRAVIS_BUILD_DIR/.stack-work"
matrix:
Include:
- ghc: 8.2.2
env: STACK_YAML="$TRAVIS_BUILD_DIR/stack-8.2.2.yaml"
- ghc: 8.4.4
env: STACK_YAML="$TRAVIS_BUILD_DIR/stack-8.4.4.yaml"
- ghc: 8.6.5
env: STACK_YAML="$TRAVIS_BUILD_DIR/stack.yaml"
install:
- curl -sSL https://get.haskellstack.org/ | sh
- stack --version
- stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks
script:
- stack test --system-ghc
notifications:
email: false