Convert a Universal (FAT) Framework to an XCFramework

Matt Robinson
strava-engineering
Published in
4 min readFeb 17, 2021

Background

Strava consumes various third party frameworks via Carthage to power our application. These frameworks are either built from an Xcode project stored in a Github repository (Carthage’s github directive in the Cartfile) or distributed as prebuilt artifacts (Carthage’s binary directive in the Cartfile). The binary dependencies are typically distributed as universal (AKA fat) binaries.

$ cat Cartfilebinary “https://foo.com/downloads/foo.json" == 1.0.0

Typically, these universal frameworks are a multi-architecture binary that is the result of lipo-ing together multiple binaries to form a single framework binary. This multi-architecture binary would have some device slices (perhaps for both iOS and watchOS) and a simulator slice.

|-----------|
| x86_64 | \
|-----------| \ |---------------|
\ |------| | |
---> | lipo | ---> | x86_64, arm64 |
/ |------| | |
|-----------| / |---------------|
| arm64 | /
|-----------|

For the Carthage artifacts stored in Carthage/Build/iOS (for example), the architectures can be shown using lipo -i.

$ xcrun lipo -i Carthage/Build/iOS/Foo.framework/FooArchitectures in the fat file: Carthage/Build/iOS/Foo.framework/Foo are: armv7 i386 x86_64 arm64

x86_64 will be the slice that engineers use when working with the binary in the iPhone simulator shipped with macOS. arm64 will be the slice that engineers (and our athletes) use when running the code on a modern device.

As mentioned in previous posts, Strava checks the Carthage artifacts into our git repository to make repository management simpler for both engineers and CI. In practice, this means that multi-architecture binaries that are larger than 100 MB (Github’s strict file size limit, error: GH001: Large files detected) require the usage of git large file storage (git-lfs). This would increase our operational complexity so we wanted to avoid it.

$ git push ......remote: error: GH001: Large files detected. You may want to try Git Large File Storage — https://git-lfs.github.com.
remote: error: See http://git.io/iEPt8g for more information.
remote: error: File Carthage/Build/iOS/Foo.framework/Foo is 140.95 MB; this exceeds GitHub’s file size limit of 100.00 MB

To make the multi-architecture binary smaller on the filesystem, we can do a couple things:

  • Remove the armv7 slice since it isn’t used by our Strava application and is wasting bytes in our repository.
  • Separate the simulator and device architectures into separate binaries such that the size of each sliced up binary is less than the whole. This is easy to do using the new-ish XCFramework packaging.

Does Carthage’s new --use-xcframeworks solve this issue?

No. binary dependencies aren’t decomposed into an XCFramework, even when using both --use-xcframeworks and --no-use-binaries. The Carthage artifact is still the binary packaged by the publisher.

Transforming a Framework to XCFramework

Roughly, this process will separate the architectures using the reverse of how above combined them.

                                         |-----------|
/> | x86_64 |
/ |-----------|
|---------------| /
| x86_64 | |------| / |-----------|
| armv7 | ---> | lipo | --- > | armv7 🗑 |
| arm64 | |------| \ |-----------|
|---------------| \
\ |-----------|
\> | arm64 |
|-----------|

First, create duplicates of the original framework for the device and simulator portion of the XCFramework. This gives a slice-specific sandbox to run the lipo commands to the unnecessary architectures.

Foo.framework (Device)   Foo.framework (Simulator)
|---------------| |---------------|
| x86_64 | | x86_64 |
| armv7 | | armv7 |
| arm64 | | arm64 |
|---------------| |---------------|

Next, remove the unnecessary architectures from each framework depending on the goal (iOS device vs. iOS simulator, in this case). For Strava’s purposes, the device framework needs just arm64 and simulator framework needs just x86_64.

Foo.framework (Device)   Foo.framework (Simulator)
|---------------| |---------------|
| arm64 | | x86_64 |
| | | |
| | | |
|---------------| |---------------|

Finally, combine the two frameworks together using the XCFramework creation process outlined at WWDC 2019.

             Foo.xcframework
|------------------------------------|
| ios-arm64 ios-x86_64-simulator |
| |-------| |--------| |
| | arm64 | | x86_64 | |
| |-------| |--------| |
|------------------------------------|

The resulting XCFramework will not require the usage of the carthage copy-frameworks script anymore since the architectures are already properly sliced into the device and simulator slices.

Commands

Prepare the directory for creation of the various XCFramework slices.

Remove the architectures (using lipo -remove) that aren’t necessary for the device slice of the XCFramework.

Remove the architectures (using lipo -remove) that aren’t necessary for the simulator slice of the XCFramework.

Combine the two slices into an XCFramework using xcodebuild -create-xcframework.

Confirm that the debugging symbols can still be paired up to the original dSYM bundle using a similar approach as one of our previous posts.

--

--