Davide De Rosa

Cross-platform Swift: Build system (pt. 1)

Cross-platform Swift: Build system (pt. 1)

part of a series: < prev | next >

Table of contents (ongoing)

  1. Introduction
  2. Combine
  3. Core libraries
  4. Platform specifics
  5. C interop (pt. 1)
  6. C interop (pt. 2)
  7. Build system (pt. 1)

There is a reason why it took me two months since the last post, and ironically it’s the very subject of this writing. I hate to say, but the build tools are the most painful and work-in-progress aspect of Swift, at least at the time of writing. Brace yourself.

SwiftPM is just not ready

This post risks having a negative vibe, so I want to discuss solutions before problems. The first advice I can give if you want to build consistently across platforms is to avoid SwiftPM, except for Swift Testing. It’s just not fit for the role yet. XCFrameworks only work on Apple, and binary artifact bundles are on their way, but haven’t been released yet. If you need something that works today, get familiar with swiftc and clang, and switch to CMake with ninja. With CMake in place, Swift gains back the freedom and power it deserves. You take full control of what’s going on, and this will help you understand and fix the build issues that you’re bound to encounter.

At this point, you may come to realize how SwiftPM could lead you to a complex source hierarchy. The highly granular dependency model encouraged by the manifest is a neat abstraction, and works very well for modular Swift-only systems. But when you have to account for C/C++ targets, modulemaps, DocC, and external non-Swift libraries, it makes you want to jump off the nearest cliff without even a goodbye letter. In that case, making your source hierarchy flat and monolithic will make your life much easier. Two targets: one for Swift files, and one for C/C++ with a single modulemap.

Unfortunately, there’s one thing you might want SwiftPM for: the Swift SDKs. For what it’s worth, I use a Swift SDK for my Android builds, and I have no idea if there’s a place for it in a CMake build, but I kind of doubt it. Nevertheless, given that Passepartout –and other consumers– still need Partout as a SwiftPM dependency, I’m okay with keeping both the Package.swift and CMake around for some time.

The Swift runtime

There are two common ways to compile Swift files:

  • swiftc
  • swift build (Swift Package Manager)

The majority of Swift programmers, hardly use or tweak swiftc directly, which is similar to how one would use the clang or gcc C/C++ compilers. Over the years, the Xcode integration and the fast turnaround of SwiftPM execution from the command line made swift build the natural way to compile and link Swift projects, both standalone and inside Xcode.

This is all great, until it’s not. The lack of insight into what SwiftPM exactly does may be very problematic when we face a compiler or linker issue. However, pure Swift projects hardly hit major build issues, so don’t be surprised if the average iOS programmer doesn’t know how a Swift executable is generated or what it needs to run on a machine, because Apple devices come with the Swift runtime preinstalled.

The language “runtime” is a set of libraries that an executable requires to run on a specific architecture. Let me explain why I find this a serious blocker for Swift adoption.

Binary distribution is impractical

Let’s pick Linux, for example, which on a typical modern desktop may come in arm64 or x86_64 flavors. Not only does this imply two runtimes to deal with for distributing our binaries, the Swift runtime is also split into dozens of files. If we close an eye on the footprint, this is still annoying, but less of a problem on platforms where the executable model is self-contained (iOS, macOS, Android, …). You could distribute a self-contained folder/installer on Linux, but it always feels off on a system where libraries are typically shared in /usr/lib for everybody.

Installing the Swift runtime with a package manager seems like a smoother solution, and saves us from the burden of manually bundling and maintaining the binaries for each architecture. To my knowledge, apt has a libswiftlang package. I don’t know how maintained it is, because the apt package is at 6.0.3 with the latest toolchain being at 6.1.2. Not a 100% healthy sign, but not worrying either.

Still, this introduces a dependency, and forces you to either distribute your executable through a package manager, or include manual steps in order to fetch the proper Swift runtime, for the proper platform, and for the proper architecture. In that regard, I don’t like that swift.org doesn’t offer direct links to download runtime-only installers, like Java used to do with the Java Runtime Environment (JRE).

A more convenient solution for Linux was introduced in Swift 5.3.1 with static linking in an attempt to match the self-contained approach of Golang, which is the state of the art for native multiplatform applications. Static linking to the stdlib is not as straightforward as running go build, but still.

The loneliness of the other platforms

All things considered, the friction remains very real. Now, if this already sounds complicated, what if I told you that Linux is the best supported platform after Apple’s?

The weight of Windows and Android is on the shoulders of a few kind individuals, whose progress can be followed on the official Swift forums. For example, hard work is being done to bring static linking on Windows as of Swift 6.2 (unreleased yet), and a few months ago, an Android workgroup was made official. Until then, Android development was mostly pushed by volunteers.

I’m all about supporting the maintainers of these exciting initiatives, and that’s why I took this winding path to experiment myself, but if you need something that works right away for building production software in Swift, you’d better assume that you’re on your own. There’s still work to be done to make the experience acceptable for the public domain, and Android builds in particular take a lot of manual steps, or way more than a lot.

At the end of the day, it depends on your goals. Personally, I was thrilled when I made Partout connect to a VPN on Android, with Linux syscalls over a Swift codebase that talks to Kotlin via JNI (!). That chill of “could this even work?”, because nobody had done it before. You hardly get that feeling with battle-tested languages, and if you like novelties, Swift is a greenfield that I would encourage you to explore and support.

The footprint is huge by default

In a former article, I mentioned that Foundation is not part of the standard library, as in it’s not part of the essential runtime (libSwiftCore). In terms of size, the core runtime is not excessively concerning at around 10-15MB total. In fact, I’m way more annoyed by the fact that it’s made of tens of files. Well, add Foundation to the mix, and your runtime spikes to almost 100MB, with ICU contributing to around 40MB of it. And Foundation is hard to avoid. Frankly, that’s insane, but I still want to believe that this is the kind legacy from the Apple era of Swift. Let’s not forget that Foundation is bundled with any Apple OS.

My advice? If you’re starting from scratch, and footprint is a concern, never import Foundation, or maybe consider not using Swift at all. Go is more mature for the purpose of standalone outputs, easy to build, ubiquitous. If you want to stick with Swift or port your existing codebase to non-Apple, instead, consider dropping Foundation and reimplementing the parts that you need. With careful analysis, you might conclude that, despite its convenience, you don’t need it to the point of justifying a 100MB executable.

Bottom line

So, it’s 2025 and you want to make native cross-platform apps or libraries. Hard truth: if you want to make them today, Go is a wiser choice. If you want to use Swift, for now, prefer CMake over SwiftPM. Get your feet wet with the raw tooling to understand how Swift libraries are generated. Don’t be like me, keep a flat source hierarchy from the beginning.

In the next article, we’ll go through real examples of how I managed to build consistent outputs with SwiftPM/CMake on macOS, Linux, Windows, and even Android.