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.