Cross-compiling for iOS - Part 1: Build a SwiftUI App from Linux


Cross-compiling is not something new, it’s just not popular in the Apple platforms world because of the lack of the official toolchains (for obvious reasons). But that can’t prevent people from trying. There are open source toolchains out there. Jailbreak tweaks developers do it. Rumor has it that big corps do it.

To build an iOS app, we need a compiler (Clang and/or Swift), cctools (libtool, ld64, …) and an SDK. Clang and Swift are already perfect cross-compilers. cctools has a port for Linux and BSD that is actively maintained. The iOS SDK provided within Xcode can be downloaded from Apple Developer website.

Besides source code, an iOS app may have non-code resources, notably plists, asset catalogs, and Interface Builder / Storyboard files. Xcode provides tools for compiling those resources, but not all of them have been ported over to Linux. Facebook’s xcbuild project (now archived) provides some very good ones for handling plists and asset catalogs. If your app doesn’t have any .xibs or .storyboard files (for example, a typical SwiftUI app), then you can completely cross-compile it from Linux.

Build System

On macOS, we have Xcode. Xcode doesn’t run on Linux, so we can’t use it here. We could write a bunch of scripts to compile and put everything together, but doing that is a pain and doesn’t seem to scale well. In this experiment, we are going to use the Bazel build system. Bazel already has the build rules for building iOS apps, we only need to feed it with the right toolchain.

When building on a macOS host, Bazel automatically detects the local installed Xcode and configures a toolchain based on the available Xcode command-line tools. Those who don’t want to use the default detected toolchain can provide their own by passing --apple_crosstool_top=//another/toolchain to their bazel build.

Create a Bazel Toolchain

We’re going to re-use most of the auto-configured toolchain with some modifications. First we need to extract the SDKs from Xcode, package and host them somewhere. Next, we pre-build all the ported tools, and also host them somewhere so that Bazel can download them later. Release binaries of Clang and Swift can be downloaded from their official releases websites. In this experiment, since the release tarballs are very big while we only need the compilers, we strip out what we don’t need and re-host them. This will significantly speed up the toolchain configuration later.

When building on a macOS host, Bazel injects additional environment variablesDEVELOPER_DIR and SDKROOT — to the Apple rules actions, because they have to be absolute paths and thus can be different between machines. The tool that detects local installed Xcodes — xcode_locator — uses an Apple API so it doesn’t build for Linux. We will need to resolve the equivalent of those values in our clang and swift wrappers, among others.

rules_swift provides two Swift toolchains — one for macOS and one for Linux – and automatically configures the right one based on the host machine. To use our own Swift toolchain (a fork of rules_swift’s xcode_swift_toolchain), we need to override this behavior by configuring a bazel_build_rules_swift_local_config repository before rules_swift resolving its dependencies.

The complete toolchain implementation can be found at https://github.com/apple-cross-toolchain/rules_applecross. From the build log, you can see that we build a SwiftUI app, its unit and UI tests on Ubuntu, then copy the test artifacts to a macOS worker, and run them there (AFAIK there is no Linux port of the iOS simulator yet). There’s also an example of building the app using the Remote Build Execution service by BuildBuddy. More real world examples can be found at https://github.com/apple-cross-toolchain/examples.

The toolchain only has support for x86_64 Linux hosts now, but can be extended to support more systems. We can even use it on macOS too. Another advantage of using a custom toolchain is that it’s very easy to swap in a different version of Clang/Swift, by just changing the download URLs in the toolchain’s clang_urls and swift_urls attributes.

Finally

Please make sure you have read and understood the Xcode and Apple SDKs Agreement before doing this.