How do I cross compile a Swift statically linked binary?

Resources

In the post about podman and sharing images I referenced having a linux static binary available to use in my container image.

How’d I get it?

The first thing I did was simply follow the tutorial with a demo hello app provided by Swift.org.

To complete it I needed two things, an NON-Xcode open source version of Swift and a Swift Linux SDK.

Non-Xcode Swift

In the shell where one intends to compile it’s important to run

which swift
swift --version

to make sure the non-Xcode version of Swift will be used. I had trouble with VSCode’s integrated terminal defaulting to the XCode toolchain. I haven’t bothered to troubleshoot too deeply, but it seems to be tied to the xcode-select version being preferred by default. This may have JUST been addressed, but for now I just used Terminal instead.

Swiftly

If you haven’t installed Swiftly on your Mac, it’s a good choice for switching between different version of Swift. It’s now the recommended path on the swift/install/macos page

I used

# https://formulae.brew.sh/formula/swiftly
brew install swiftly

but it’s doable by hand:

curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && \
installer -pkg swiftly.pkg -target CurrentUserHomeDirectory && \
~/.swiftly/bin/swiftly init --quiet-shell-followup && \
. "${SWIFTLY_HOME_DIR:-$HOME/.swiftly}/env.sh" && \
hash -r

Example usage:

## install and switch
swiftly install --use latest
which swift
# /Users/$USER/.swiftly/bin/swift
swift --version
# Apple Swift version 6.1.2 (swift-6.1.2-RELEASE)
# Target: arm64-apple-macosx15.0

## to see other toolchains visible to Swiftly
swiftly list 
## to switch
swiftly use $SOME_TOOLCHAIN_IN_LIST

Linux SDK

Since I’m compiling on a Mac I need the software dev kit that will work for my target version of Linux.

I found the reference I needed halfway down the main install page.

The dev kit needs to match the version of Swift one is compiling with, so since I am using the latest release (6.1.2), that’s the dev kit I need.

swift sdk install https://download.swift.org/swift-6.1.2-release/static-sdk/swift-6.1.2-RELEASE/swift-6.1.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz
 --checksum df0b40b9b582598e7e3d70c82ab503fd6fbfdff71fd17e7f1ab37115a0665b3b

To check that its there

swift sdk list

The installer put the sdk at:

Building An Example Package

These are all the steps to run and test the the example, assuming one has a remote machine to port it to. If not check out some of the podman series to run it in a container!

cd Place/For/Code
mkdir hello
cd hello
swift package init --type executable
swift build --swift-sdk x86_64-swift-linux-musl
file .build/x86_64-swift-linux-musl/debug/hello
scp .build/x86_64-swift-linux-musl/debug/hello username@remote.example.com:~/hello
ssh username@remote.example.com ~/hello

Applying To an Existing Project

With the SDK’s installed one can do this for any Swift package with an executable†.

cd /location/of/MyPackage
swift build -configuration release --swift-sdk x86_64-swift-linux-musl
file .build/x86_64-swift-linux-musl/release/$EXECUTABLE_NAME

Once moving into release mode do consider having the linker perform a full strip. It makes the binaries much smaller, and one could argue its a good idea to do for binaries going on internet exposed machines in general. I had forgotten about this until reminded!

    targets: [
        .executableTarget(
            name: "hello-world",
            dependencies: [
                .product(name: "Hummingbird", package: "hummingbird"),
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
            ],
            linkerSettings: [
				.unsafeFlags(["-Xlinker", "-s"], .when(configuration: .release)), // STRIP_STYLE = all
			]
        )
    ]

† written with frameworks that will run on Linux

There is another way to cross compile that involves containers that will be the next post in the podman series!