Packaging Swift Executables
Posted Monday, April 6, 2026.This post walks through a complete workflow for turning a Swift Package Manager executable into distributable artifacts: building with a chosen toolchain and static SDK, reducing binary size, codesigning the macOS binary, creating a signed installer, and submitting for notarization. This will be helpful if you ship Swift CLIs and want a repeatable, auditable macOS installer plus cross‑platform Linux bundles.
Package structure
Imagine we have:
Example entry point:
You don't have to use
ArgumentParser, but it's my go-to for Swift CLIs.
Building executable
Toolchain and Static SDK
You can't use the Apple-provided Swift inside Xcode - you need a non-Xcode release toolchain and a static SDK for Linux builds. Whether you install a toolchain manually or use Swiftly, download a static Linux SDK that matches the toolchain version.
At the time of writing, Swift 6.3 is out, but it caused some compile errors in my project - so sticking with 6.2.4 for the moment. These instructions will work for 6.3 assuming your project builds fine with that version.
- Find the static Linux SDK you want at https://www.swift.org/install/macos/#osx-builds.
- Copy and run the provided install command (example for 6.2.4):
swift sdk install https://download.swift.org/swift-6.2.4-release/static-sdk/swift-6.2.4-RELEASE/swift-6.2.4-RELEASE_static-linux-0.1.0.artifactbundle.tar.gz --checksum 24bdf84495dd31a6de2eb679647c1982b747bfbfe1a2060c779d84dcecd902a4 - Validate the SDK is installed with
swift sdk list.
xcrun
- Download and install a matching toolchain package from https://www.swift.org/install/macos/#osx-builds.
- Inspect the toolchain info with:
cat /Library/Developer/Toolchains/swift-6.2.4-RELEASE.xctoolchain/Info.plist(or~/Library/Developer/Toolchains/...for a user install). - Copy the
CFBundleIdentifier(for exampleorg.swift.624202602241a) and verify the toolchain is visible toxcrun:xcrun --toolchain org.swift.624202602241a swift --version. - Build using that toolchain:
xcrun --toolchain org.swift.624202602241a swift build -c release BUILD_OPTIONS.
Swiftly
- Install
swiftlyper https://www.swift.org/install/macos/#swiftly. - Install the desired toolchain and make
swiftlyswitch to it (update.swift-versionif necessary). - Confirm
swift --versionreports the chosen toolchain. - Build as usual:
swift build -c release BUILD_OPTIONS.
Reducing Size
Default builds can produce large binaries — especially when using static SDKs. Options to reduce size:
- Add
-Xlinker -sto theswift buildcommand to strip the symbol table at link time. - Run the
strip* tool on the produced binary (for examplestrip MY_BINARY) to remove additional symbols.
Build for release with
swift build -c release BUILD_OPTIONS(example numbers usingswift-6.2.4-RELEASEon a project of mine with 45 dependencies, 7 targets, ~30 subcommands):
| Platform | Arch | BUILD_OPTIONS | Default | -Xlinker -s | strip* | -Xlinker -s + strip* |
|---|---|---|---|---|---|---|
| macOS | arm64 | --triple arm64-apple-macosx | 72.8MB | 37MB | 31.1MB | 31.1MB |
| macOS | x86_64 | --triple x86_64-apple-macosx | 75.3MB | 39.6MB | 33.8MB | 33.8MB |
| linux | aarch64 | --swift-sdk aarch64-swift-linux-musl | 341.8MB | 98MB | 98MB | 98MB |
| linux | x86_64 | --swift-sdk x86_64-swift-linux-musl | 340.9MB | 105.2MB | 105.2MB | 105.2MB |
| macOS | arm64/x86_64 | --arch arm64 --arch x86_64 | 140.4MB | 73.7MB | 61.4MB | 61.4MB |
| macOS | arm64/x86_64 | lipo -create -output x x86_64 arm64** | 148.1MB | 76.6MB | 64.9MB | 64.9MB |
* strip refers to the platform-appropriate strip tool (macOS strip, aarch64-elf-strip*** for aarch64 Linux, x86_64-linux-gnu-strip*** for x86_64 Linux).
** lipo tool uses the output of the two --triple (arm64|x86_64)-apple-macosx binaries
*** Can be installed with brew install x86_64-linux-gnu-binutils aarch64-elf-binutils
-Xlinker -soutput is slightly bigger than fromstrip* for macOS due to embedded reflection metadata.-Xlinker -sdoesn't touch this and only strips the symbol table. If you are ok with a slightly bigger binary you can skip thestripdependencies and defer reduction to the removal of the symbol table alone.
Gotchas
- Resources are not included automatically in the single binary; package them alongside the binary (see Packaging below).
- In my experience,
--archstyle builds did not work with package plugins. I switched to running generators as a separate step (for example:swift package plugin --allow-writing-to-package-directory generate-code-from-openapi --target ...). That allowed--archbuilds to succeed. (however it might have been user error...)
Packaging
macOS
- Create a directory.
- Copy the binary into it (after running
strip, if desired). - Copy the build bundles for any target that has resources. In my example we copy from
CLIandTargetAbecause they declare.resources([])inPackage.swift.
Codesigning
- Create a "Developer ID Application" certificate in the Apple Developer portal.
- Download and install it into your Keychain.
- Validate that
security find-identitylists it. It should contain "Developer ID Application: ..." (this is the identity string used bycodesign).
Create Package Installer
- Create a "Developer ID Installer" certificate in the Apple Developer portal.
- Download and install it into your Keychain.
- Validate that
security find-identitylists it. It should contain "Developer ID Installer: ..." (this is the identity string used byproductbuild). - Create a
ComponentList.plistfile with an entry for each bundle you copied:
Make sure to match
--min-os-versionoption with whatever.macOS(.vXX)you configured inPackage.swift. Choose whatever--compressionoption (if any) works for you.
Notarizing the Installer
- Create an app-specific password for your Apple ID account.
- Use it to create a keychain profile:
xcrun notarytool store-credentials <profile> --apple-id MY_APPLE_ID --password MY_APP_SPECIFIC_PASSWORD --team-id TEAM_ID - Use the keychain profile to notarize and staple your package installer:
<profile>is the keychain profile name
You can also create a keychain profile using an App Store Connect API key:
xcrun notarytool store-credentials <profile-name> --key /path/to/AuthKey_ABC123XYZ.p8 --key-id ABC123XYZ --issuer 11111111-2222-3333-444444444444
Linux
There is no codesigning needed for Linux, but you must produce separate binaries for each architecture.
- Create a directory.
- Copy the binary into it (after running
strip*, if desired). - Copy the build bundles for any target that has resources. In my example we copy from
CLIandTargetAbecause they declare.resources([])in thePackage.swift.
Compressing binary
You can run upx to compress the binaries (brew install upx).
Build for release with
swift build -c release BUILD_OPTIONS(same project from above):
| Arch | Invocation | -Xlinker -s + strip* | upx |
|---|---|---|---|
| aarch64 | --swift-sdk aarch64-swift-linux-musl | 98MB | 35.8MB |
| x86_64 | --swift-sdk x86_64-swift-linux-musl | 105.2MB | 40.4MB |
Bundle and Tar
Do the same thing for your x86_64 build.
Summary
In this post we learned how to create a Swift CLI app which supports:
- macOS arm64
- macOS x86_64
- Linux aarch64
- Linux x86_64
This workflow demonstrates signing, notarizing, packaging resources, and producing a macOS installer.
Helpful resources that provided the foundation for this post:
- https://scriptingosx.com/2021/07/notarize-a-command-line-tool-with-notarytool/
- https://scriptingosx.com/2023/08/build-a-notarized-package-with-a-swift-package-manager-executable/
As always, if you know of a better way to do something, please let me know!
Tagged With: