Flight School

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
    │   ├── FlightPlan.swift
    │   └── FlightRules.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 AuxiliarySources module from the Swift files in the Sources/ directory.

$ xcrun swiftc -emit-library \
               -emit-module -module-name AuxiliarySources \

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 PlaygroundSupport 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 "PlaygroundSupport";                          \
  then                                                          \
    ...                                                         \

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 AuxiliarySources")                     \
    Contents.swift >  main.swift &&                       \
xcrun -sdk "${SDK}"                                       \
    swiftc -target "${TARGET}" -emit-executable           \
        -I "." -L "." -lAuxiliarySources                  \
        -module-link-name AuxiliarySources                \
    -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/AnyDecodable.playground
./Chapter\ 3/Coordinates.playground
./Chapter\ 3/EconomySeat.playground
./Chapter\ 3/EitherBirdOrPlane.playground
./Chapter\ 3/FuelPrice.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/MessagePackEncoder.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:


language: swift
osx_image: xcode9.3
    - SDK=iphoneos
    - TARGET=armv7-apple-ios10
    - PLAYGROUND_DIR="Chapter 1/Plane.playground"
    - PLAYGROUND_DIR="Chapter 2/Flight Plan.playground"
    - PLAYGROUND_DIR="Chapter 3/AnyDecodable.playground"
    - PLAYGROUND_DIR="Chapter 3/Coordinates.playground"
    - PLAYGROUND_DIR="Chapter 3/EconomySeat.playground"
    - PLAYGROUND_DIR="Chapter 3/EitherBirdOrPlane.playground"
    - PLAYGROUND_DIR="Chapter 3/FuelPrice.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/MessagePackEncoder.playground"
script: xcrun swift --version &&
  cd "${PLAYGROUND_DIR}" &&
  xcrun -sdk "${SDK}"
  swiftc -target "${TARGET}"
  -emit-library -emit-module -module-name AuxiliarySources
  Sources/*.swift &&
  if ! xcrun swiftc -emit-imported-modules Contents.swift |
  grep -q "PlaygroundSupport";
  cat <(echo "import AuxiliarySources") Contents.swift > main.swift &&
  xcrun -sdk "${SDK}"
  swiftc -target "${TARGET}"
  -I "." -L "." -lAuxiliarySources -module-link-name AuxiliarySources
  -o Playground main.swift;

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.