Update: I wrote an update for this article in February 2020: Testing Swift packages on Linux using Docker.
Speaking of testing on Linux, how do you test your Swift package on Linux when your development machine is a Mac? Here’s a quick way to set up a Linux testing environment using Docker containers. I’m assuming for the following that you have an existing project that works with the Swift Package Manager, i.e. you can run
swift build and
swift test from the project directory on your Mac.
Follow these steps:
Install Docker for Mac.
Create a file named
Dockerfilein the root directory of your project with these contents:
FROM swift:3.1 WORKDIR /package COPY . ./ RUN swift package fetch --enable-prefetching RUN swift package clean # swift build --clean for Swift 3.0 CMD swift test --parallel
Dockerfileis a recipe for building an image. The image then forms the base from which we later create the container that will run the tests.
FROM swift:3.1line sets the base image from which we derive our own image. We use the “official” Swift image1 in Docker’s public image repository. It’s a standard Ubuntu that has the Swift toolchain installed. You can use different tags (the part behind the colon) to select a specific Swift version, or
swift:latestfor the latest version.
The subsequent lines create a working directory in the image and copy the contents of the project directory from the host (your machine) to the image. If you place your
Dockerfilein a different directory than your project root, you’ll have to modify the paths in the
COPY . ./line.
We then run
swift package fetchand
swift package cleanto fetch our package’s dependencies and create a clean slate for the build. Fetching the dependencies again is not strictly necessary as they also get copied over from the host, but it’s good practice.
The last line,
CMD swift test, specifies the command we want to run when we run the container. In our case, we build the package and run the tests.
swift package cleanrequires Swift 3.1 or later. Use
swift build --cleanfor Swift 3.0 compatibility. The
--parallelflags are also new in Swift 3.1. You can just leave them out if you want to support Swift 3.0.)
Create another file named
.dockerignorein your project directory with these contents:
.git/ .build/ !.build/checkouts/ !.build/repositories/ !.build/workspace-state.json
.gitignore. It allows you to exclude certain paths on your host from being copied to the image. We specify the Git directory and the package manager’s build artifacts path because the build products produced on macOS aren’t compatible with Linux. This isn’t strictly necessary because the
RUN swift package cleancommand in the
Dockerfilewould delete these anyway.
The lines starting with
!exempt paths from the exclusion where the package manager stores the fetched dependencies. This makes building the image faster in the common case because libraries that the host has already fetched don’t have to be downloaded again.
Build the image by running the following command. Give your image a name of your choice. Mine is called
$ docker build --tag bananakit . ... Successfully built 2de5f0463f8e
This will download the base image from Docker Hub (if necessary) and execute the steps in the
Dockerfileone by one. After the build finished, you should be able to list your new image with
Now you can create and run a container based on the image:
$ docker run --rm bananakit Compile Swift Module 'BananaKit' (1 sources) Compile Swift Module 'BananaKitTests' (1 sources) Linking ./.build/debug/BananaKitPackageTests.xctest Test Suite 'All tests' started at 11:23:44.463 ... Test Suite 'All tests' passed at 11:23:44.464 Executed 3 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds
--rmflag tells Docker to automatically delete the container after running it once, which is usually what you want if you just want to check if your tests pass.
Now, every time you change your code and want to run the tests on Linux again, repeat the last two steps: rebuild the image and run the container. Building the image sounds like a time-consuming process but it’s usually very fast because Docker can cache a lot of stuff.
Alternative 1: swiftenv
What if the “official” Swift image doesn’t work for you, e.g. because it doesn’t support the Swift version you need? Take a look a Kyle Fuller’s excellent swiftenv, a command line tool for installing any Swift version or snapshot you want.
Conveniently, Kyle also provides a Docker image for swiftenv that you can use as a base image. For example, if you want to test your code with a specific Swift snapshot, you can start your
Dockerfile like this:
FROM kylef/swiftenv RUN swiftenv install DEVELOPMENT-SNAPSHOT-2017-03-30-a RUN swift --version WORKDIR /package ...
Alternative 2: Sharing the current directory between host and container
Dockerfile shown above copies the contents of the current directory to the Docker image during image creation. That’s why you have to rerun
docker build every time you modify any file in your package.
An alternative is to have the host share the directory with the container while the container runs. Both host and container have read-write access to the shared directory and any changes either side makes are immediately visible to both.
You don’t need to build an image at all in this case — the entire customization can be specified in the
docker run command. So feel free to delete
.dockerignore and use this command to run the container:
$ docker run --rm \ --volume "$(pwd):/package" \ --workdir "/package" \ swift:3.1 \ /bin/bash -c \ "swift package fetch && swift test --build-path ./.build/linux"
This tells Docker to:
- run a container based on the
- delete the container when it exits (
- map the current directory on the host to
/packagein the container (
- change into the
/packagedirectory in the container (
- and execute a Bash shell inside the container, telling the shell to run the command
swift package fetch && swift test ...and exit.
--build-path ./.build/linux parameter we’re passing to
swift test: now that host and container share the same directory, we have to tell SwiftPM to use a custom location for the Linux build products to avoid conflicts with the binaries built for macOS.
docker run invocation runs the specified command and exits. If you want to interact with the container, you can use the same technique to open a terminal session on the container:
$ docker run --rm \ --interactive \ --tty \ --volume "$(pwd):/package" \ --workdir "/package" \ swift:3.1
“Official” as in “endorsed by Docker on Docker Hub”. The image is not being maintained by Apple or the Swift team. ↩︎