Binary Targets in Swift Package Manager (SPM) allow packages to declare xcframework bundles as available targets. The technique is often used to provide access to closed-source libraries and can improve CI performance by reducing time spent on fetching SPM repositories.
Both downs and upsides are essential to consider when adding binary targets to your project. It’s essential to understand the problem they solve and how xcframeworks play a role in their supported platforms.
What is a binary target?
A binary target is explained as a target referencing a specific binary. Binary frameworks are pre-compiled source code joined by a defined interface that allows it to be used in your apps. A binary framework can be defined as static or dynamic.
Swift Package Manager allows defining binary targets by referencing so-called xcframeworks.
An XCFramework allows the distribution of multiple frameworks and libraries required to build for multiple platforms. The included frameworks can be static or dynamic and define the supported platforms for the xcframework.
Inside an XCFramework, you’ll find a framework for each supported architecture. For example, Firebase’s Crashlytics xcframework looks as follows:
You can tell from this overview that there’s support for tvOS, macOS, and iOS.
Info.plistfile defines all available frameworks:
<?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"> <dict> <key>AvailableLibraries</key> <array> <dict> <key>LibraryIdentifier</key> <string>ios-arm64_x86_64-maccatalyst</string> <key>LibraryPath</key> <string>FirebaseCrashlytics.framework</string> <key>SupportedArchitectures</key> <array> <string>arm64</string> <string>x86_64</string> </array> <key>SupportedPlatform</key> <string>ios</string> <key>SupportedPlatformVariant</key> <string>maccatalyst</string> </dict> // .. Other frameworks </array> <key>CFBundlePackageType</key> <string>XFWK</string> <key>XCFrameworkFormatVersion</key> <string>1.0</string> </dict> </plist>
Your apps will only build for the platforms supported by the referenced xcframeworks. In other words, you’ll need to ensure that any binary target added matches the supported platforms of your application.
The downsides of using binary targets
Before considering adding binary targets to your project, knowing the downsides is essential. Binary targets are great for distributing closed-source frameworks, but that also means debugging becomes harder. Since you can only reference header files, you won’t be able to debug through the lines of code as you would otherwise be able to do when referencing packages as regular SPM targets.
Secondly, an xcframework only supports the platforms for which it contains frameworks or libraries. Platforms move forward by adopting new architectures, requiring you to update and rebuild the XCFramework libraries to add support for new architectures. You can take the adoption of Apple silicon as a recent example.
The upsides of binary targets
At this point, you might wonder what the benefits of using a binary target are. A commonly mentioned benefit is that you can distribute frameworks as closed source. Yet, I want to focus on a more major upside that I experienced lately after trying to speed up our CI.
A downside of Swift Package Manager is that the whole repository has to be downloaded before your project can start building. I’m sure many of you have experienced long loading times in Xcode while waiting for all packages to be fetched. I’ve experienced examples of repositories containing snapshots for testing with a total size of more than 100MB. While I was only interested in building the actual framework, I also had to wait to fetch all those snapshots.
Swift Package Manager caches its fetched frameworks to optimize performance in a folder structure that looks as follows:
The repositories directory contains files for the Git tag matching the latest release you’ve fetched. Files are optimized using so-called Packfiles to save space and be more efficient. Yet, it still means SPM has to fetch all the large files defined inside the repository.
As an example, I downloaded the Swift Package Manager cache directory of our CI system and analyzed the size of the repositories folder:
[email protected] repositories % du -sh */ | sort -hr 357M lottie-ios-0271592b/ 173M firebase-ios-sdk-a1569f98/ 117M grpc-ios-9a5f03c1/ 44M boringssl-SwiftPM-c3eba25b/ 40M ContentGallery-20ca4562/ 34M Auth0.swift-483a50e2/ 33M PanModal-f760eb02/ 32M Alamofire-e8f130fe/ 30M dd-sdk-ios-ecc3edec/ 29M swift-protobuf-9f1e4ec4/ 26M Gifu-05fd1ae0/ 24M plcrashreporter-606c3f9d/ 23M UINotifications-98c8d6f9/ 18M snowplow-objc-tracker-08ee2f30/ 18M purchases-ios-92046cd5/ 17M CryptoSwift-72c2bbc7/ 12M swifter-2897f0d5/ 12M Nuke-6e25c069/ 8.4M Zip-06769e8d/ 6.9M Amplitude-iOS-8df29baf/ 6.7M KeychainAccess-d7d19352/ 4.4M nanopb-208d9073/ 4.1M Diagnostics-9c08c655/ 3.5M abseil-cpp-SwiftPM-cc296c64/ 2.8M fmdb-edf51281/ 2.8M JWTDecode.swift-88cf1fc9/ 2.1M gtm-session-fetcher-66fe3e42/ 1.7M dd-sdk-swift-testing-6bc33eba/ 1.6M leveldb-12d5b35c/ 1.4M GoogleDataTransport-2feee0f8/ 1.3M SimpleKeychain-ad0e4670/ 1.3M Mocker-ce713560/ 1.0M GoogleUtilities-34a5fa84/ 696K promises-effc8acf/ 644K BlueRSA-df3fcd6a/ 568K combine-schedulers-6797c0bf/ 344K experiment-ios-client-edba1876/ 204K Trekker-cbf63343/ 196K xctest-dynamic-overlay-856e3cf8/ 168K SwiftUIKitView-3a178e3e/ 144K GoogleAppMeasurement-0ed9e027/ 104K analytics-connector-ios-280b19df/ 96K ExceptionCatcher-8424f3ff/
Frameworks like Lottie and Firebase represent a significant portion of the cache size. Even worse, we only use a portion of the frameworks Firebase provides while we still have to fetch all the files defined in the repository.
While the Swift Package Registry should be an actual solution to this problem, we currently have to deal with how SPM works.
Binary targets to the rescue
When dealing with large repositories you can start considering binary targets to help you speed up CI and SPM performance. Carefully consider whether you’ll need to often debug those targets as you won’t have access to the actual code anymore. In our case, we never really debug Firebase, so we decided to switch over to directly referencing xcframeworks.
For us, this resulted in only having to fetch 117MB directly from Git compared to ~360MB+. A significant improvement that can vary based on the frameworks your project needs.
There are several ways of including XCFrameworks, for which I’d like to refer you to this thread specifically. The thread covers the same problem I faced, resulting in a specific solution for Firebase. Yet, you might be looking to add binary targets for any framework.
Defining a binary target
You can define a binary target using a local path or a remote URL. The latter will let SPM fetch the binary up on fetching all other packages. The local path reference can be helpful for local packages or when you want to provide binary targets for frameworks that don’t offer xcframeworks themselves. Note that this is only required if you define dependencies inside a
Package.swift file. You can add xcframework files directly into Xcode otherwise.
Remote Binary Targets
An example binary target referencing a remote URL can look as follows:
.binaryTarget( name: "SwiftLintBinary", url: "https://github.com/realm/SwiftLint/releases/download/0.49.1/SwiftLintBinary-macos.artifactbundle.zip", checksum: "227258fdb2f920f8ce90d4f08d019e1b0db5a4ad2090afa012fd7c2c91716df3" )
This is an example of SwiftLint that provides a binary with each release. Remote binary targets require you to provide a checksum to verify that the hosted archive file matches the archive you declare in the manifest file. Not all packages provide such checksum, so let me explain how you can define them yourself.
In case your package comes with a
Package.swift file you can execute the following command in your terminal:
swift package compute-checksum FirebaseAnalytics.xcframework Prints: 547258fdb2f920f8ce90d4fkli9873mb0db5a4ad2090afa012fd7c2c91716ds1
However, you might be running into an error:
error: Could not find Package.swift in this directory or any of its parent directories.
In this case, you can run the following command:
shasum -a 256 SwiftLintBinary-macos.artifactbundle.zip | sed 's/ .*//' Prints: 227258fdb2f920f8ce90d4f08d019e1b0db5a4ad2090afa012fd7c2c91716df3
Local Binary Targets
You can define local binary targets using a path inside your
.binaryTarget( name: "FirebaseAnalytics", path: "FirebaseAnalytics.xcframework" )
Since you’re referencing a framework found locally, there’s no need to compare a checksum for security reasons.
Creating an xcframework
Referencing an xcframework only works if the framework you’re trying to adopt provides an actual xcframework variant. In my case, Lottie didn’t yet provide such a framework, so I decided to create one myself.
Creating multiplatform binaries can be complicated for each library you work with. Therefore, I’d like to reference the detailed instructions provided by Apple here.
Following Apple’s instructions I downloaded the Lottie repository and executed the following command from the terminal:
$ xcodebuild archive -workspace Lottie.xcworkspace -scheme "Lottie (iOS)" -destination generic/platform=iOS -archivePath "archives/Lottie_iOS" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
And another command for adding support for the Simulator:
$ xcodebuild archive -workspace Lottie.xcworkspace -scheme "Lottie (iOS)" -destination "generic/platform=iOS Simulator" -archivePath "archives/Lottie_iOS_Simulator" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
Lastly, I executed the following command to generate an xcframework:
$ xcodebuild -create-xcframework -framework archives/Lottie_iOS.xcarchive/Products/Library/Frameworks/Lottie.framework -framework archives/Lottie_iOS_Simulator.xcarchive/Products/Library/Frameworks/Lottie.framework -output xcframeworks/Lottie.xcframework xcframework successfully written out to: /Users/avanderlee/Downloads/lottie-ios-master 2/xcframeworks/Lottie.xcframework
The result of these commands is a new xcframework:
We can start referencing this xcframework locally inside our package manifest file:
.binaryTarget( name: "Lottie", path: "Lottie.xcframework" )
Binary targets in Swift Package Manager allow you to optimize the performance of SPM and CI by no longer having to download all repository files. While there are downsides like no longer being able to debug through the code, there are upsides like a reduced size of packages cache. When there’s no xcframework available, you can create one yourself.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.