Cross-compiling for iOS - Part 1: Build a SwiftUI App from Linux
Disclaimer:
The information provided in this guide is for educational and informational purposes only. It is not intended to be a substitute for professional advice. Always seek the advice of a qualified provider with any questions you may have regarding this process. Importantly, before proceeding, ensure that you read and understand the Xcode and Apple SDKs Agreement. Misuse of the tools and techniques described here could violate the agreement and may have legal consequences.
Prerequisites
Cross-compiling isn’t a new concept. However, it hasn’t gained much traction in the realm of Apple platforms due to the absence of official toolchains—for obvious reasons. But that hasn’t deterred enthusiasts from exploring this route. There are open-source toolchains available. Both jailbreak tweak developers and reputedly even large corporations use them.
Building an iOS app requires a compiler (like Clang or Swift), cctools (libtool, ld64, etc.), and an SDK. Clang and Swift have already established themselves as efficient cross-compilers. The ‘cctools’ even have a port for Linux and BSD that is being actively maintained. The iOS SDK, which comes with Xcode, can be downloaded from the Apple Developer website.
Besides source code, an iOS app may contain non-code resources such as plists,
asset catalogs, and Interface Builder/Storyboard files. While Xcode provides
tools to compile these resources, not all of them have been ported to Linux.
Facebook’s xcbuild project (now
archived) offers excellent solutions for handling plists and asset catalogs. If
your app doesn’t contain any .xibs
or .storyboard
files (for example, a
typical SwiftUI app), you can cross-compile it entirely from Linux.
Build System
While macOS users have Xcode, this isn’t an option on Linux. One could script the compilation and assembly processes, but that can become tedious and doesn’t scale well. For this exercise, we’ll be utilizing the Bazel build system. Bazel already has the build rules for creating iOS apps; all we need to do is supply it with the right toolchain.
When building on a macOS host, Bazel automatically detects the locally
installed Xcode and configures a toolchain based on the available Xcode
command-line tools. Those who prefer not to use the default detected toolchain
can specify their own by passing --apple_crosstool_top=//another/toolchain
to
their bazel build
.
Creating a Bazel Toolchain
Our goal is to largely leverage the auto-configured toolchain, with a few modifications. First, we need to extract the SDKs from Xcode, package them, and host them somewhere accessible. Next, we should pre-build all the ported tools and host them too, so Bazel can download them when necessary. The release binaries of Clang and Swift can be obtained from their official release websites. In this experiment, since the release tarballs are quite large and we only need the compilers, we strip out the components we don’t need and host them again. This significantly speeds up the toolchain configuration process.
On a macOS host, Bazel injects additional environment
variables
like DEVELOPER_DIR
and SDKROOT
into the Apple rules actions because they
have to be absolute paths and can differ between machines. The tool that
detects locally installed
Xcodes—xcode_locator
—relies
on an Apple
API,
which doesn’t compile for Linux. We’ll have to find equivalent values in our
clang
and
swift
wrappers, among other things.
rules_swift
offers two Swift toolchains—one for macOS and another for
Linux—and automatically configures the appropriate one based on the host
machine. To use our custom 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
resolves 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 built a SwiftUI app, its unit, and UI tests on Ubuntu. We then transferred the test artifacts to a macOS worker and ran them there (to the best of my knowledge, 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.
Currently, the toolchain only supports x86_64 Linux hosts, but it can be
extended to accommodate more systems. We can even use it on macOS. Another
advantage of using a custom toolchain is that swapping in a different version
of Clang/Swift becomes incredibly simple—just change the download URLs in the
toolchain’s clang_urls
and swift_urls
attributes.