Keith Smiley | About

Cross compiling for Apple Silicon with Swift Package Manager

If you distribute binaries for command line tools built with Swift Package Manager, you might have previously built your distribution binary with:

% swift build --configuration release

If you inspect the binary, you can see it was built for the current machine's architecture by default:

% file .build/release/package
.build/release/package: Mach-O 64-bit executable x86_64

Previously, this was sufficient since macOS only supported one architecture. Now, in order to fully utilize the native performance of Apple Silicon chips, we need to produce a fat binary that contains a slice for both x86_64 and arm64.

Swift Package Manager has a few different ways to achieve this. The easiest way, as far as I can tell, is to pass the hidden --arch flag once for each architecture:

% swift build --configuration release --arch arm64 --arch x86_64

This goes through a different code path in Swift Package Manager, and utilizes Xcode's underlying XCBuild tool. This results in the built binary being in a different path than usual. Inspecting the new artifact, we can see we have a binary containing both requested architectures:

% file .build/apple/Products/Release/package
.build/apple/Products/Release/package: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
.build/apple/Products/Release/package (for architecture x86_64):        Mach-O 64-bit executable x86_64
.build/apple/Products/Release/package (for architecture arm64): Mach-O 64-bit executable arm64

Another option is to build once for each architecture, and then combine the binaries using lipo. Unlike the --arch option, this approach also works on Linux. Here's an example:

% swift build --configuration release --triple arm64-apple-macosx
% swift build --configuration release --triple x86_64-apple-macosx
% lipo -create -output package .build/arm64-apple-macosx/release/package .build/x86_64-apple-macosx/release/package

Inspecting our final binary we can see it correctly has both architectures:

% file package
package: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
package (for architecture x86_64):      Mach-O 64-bit executable x86_64
package (for architecture arm64):       Mach-O 64-bit executable arm64