Easily test your Swift Script on Linux

Let's learn some Docker basics to run Swift in a Linux environment

Swift can be a lot of fun for building quick, little scripts. It’s powerful, readable, and many frameworks can be used on Linux systems as well.

However, some of these are macOS-only. If you’ve built a script on macOS and want to check if it runs on Linux (and you don’t feel like setting up a VM just for a quick test), read on!

The setup

So here’s a little example script we’ll work with:

import Foundation
import Alamofire

print("Checking..")

AF.request("https://wttr.in?format=3").responseString { response in
        switch response.result {
        case .success(let weather):
                print("Current weather for \(weather)")
                exit(0)
        default:
                print("Couldn't get the weather at the moment, sorry!")
                exit(0)
        }
}

RunLoop.main.run()

If it’s not clear, we’re simply requesting and displaying current weather information from this neat little API I set it up with $ swift package init —type executable, added Alamofire as dependency, and ran $ swift package update.

We can now build the script using $ swift build from the project folder and execute it using $ swift run.

The plan

So what we’re going to use now is Docker. If you’ve never used it before, don’t worry! Yes, Docker can become quite complex, but we won’t have to dig deep here. Instead of plain executables, Docker runs your application in a “container”. These containers have two aspects that will be useful for us:

  • Each container runs in an isolated environment with its own file system and dependencies.
  • Containers are built from “images”*, which can be based on other images, so we can take an existing image and build on top of it. Examples can be plain Linux images (e.g. Ubuntu), or entire services (e.g. WordPress).

*In reality, there are a few more nuances to containers and images. My definition likely isn’t 100% accurate, but it will help us get through the tutorial. More detailed explanations can be found here.

To our convenience, there is an official Swift image, based on Ubuntu, which we can use to run our code with.

The magic

To declare how our image should be constructed, we can set up a Dockerfile in our project root with the following content:

# the base image to use
FROM swift:5.0

# copy all files from folder root into image
COPY . .

# this will be run when building the image
RUN swift build

# this will be called when executing the image as container
CMD swift run

If you haven’t done so already, install Docker (for example using $ brew cask install docker, then launch it.

We can now try to build our image and see if Swift complains: $ docker build .

(This will take a bit, Docker will download all the necessary components and then start the build process. Luckily all of this is cached, so subsequent builds should be much faster.)

...
Step 3/4 : RUN swift build
 ---> Running in f653cfc8fe3b
[1/3] Compiling Swift Module 'Alamofire' (31 sources)
/.build/checkouts/Alamofire/Source/NetworkReachabilityManager.swift:28:8: error: no such module 'SystemConfiguration'
import SystemConfiguration

Oops! That didn’t work. It seems like our Alamofire dependency couldn’t be built. If we had RTFM before coding away, we would have realized sooner that Alamofire doesn’t run on Linux.

So let’s go ahead and remove Alamofire, replacing our network calls with URLSession methods from Swift’s built-in Foundation framework.

import Foundation

print("Checking..")

let url = URL(string: "https://wttr.in?format=3")!
URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
	guard let data = data, error == nil else {
		print("Couldn't get the weather at the moment, sorry!")
		exit(0)
	}
	let weather = String(decoding: data, as: UTF8.self)
	print("Current weather for \(weather)")
	exit(0)
}).resume()

RunLoop.main.run()

If we now build our Docker image..

...
[2/2] Linking /.build/x86_64-unknown-linux/debug/scripto
Removing intermediate container c8a7aecc10a2
 ---> 5f9a08e8f053
Step 4/4 : CMD swift run
 ---> Running in e7291a3009fd
Removing intermediate container e7291a3009fd
 ---> c52b7c898750
Successfully built c52b7c898750

Sweet! Our image was built and got the ID c52b7c898750. You can verify this by listing all the images on your machine using $ docker image ls. You should see your image at the top, as it’s the most recently built image. To better identify it you can build it using $docker build --tag some-name ., to give it a name and use that instead of IDs.

We can now run the image using $ docker run c52b7c898750 and should see our weather info display as expected.

We have now adjusted our code to be runnable on Linux and verified it by using a Docker container based on Swift’s ubuntu image.

I’ve put the code on Github and if you notice some gaps or inaccuracies, feel free to let me know. Thanks for reading and have a great day! :)

Some debugging tips

I’ve sometimes experienced little hick-ups while using this technique, like not seeing my container’s output or the container hanging around without shutting down properly.

To debug this, it helps to list the currently running containers using $ docker ps. If you see an unexpected container, you can terminate it with $ docker kill <id>.

As mentioned above, listing your built images is done via $ docker image ls and you can remove one of those using $ docker image rm -f <id | name>. This can be useful to force a clean rebuild or to reclaim the used storage space after you’re done.

On some systems, Docker needs privileged access, so prefix your Docker-Calls with sudo if you run into issues. I didn’t have to do so, probably because I gave the macOS Client access during setup.

May 2019

from Hamburg with love