How do I cross compile a Swift statically linked binary?
Resources
- https://www.swift.org/documentation/articles/static-linux-getting-started.html
- Swift to the cloud in a single step - Euan Harris: https://www.youtube.com/watch?v=9AaINsCfZzw
- WWDC24: What’s new in Swift | Apple - (7:23) https://www.youtube.com/watch?v=17fZZ1v_N2U&t=457s
- https://theswiftdev.com/building-static-and-dynamic-swift-libraries-using-the-swift-compiler/
- https://github.com/swiftlang/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md
- https://github.com/swiftlang/swift-sdk-generator
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:
- Real and the same:
/System/Volumes/Data/Users/$USER/Library/org.swift.swiftpm/swift-sdks/
/Users/$USER/Library/org.swift.swiftpm/swift-sdks/
- Symlink:
/Users/$USER/.swiftpm/swift-sdks
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
]
)
]
- About strip in general:
- Its companion strings : https://www.howtogeek.com/427805/how-to-use-the-strings-command-on-linux/
† 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!