Getting Started with the Static Linux SDK

It’s well known that Swift can be used to build software for Apple platforms such as macOS or iOS, but Swift is also supported on other platforms, including Linux and Windows.

Building for Linux is especially interesting because, historically, Linux programs written in Swift needed to ensure that a copy of the Swift runtime—and all of its dependencies—was installed on the target system. Additionally, a program built for a particular distribution, or even a particular major version of a particular distribution, would not necessarily run on any other distribution or in some cases even on a different major version of the same distribution.

The Swift Static Linux SDK solves both of these problems by allowing you to build your program as a fully statically linked executable, with no external dependencies at all (not even the C library), which means that it will run on any Linux distribution as the only thing it depends on is the Linux system call interface.

Additionally, the Static Linux SDK can be used from any platform supported by the Swift compiler and package manager; this means that you can develop and test your program on macOS before building and deploying it to a Linux-based server, whether running locally or somewhere in the cloud.

Static vs Dynamic Linking

Linking is the process of taking different pieces of a computer program and wiring up any references between those pieces. For static linking, generally speaking those pieces are object files, or static libraries (which are really just collections of object files).

For dynamic linking, the pieces are executables and dynamic libraries (aka dylibs, shared objects, or DLLs).

There are two key differences between dynamic and static linking:

The latter is important because traditionally, the static linker will include every object explicitly listed on its command line, but it will only include an object from a static library if doing so lets it resolve an unresolved symbolic reference. If you statically link against a library that you do not actually use, a traditional static linker will completely discard that library and not include any code from it in your final binary.

In practice, things can be more complicated—the static linker may actually work on the basis of individual sections or atoms from your object files, so it may in fact be able to discard individual functions or pieces of data rather than just whole objects.

Pros and Cons of Static Linking

Pros of static linking:

Cons of static linking:

On Linux in particular, it’s also possible to use static linking to completely eliminate dependencies on system libraries supplied by the distribution, resulting in executables that work on any distribution and can be installed by simply copying.

Installing the SDK

Before you start, it’s important to note:

Once that is out of the way, actually installing the Static Linux SDK is easy; at a prompt, enter

$ swift sdk install <URL-or-filename-here>

giving the URL or filename at which the SDK can be found.

For instance, assuming you have installed the swift-6.0-DEVELOPMENT-SNAPSHOT-2024-06-06-a toolchain, you would need to enter

$ swift sdk install

to install the corresponding Static Linux SDK.

Swift will download and install the SDK on your system. You can get a list of installed SDKs with

$ swift sdk list

and it’s also possible to remove them using

$ swift sdk remove <name-of-SDK>

Your first statically linked Linux program

First, create a directory to hold your code:

$ mkdir hello
$ cd hello

Next, ask Swift to create a new program package for you:

$ swift package init --type executable

You can build and run this locally:

$ swift build
Building for debugging...
[8/8] Applying hello
Build complete! (15.29s)
$ .build/debug/hello
Hello, world!

But with the Static Linux SDK installed, you can also build Linux binaries for x86-64 and ARM64 machines:

$ swift build --swift-sdk x86_64-swift-linux-musl
Building for debugging...
[8/8] Linking hello
Build complete! (2.04s)
$ file .build/x86_64-swift-linux-musl/debug/hello
.build/x86_64-swift-linux-musl/debug/hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
$ swift build --swift-sdk aarch64-swift-linux-musl
Building for debugging...
[8/8] Linking hello
Build complete! (2.00s)
$ file .build/aarch64-swift-linux-musl/debug/hello
.build/aarch64-swift-linux-musl/debug/hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped

These can be copied to an appropriate Linux-based system and executed:

$ scp .build/x86_64-swift-linux-musl/debug/hello linux:~/hello
$ ssh linux ~/hello
Hello, world!

What about package dependencies?

Swift packages that make use of Foundation or Swift NIO should just work. If you try to use a package that uses the C library, however, you may have a little work to do. Such packages often contain files with code like the following:

#if os(macOS) || os(iOS)
import Darwin
#elseif os(Linux)
import Glibc
#elseif os(Windows)
import ucrt
#error(Unknown platform)

The Static Linux SDK does not use Glibc; instead, it is built on top of an alternative C library for Linux called Musl. We chose this approach for two reasons:

  1. Musl has excellent support for static linking.

  2. Musl is permissively licensed, which makes it easy to distribute executables statically linked with it.

If you are using such a dependency, you will therefore need to adjust it to import the Musl module instead of the Glibc module:

#if os(macOS) || os(iOS)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#elseif os(Windows)
import ucrt
#error(Unknown platform)

Occasionally there might be a difference between the way a C library type gets imported between Musl and Glibc; this sometimes happens if someone has added nullability annotations, or where a pointer type is using a forward-declared struct for which no actual definition is ever provided. Usually the problem will be obvious—a function argument or result will be Optional in one case and non-Optional in another, or a pointer type will be imported as OpaquePointer rather than UnsafePointer<FOO>.

If you do find yourself needing to make these kinds of adjustments, you can make your local copy of the package dependency editable by doing

$ swift package edit SomePackage

and then editing the files in the Packages directory that appears in your program’s source directory. You may wish to consider raising PRs upstream with any fixes you may have.