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:

import PackageDescription

let package = Package(
name: "cli",
platforms: [...],
products: [
.executable(name: "cli", targets: ["CLI"]),
...
],
traits: [...],
dependencies: [...],
targets: [
.executableTarget(
name: "CLI",
dependencies: [...],
resources: [
...
],
swiftSettings: ...
),
.target(
name: "TargetA",
dependencies: [...],
resources: [
...
],
swiftSettings: ...
),
.target(
name: "TargetB",
dependencies: [...],
swiftSettings: ...
)
]
)

Example entry point:

import ArgumentParser

@main
struct CLI: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init(
commandName: "cli",
abstract: "My Swift CLI",
version: "1.0.0",
subcommands: [...]
)
}

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.

  1. Find the static Linux SDK you want at https://www.swift.org/install/macos/#osx-builds.
  2. 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
  3. Validate the SDK is installed with swift sdk list.

xcrun

  1. Download and install a matching toolchain package from https://www.swift.org/install/macos/#osx-builds.
  2. 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).
  3. Copy the CFBundleIdentifier (for example org.swift.624202602241a) and verify the toolchain is visible to xcrun: xcrun --toolchain org.swift.624202602241a swift --version.
  4. Build using that toolchain: xcrun --toolchain org.swift.624202602241a swift build -c release BUILD_OPTIONS.

Swiftly

  1. Install swiftly per https://www.swift.org/install/macos/#swiftly.
  2. Install the desired toolchain and make swiftly switch to it (update .swift-version if necessary).
  3. Confirm swift --version reports the chosen toolchain.
  4. 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:

  1. Add -Xlinker -s to the swift build command to strip the symbol table at link time.
  2. Run the strip* tool on the produced binary (for example strip MY_BINARY) to remove additional symbols.

Build for release with swift build -c release BUILD_OPTIONS (example numbers using swift-6.2.4-RELEASE on a project of mine with 45 dependencies, 7 targets, ~30 subcommands):

PlatformArchBUILD_OPTIONSDefault-Xlinker -sstrip*-Xlinker -s + strip*
macOSarm64--triple arm64-apple-macosx72.8MB37MB31.1MB31.1MB
macOSx86_64--triple x86_64-apple-macosx75.3MB39.6MB33.8MB33.8MB
linuxaarch64--swift-sdk aarch64-swift-linux-musl341.8MB98MB98MB98MB
linuxx86_64--swift-sdk x86_64-swift-linux-musl340.9MB105.2MB105.2MB105.2MB
macOSarm64/x86_64--arch arm64 --arch x86_64140.4MB73.7MB61.4MB61.4MB
macOSarm64/x86_64lipo -create -output x x86_64 arm64**148.1MB76.6MB64.9MB64.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 -s output is slightly bigger than from strip* for macOS due to embedded reflection metadata. -Xlinker -s doesn't touch this and only strips the symbol table. If you are ok with a slightly bigger binary you can skip the strip dependencies and defer reduction to the removal of the symbol table alone.

Gotchas

  1. Resources are not included automatically in the single binary; package them alongside the binary (see Packaging below).
  2. In my experience, --arch style 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 --arch builds to succeed. (however it might have been user error...)

Packaging

macOS

  1. Create a directory.
  2. Copy the binary into it (after running strip, if desired).
  3. Copy the build bundles for any target that has resources. In my example we copy from CLI and TargetA because they declare .resources([]) in Package.swift.
mkdir macos

# copy binary

# if using --arch build
cp .build/apple/Products/Release/cli macos/cli
# if using --triple and lipo
cp $YOUR_LIPO_OUTPUT_LOCATION macos/cli

# Copy resource bundles for any target that includes `.resources`

# if using --arch build
cp -r .build/apple/Products/Release/cli_CLI.bundle macos/
cp -r .build/apple/Products/Release/cli_TargetA.bundle macos/
# if using --triple and lipo
cp -r .build/release/cli_CLI.bundle macos/
cp -r .build/release/cli_TargetA.bundle macos/

Codesigning

  1. Create a "Developer ID Application" certificate in the Apple Developer portal.
  2. Download and install it into your Keychain.
  3. Validate that security find-identity lists it. It should contain "Developer ID Application: ..." (this is the identity string used by codesign).
codesign --sign "Developer ID Application: My Team Name (TEAM_ID)" --options runtime --timestamp cli

Create Package Installer

  1. Create a "Developer ID Installer" certificate in the Apple Developer portal.
  2. Download and install it into your Keychain.
  3. Validate that security find-identity lists it. It should contain "Developer ID Installer: ..." (this is the identity string used by productbuild).
  4. Create a ComponentList.plist file with an entry for each bundle you copied:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>BundleHasStrictIdentifier</key> <!-- If something is at this path, verify it has matching bundle identifier before overwriting -->
<true/>
<key>BundleIsRelocatable</key> <!-- Always install to /usr/local/bin, never look for an existing copy -->
<false/>
<key>BundleIsVersionChecked</key> <!-- Always overwrite, even if the version strings look the same or older - bundles mutch match the binary -->
<false/>
<key>BundleOverwriteAction</key> <!-- Replace existing bundle contents on upgrade -->
<string>upgrade</string>
<key>RootRelativeBundlePath</key>
<string>cli_CLI.bundle</string> <!-- Make another entry but with cli_TargetA.bundle as the path -->
</dict>
</array>
</plist>
pkgbuild --root macos --identifier my.bundle.identifier --version 1.0.0 --install-location /usr/local/bin --min-os-version 15.0 --compression latest --component-plist ComponentList.plist cli-macos.pkg
productbuild --package cli-macos.pkg --identifier my.bundle.identifier --version 1.0.0 --sign "Developer ID Installer: My Team Name (TEAM_ID)" cli-1.0.0-macos.pkg
tar -czvf cli-1.0.0-macos.pkg
rm -rf cli-macos.pkg macos

Make sure to match --min-os-version option with whatever .macOS(.vXX) you configured in Package.swift. Choose whatever --compression option (if any) works for you.

Notarizing the Installer

  1. Create an app-specific password for your Apple ID account.
  2. 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
  3. Use the keychain profile to notarize and staple your package installer:

<profile> is the keychain profile name

xcrun notarytool submit cli-1.0.0-macos.pkg --keychain-profile <profile> --wait
xcrun stapler staple cli-1.0.0-macos.pkg

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.

  1. Create a directory.
  2. Copy the binary into it (after running strip*, if desired).
  3. Copy the build bundles for any target that has resources. In my example we copy from CLI and TargetA because they declare .resources([]) in the Package.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):

ArchInvocation-Xlinker -s + strip*upx
aarch64--swift-sdk aarch64-swift-linux-musl98MB35.8MB
x86_64--swift-sdk x86_64-swift-linux-musl105.2MB40.4MB

Bundle and Tar

mkdir aarch64

cp .build/aarch64-swift-linux-musl/cli aarch64/cli

# Copy resource bundles for any target that includes `.resources`
cp -R .build/aarch64-swift-linux-musl/release/cli_CLI.resources aarch64/
cp -R .build/aarch64-swift-linux-musl/release/cli_TargetA.resources aarch64/

tar -czvf cli-1.0.0-linux-aarch64.tar.gz -C aarch64

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:

As always, if you know of a better way to do something, please let me know!


Tagged With: