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
variables
—DEVELOPER_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.