Recently I’ve been working with some cross-platform Swift, that needs to build ok on both macOS and Linux.
Naturally I want to unit test it, and I’d like to have the tests hooked up to continuous integration.
Previously I was a big fan of Jenkins, and whilst working on Sketch I helped to build up a fairly complex testing setup with a bunch of Mac Minis all hooked up as remotes to a Jenkins server.
At the time, it gave us the flexibility we needed, and it still has a lot of things going for it as an approach, especially if you want it to perform a lot of complex operations, including code signing, and preparing and releasing final builds. It does mean housing (or co-locating), and maintaining a bunch of physical machines though, which is a pain in the arse.
These days, my needs are a little simpler, and I’m an even bigger fan of Travis.
The single biggest upside of Travis, for me, is that it’s all in the cloud, based off virtual machines with known snapshots. No more tedious updating of Xcode on every test machine! No more inconsistent results due to minor discrepancies between test machines! No more noisy Mac Minis sitting in my office burning my electricity!
The single biggest downside is that it’s costly, starting at about $69/month for the most basic setup if you want to use it for private projects. The cost is understandable - it’s burning a lot of resources in the cloud, after all - but it could still be substantial barrier to an indy developer (it’s enough to buy a new test machine every year, for example, which you can also use for other things).
The good news, however, is that it’s completely free for open source projects. This is fantastic, and the Travis guys are to be applauded for doing it this way. It is also a powerful incentive to open-source ) - which in itself is no bad thing at all.
As luck would have it, I’m currently trying hard to open-source as much of my work as I can, so Travis is just the ticket.
I’m not going to give a complete Travis tutorial here (their own docs are excellent), but essentially the way it works is that you specify an os image (eg “macOS 10.13 with Xcode 9.3”), then some commands to run to perform your tests.
You can also specify a matrix of alternative images and settings (for example, specifying both a macOS and a Linux image), and Travis will run your tests across each permutation.
If there’s a preconfigured image that has the right combination of stuff, this is an incredibly simple process.
However, when testing cross-platform Swift, there are a few wrinkles:
As a concrete example of this, I wanted to test my Builder project on both platforms.
It’s a plain-vanilla Swift module, with no dependency on Xcode, but it does rely on Swift 4.2.
So how would we go about doing this in Travis? It’s actually pretty easy…
On the Mac, the newest image contains Xcode 9.3, but Swift 4.2 is in 9.4.
You can, however, download it and install it into Xcode as a toolchain.
The .travis.yml
file looks like this:
os: osx
osx_image: xcode9.3
language: swift
sudo: required
install:
- wget https://swift.org/builds/swift-4.2-branch/xcode/swift-4.2-DEVELOPMENT-SNAPSHOT-2018-05-30-a/swift-4.2-DEVELOPMENT-SNAPSHOT-2018-05-30-a-osx.pkg
- sudo installer -pkg swift-4.2-DEVELOPMENT-SNAPSHOT-2018-05-30-a-osx.pkg -target /
- export PATH="/Library/Developer/Toolchains/swift-4.2-DEVELOPMENT-SNAPSHOT-2018-05-30-a.xctoolchain/usr/bin:$PATH"
script:
- swift --version
- swift package update
- swift test
What does this do?
Basically the stuff in the install:
section downloads the relevant Swift snapshot, runs the installer to install it, then sets up PATH
so that the version of Swift contained in it is the one that will get picked up when we run swift
on the command line.
Simples!
On Linux, the picture is similar, except that there’s no Xcode to worry about, and hence no toolchain installer. You just need to download and unpack the snapshot.
Here’s an example of setup where I’m testing on both platforms, using the matrix feature of Travis:
env:
global:
- SWIFT_BRANCH=swift-4.2-branch
- SWIFT_VERSION=swift-4.2-DEVELOPMENT-SNAPSHOT-2018-05-30-a
matrix:
include:
- os: linux
language: generic
dist: trusty
sudo: required
install:
- sudo apt-get install clang libicu-dev
- mkdir swift
- curl https://swift.org/builds/$SWIFT_BRANCH/ubuntu1404/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu14.04.tar.gz -s | tar xz -C swift &> /dev/null
- export PATH="$(pwd)/swift/$SWIFT_VERSION-ubuntu14.04/usr/bin:$PATH"
script:
- swift package update
- swift test
- os: osx
osx_image: xcode9.3
language: swift
sudo: required
install:
- wget https://swift.org/builds/$SWIFT_BRANCH/xcode/$SWIFT_VERSION/$SWIFT_VERSION-osx.pkg
- sudo installer -pkg $SWIFT_VERSION-osx.pkg -target /
- export PATH="/Library/Developer/Toolchains/$SWIFT_VERSION.xctoolchain/usr/bin:$PATH"
script:
- swift package update
- swift test
Notice that I’ve extracted out the Swift branch and snapshot version into variables. This makes it a doddle to update them when new versions are released on the Swift site.
Every time I push a commit to Github, Travis will run the tests on both macOS and Linux, and shout at me if I’ve done something stupid.
If you’re relying on a particular version of Swift, and there’s a snapshot of it, this is a really easy way to test with it.
If the snapshot that you need doesn’t exist on the Swift site, there’s also nothing to stop you from building it yourself locally, uploading it somewhere, and having your Travis script download it.