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
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
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
.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
--apple_crosstool_top=//another/toolchain to their
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
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 —
— uses an Apple
so it doesn’t build for Linux. We need to resolve the equivalent of those
values in our
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
need to override this behavior by configuring a
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