Running Xcode Playgrounds on Travis CI
Perform automated compilation checks on sample code
Xcode Playgrounds are a great way to share sample code. They allow you to communicate ideas effectively without getting bogged down in implementation details. The question is: how do you ensure that things continue to work with each new version of Swift and platform SDKs?
This is something we’ve been thinking about since the release of our Guide to Swift Codable. We wanted to release the sample code Playgrounds as open source on GitHub, but not without some kind of testing strategy in place first (with over a dozen Playgrounds in total, doing this manually was out of the question).
As a baseline, our goal was to create a continuous integration that tested whether our code compiled and ran without any issues. (From there, we can progressively add more comprehensive tests for expected output and behavior.)
Each chapter directory contains one or more .playground
bundles,
each of which contains a Contents.swift
file
(this is what you first see when you open a Playground with Xcode)
as well as any auxiliary sources.
$ tree "Chapter 2"
Chapter\ 2
└── Flight\ Plan.playground
├── Contents.swift
├── Sources
│ ├── Aircraft.swift
│ ├── Flight Plan.swift
│ └── Flight Rules.swift
└── contents.xcplayground
#Compiling a Playground from Scratch
Without an Xcode project or Swift package manifest,
we can’t directly hook into familiar solutions
for testing apps or libraries.
However, we can reasonably approximate how Xcode builds playgrounds
by invoking swiftc
directly
(well, almost directly —
we’ll call it via xcrun
).
First,
we cd
into the .playground
bundle.
$ cd "Chapter 1/Plane.playground"
Next, we use swiftc
to build an Auxiliary
module
from the Swift files in the Sources/
directory.
$ xcrun swiftc -emit-library \
-emit-module -module-name Auxiliary Sources \
Sources/*.swift
Some playgrounds depend on Playground-specific functionality in order to run.
We can use the swiftc
command’s -emit-imported-modules
option
to detect whether Playground
is imported,
and only attempt to build and run the Playground if it isn’t.
$ if ! xcrun swiftc -emit-imported-modules Contents.swift | \
grep -q "Playground Support"; \
then \
... \
fi;
When running a Playground in Xcode,
the shared sources module is imported automatically.
We can add a missing import
statement
by concatenating it with Contents.swift
and then writing the output to a new main.swift
file.
cat <(echo "import Auxiliary Sources") \
Contents.swift > main.swift && \
xcrun -sdk "${SDK}" \
swiftc -target "${TARGET}" -emit-executable \
-I "." -L "." -l Auxiliary Sources \
-module-link-name Auxiliary Sources \
-o Playground main.swift \
#Building Across All Playgrounds
We can get a list of all the .playground
bundles
by running the following command from the root directory:
$ find . -name Chapter -prune -o -name '*.playground' -print | sort
./Chapter\ 1/Plane.playground
./Chapter\ 2/Flight Plan.playground
./Chapter\ 3/Any Decodable.playground
./Chapter\ 3/Coordinates.playground
./Chapter\ 3/Economy Seat.playground
./Chapter\ 3/Either Bird Or Plane.playground
./Chapter\ 3/Fuel Price.playground
./Chapter\ 3/Pixel.playground
./Chapter\ 3/Route.playground
./Chapter\ 4/Music Store.playground
./Chapter\ 5/In Flight Service.playground
./Chapter\ 6/Luggage Scanner.playground
./Chapter\ 7/Message Pack Encoder.playground
This list can be fed into the continuous integration system as environment variables, which are set on individual build jobs:
$ export PLAYGROUND_DIR="Chapter 1/Plane.playground"
$ cd "${PLAYGROUND_DIR}" && # compile playground
#Putting it All Together
Now that we can compile Playground files locally
and know how to repeat the process across our sample code,
it’s time to write our .travis.yml
file
and kick off a test build:
#.travis.yml
language: swift
osx_image: xcode9.3
env:
global:
- SDK=iphoneos
- TARGET=armv7-apple-ios10
matrix:
- PLAYGROUND_DIR="Chapter 1/Plane.playground"
- PLAYGROUND_DIR="Chapter 2/Flight Plan.playground"
- PLAYGROUND_DIR="Chapter 3/Any Decodable.playground"
- PLAYGROUND_DIR="Chapter 3/Coordinates.playground"
- PLAYGROUND_DIR="Chapter 3/Economy Seat.playground"
- PLAYGROUND_DIR="Chapter 3/Either Bird Or Plane.playground"
- PLAYGROUND_DIR="Chapter 3/Fuel Price.playground"
- PLAYGROUND_DIR="Chapter 3/Pixel.playground"
- PLAYGROUND_DIR="Chapter 3/Route.playground"
- PLAYGROUND_DIR="Chapter 4/Music Store.playground"
- PLAYGROUND_DIR="Chapter 5/In Flight Service.playground"
- PLAYGROUND_DIR="Chapter 6/Luggage Scanner.playground"
- PLAYGROUND_DIR="Chapter 7/Message Pack Encoder.playground"
script: xcrun swift --version &&
cd "${PLAYGROUND_DIR}" &&
xcrun -sdk "${SDK}"
swiftc -target "${TARGET}"
-emit-library -emit-module -module-name Auxiliary Sources
Sources/*.swift &&
if ! xcrun swiftc -emit-imported-modules Contents.swift |
grep -q "Playground Support";
then
cat <(echo "import Auxiliary Sources") Contents.swift > main.swift &&
xcrun -sdk "${SDK}"
swiftc -target "${TARGET}"
-I "." -L "." -l Auxiliary Sources -module-link-name Auxiliary Sources
-o Playground main.swift;
fi
Global environment variables are used to declare constants for the SDK and target. Together, they allow us to quickly tell (and later change) what platform our builds are targeting.
Each Playground directory is specified as a separate environment variable, forming a Build Matrix with separate jobs for each one.
Newlines and indentation help break up the long script command
into more meaningful chunks.
Everything works out in the end because
YAML Plain Style
helpfully strips out the extra whitespace when being parsed.
With CI all set up and ready to go, we’re thrilled to share the sample code for our first book.
Feel free to Clone, Star, and Fork to your heart’s content! And if you have any ideas for how to improve our setup even further, please open an issue or reach out via Twitter.