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

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

May 9, 2021

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 reason). 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), the cctools (libtool, ld64, …) and the SDK. Clang and Swift are already perfect cross-compilers. cctools has a port for Linux and BSD that is being actively maintained. The iOS SDK is provided within Xcode that can be downloaded from Apple.

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. 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, hence the title, sorry :P) 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 can write a bunch of scripts that compile and put everything together, but doing that is a pain and doesn’t seem to scale well. In this experiment, we’re going to use the Bazel build system. Bazel already has the build rules for building iOS apps, we just 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 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. For Clang and Swift compilers, we can download them from the official releases, but because 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 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 seen 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 (is there a Linux port for 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 supports 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 — just change 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.