Last week, three repos submitted patches on the same day, and CI ended up rebuilding the same internal audio-processing framework three times — each run taking nearly eight minutes just for the archive step. When the third build failed because the signing certificate had expired, whoever was on call asked in chat: "Wasn't this just built yesterday? Why does every repo have to rebuild it from scratch?" That question hits a nerve for a lot of teams: a shared binary framework should only need to be compiled once, but without a caching layer, every pipeline ends up doing the same work over and over.
On a dedicated cloud Mac from HireVPS, we rebuilt this workflow from the ground up: compile a multi-slice XCFramework once, sign it, compute a checksum, and drop it into a lightweight local binary cache that multiple repos can pull from directly — no redundant builds required. Here's how we set it up.
Why bother caching XCFrameworks yourself
Swift Package Manager's binaryTarget support for referencing prebuilt XCFrameworks isn't new. What actually makes it useful is having an artifact that's trustworthy, traceable, and shareable across teams. If every repo's CI runs its own archive, you end up with several binaries that are supposedly identical but never actually verified to match — and if there's any subtle difference in the build environment (say, a minor Xcode point release), tracking down the discrepancy becomes a headache. Compiling once and distributing centrally is the only way to guarantee every downstream consumer gets the exact same artifact.
Rule of thumb: any internal framework referenced by three or more repos is worth pulling out into its own binary cache. Below that threshold, the overhead of maintaining a cache may outweigh the cost of just rebuilding it each time.
Why a cloud Mac's resources matter here
This workflow has two hard requirements: a dedicated macOS environment (GUI and CLI) to run a full xcodebuild archive, and enough headroom on disk I/O when compiling multiple architecture slices, since shared-resource machines tend to slow each other down under that kind of load. Renting an M4 or M4 Pro on HireVPS by the day gets you independent SSH credentials and root access right after picking a node — no fighting other tenants for CPU cycles, and a multi-architecture build that runs for hours won't get interrupted by a noisy neighbor. Check the console for which machine types and nodes are currently available.
Building a multi-slice XCFramework
An XCFramework that works for both devices and simulators needs at least two slices: ios-arm64 for physical devices and ios-arm64-simulator for Apple Silicon simulators. If anyone on your team is still running simulators on an Intel Mac, add ios-x86_64-simulator too — otherwise their test runs will fail with an architecture mismatch.
Archive each slice separately
xcodebuild archive \
-scheme AudioCore \
-destination "generic/platform=iOS" \
-archivePath build/AudioCore-iOS.xcarchive \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
xcodebuild archive \
-scheme AudioCore \
-destination "generic/platform=iOS Simulator" \
-archivePath build/AudioCore-iOSSim.xcarchive \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
Merge into an XCFramework
xcodebuild -create-xcframework \
-framework build/AudioCore-iOS.xcarchive/Products/Library/Frameworks/AudioCore.framework \
-framework build/AudioCore-iOSSim.xcarchive/Products/Library/Frameworks/AudioCore.framework \
-output build/AudioCore.xcframework
Once it's built, zip it up — that archive is the final artifact you'll distribute. Don't touch it by hand after this point.
Signing and checksums
It's worth signing internal frameworks too, so downstream consumers can verify provenance:
codesign --sign "Apple Distribution: Your Team" \
--timestamp \
build/AudioCore.xcframework
zip -r AudioCore-1.4.0.xcframework.zip build/AudioCore.xcframework
After signing and zipping, run SwiftPM's built-in command to compute a checksum. That value goes straight into the downstream repo's Package.swift:
swift package compute-checksum AudioCore-1.4.0.xcframework.zip
The most common cause of checksum verification failures is editing the zip contents after the fact, or switching compression tools and ending up with a different internal file order. The rule is simple: once you've computed the checksum, that zip file is frozen — don't touch it again.
Setting up a local binary cache
You don't need anything elaborate here — a directory structure that can serve static files over HTTP is enough:
| Level | Contents | Example path |
|---|---|---|
| Framework name | One directory per internal framework | /cache/AudioCore/ |
| Version | One subdirectory per version | /cache/AudioCore/1.4.0/ |
| Artifacts | zip archive + checksum text | AudioCore-1.4.0.xcframework.zip, checksum.txt |
Downstream repos reference it in Package.swift like this:
.binaryTarget(
name: "AudioCore",
url: "https://cache.internal.example/AudioCore/1.4.0/AudioCore-1.4.0.xcframework.zip",
checksum: "the checksum computed in the previous step"
)
For storage planning, keeping the last 10 versions of 3-4 core frameworks is plenty for a mid-sized team. A single archive is usually 20-80MB, so total storage stays within a few GB. Prune old versions that haven't been pulled in 90 days.
Wiring it into multi-repo CI pipelines
Downstream CI no longer runs xcodebuild archive at all — it just calls swift package resolve to pull the specified version, cutting build time from minutes of compilation down to seconds of download. When shipping a new version, just create a new version directory in the cache and update the checksum reference in the relevant repo — no changes needed to the cache server itself.
Gotcha: never let multiple repos reference a mutable tag like "latest." Checksums assume a one-to-one mapping between URL and content — the moment you allow the file under a given version number to be overwritten, SwiftPM's integrity verification stops meaning anything.
Checklist
- Do the slices cover both physical devices and every simulator architecture you need?
- Was the checksum computed immediately after zipping, with no further edits to the archive?
- Is each version directory in the cache written once and never overwritten?
- Does the checksum in the downstream
Package.swiftmatch the actual file? - Have versions untouched for 90+ days been cleaned up?
Frequently asked questions
Which slices does an XCFramework need to cover devices and simulators?
You need at minimum ios-arm64 for devices and ios-arm64-simulator for Apple Silicon simulators; if any Intel Mac in the team still runs simulators, add ios-x86_64-simulator too, otherwise tests fail immediately with an architecture mismatch.
Why does binaryTarget checksum verification fail?
The usual cause is editing the zip after packaging, or using a different compression tool that reorders internal files; the fix is to run swift package compute-checksum directly against the final zip you plan to ship and never touch that file again.
How much storage should a local binary cache server plan for?
For a mid-size team keeping the last 10 versions of 3-4 core frameworks, each compressed artifact usually sits between 20-80MB, so a few GB total is enough — put it on its own partition on the cloud Mac's SSD and prune versions untouched for 90+ days.
HireVPS
Try a dedicated cloud Mac mini today
Rent by the day, get SSH/VNC credentials in 2 minutes, and upgrade your configuration anytime.