Like many other open source projects, the i3 window
manager is using Travis CI for continuous
integration (CI). In our specific case, we not only verify that every pull
request compiles and the test suite still passes, but we also ensure the code
is auto-formatted using clang-format, does not
contain detectable spelling errors and does not accidentally use C functions
sprintf() without error checking.
By offering their CI service for free, Travis provides a great service to the open source community, and I’m very thankful for that. Automatically running the test suite for contributions and displaying the results alongside the pull request is a feature that I’ve long wanted, but would have never gotten around to implementing in the home-grown code review system we used before moving to GitHub.
Motivation (more recent build environment)
Nothing is perfect, though, and some aspects of Travis can make it hard to work with. In particular, the build environment they provide is rather old: at the time of writing, the latest you can get is Ubuntu Trusty, which was released almost two years ago. I realize that Ubuntu Trusty is the current Ubuntu Long-Term Support release, but we want to move a bit quicker than being able to depend on new packages roughly once every two years.
For quite a while, we had to make do with that old environment. As a
mitigation, in our
.travis.yml file, we added the whitelisted
ubuntu-toolchain-r-test source for newer versions of clang (notably also
clang-format) and GCC. For integrating lintian’s spell checking into our CI
infrastructure, we needed a newer lintian version, as the version in Ubuntu
Trusty doesn’t have an interface for external scripts to use. Trying to make
.travis.yml file install a newer version of lintian (and only
lintian!) was really challenging. To get a rough idea, take a look at our
.travis.yml before we upgraded to Ubuntu Trusty and were stuck
with Ubuntu Precise. Cherry-picking a newer lintian version into Trusty would
have been even more complicated.
With Travis starting to offer Docker in their build environment, and by looking at Docker’s contribution process, which also makes heavy use of containers, we were able to put together a better solution:
The basic idea is to build a Docker container based on Debian testing and then
run all build/test commands inside that container. Our Dockerfile
installs compilers, formatters and other development tools first, then installs
all build dependencies for i3 based on the
debian/control file, so
that we don’t need to duplicate build dependencies for Travis and for Debian.
This solves the immediate issue nicely, but comes at a significant cost: building a Docker container adds quite a bit of wall clock time to a Travis run, and we want to give our contributors quick feedback. The solution to long build times is caching: we can simply upload the Docker container to the Docker Hub and make subsequent builds use the cached version.
We decided to cache the container for a month, or until inputs to the build
environment (currently the
debian/control) change. Technically, this is implemented by a
little shell script called ha.sh
(get it? hash!) which prints the SHA-256 hash of the input files. This hash,
appended to the current month, is what we use as tag for the Docker container,
See our .travis.yml for how to plug it all together.
We’ve been successfully using this setup for a bit over a month now. The advantages over pure Travis are:
- Our build environment is more recent, so we do not depend on Travis when we want to adopt tools that are only present in more recent versions of Linux.
- CI runs are faster: what used to take about 5 minutes now takes only 1-2 minutes.
- As a nice side effect, contributors can now easily run the tests in the same environment that we use on Travis.
There is some potential for even quicker CI runs: currently, all the different steps are run in sequence, but some of them could run in parallel. Unfortunately, Travis currently doesn’t provide a nice way to specify the dependency graph or to expose the different parts of a CI run in the pull request itself.