Recipe For Building a Closed Source Swift Framework With Dependencies

Aaron Sutula
Textile
Published in
4 min readSep 7, 2017

ABI compatibility issues aside, sometimes you may want to distribute a binary version of your Swift framework. This framework should be a ‘fat’ framework (supporting both simulator and iOS device architectures), and may depend on other third party frameworks. There is no Xcode project template for this, but you’ll find helpful information on the Internet. It basically comes down to using xcodebuild to create two versions of your framework, one for the simulator and one for real iOS devices, and then using the lipo command to combine them into a single fat framework.

It seems pretty straight forward, but after following instructions found online (examples here, here, and here), we experienced runtime crashes when using the binary version of our framework in an app. The crash would happen when our framework, called MyFramework in the following example, would try to use a symbol from any third party framework dependency. The crash would look like this when MyFramework tried to use RxSwift's ObservableType:

dyld: lazy symbol binding failed: Symbol not found: __TIFE7RxSwiftPS_14ObservableType9subscribeFT6onNextGSqFwx1ET__7onErrorGSqFPs5Error_T__11onCompletedGSqFT_T__10onDisposedGSqFT_T___PS_10Disposable_A0_Referenced from: /Users/aaron/Library/Developer/CoreSimulator/Devices/335F6B8A-0CBC-406D-9559–5718D4910F5A/data/Containers/Bundle/Application/47AA11E3-C2E5–4FCB-A86C-36092990EF0E/Consumer.app/Frameworks/MyFramework.framework/MyFrameworkExpected in: /Users/aaron/Library/Developer/CoreSimulator/Devices/335F6B8A-0CBC-406D-9559–5718D4910F5A/data/Containers/Bundle/Application/47AA11E3-C2E5–4FCB-A86C-36092990EF0E/Consumer.app/Frameworks/RxSwift.framework/RxSwift

After lots of investigation and experimention, we found a process that produces a fat framework that is crash-free when referencing symbols from other third party frameworks. It’s one of those things where I can’t totally explain what the problem was or why this works, but I wanted to share it here:

Create your framework

Use the Cocoa Touch Framework Xcode template, and feel free to depend on other frameworks however you choose (Cocoapods, Carthage, drag n’ drop, etc).

Create a target to build the fat framework

Use the Aggregate target type in the Cross-platform section. This creates a minimal target we can add a single run script build phase to. I’ll call this target BuildFatFramework.

Add the fat framework building shell script to the target

Add a run script build phase to the BuildFatFramework target, and add the following shell script to it:

# 1
# Set bash script to exit immediately if any commands fail.
set -e# 2
# Setup some constants for use later on.
FRAMEWORK_NAME="MyFramework"
OUTPUT_DIR="${SRCROOT}/build"
# 3
# If remnants from a previous build exist, delete them.
if [ -d "${OUTPUT_DIR}" ]; then
rm -rf "${OUTPUT_DIR}"
fi
# 4
# Build the framework for device and for simulator (using
# all needed architectures).
xcodebuild -workspace "${FRAMEWORK_NAME}.xcworkspace" -scheme "${FRAMEWORK_NAME}" -configuration Release -arch arm64 -arch armv7 -arch armv7s only_active_arch=no defines_module=yes -sdk "iphoneos" -derivedDataPath "${OUTPUT_DIR}"xcodebuild -workspace "${FRAMEWORK_NAME}.xcworkspace" -scheme "${FRAMEWORK_NAME}" -configuration Release -arch x86_64 -arch i386 only_active_arch=no defines_module=yes -sdk "iphonesimulator" -derivedDataPath "${OUTPUT_DIR}"# 5
# Remove .framework file if exists from previous run.
if [ -d "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework" ]; then
rm -rf "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework"
fi
# 6
# Copy the device version of framework.
cp -r "${OUTPUT_DIR}/Build/Products/Release-iphoneos/${FRAMEWORK_NAME}.framework" "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework"# 7
# Replace the framework executable within the framework with
# a new version created by merging the device and simulator
# frameworks' executables with lipo.
lipo -create -output "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${OUTPUT_DIR}/Build/Products/Release-iphoneos/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${OUTPUT_DIR}/Build/Products/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}"# 8
# Copy the Swift module mappings for the simulator into the
# framework. The device mappings already exist from step 6.
cp -r "${OUTPUT_DIR}/Build/Products/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule/" "${OUTPUT_DIR}/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule"

Customize this script as needed to fit your project.

Initiate the build from the BuildFatFramework target

Now you can build your fat framework by running the BuildFatFramework target of your Xcode project.

Very important: If buiding from the command line, be sure to use xcodebuild, specifying BuildFatFramework as the scheme and build as the action (not archive which is the default if you don’t specify an action). This was the key step that led to our crash-free framwork.

The archive action is not correct, and additionaly, it seems like Xcode is doing something to properly set up the build environment when you build the BuildFatFramework scheme. Saving the contents of the shell script that builds the fat framework to a stand alone file, and running it directly, bypassing the BuildFatFramework scheme, resulted in a framework that crashed.

Here is the command we use in our CI system to initiate the building of the fat framework:

xcodebuild -scheme BuildFatFramework -workspace MyFramework.xcworkspace build

Hopefully Xcode will provide better support for building fat frameworks in the future, but until then, I hope this information can be useful to someone out there.

--

--