How to build a Fat Framework that Includes Mac Catalyst Support

With Apple's release of Xcode 11, app developers can now build their iPad apps for Mac by using Mac Catalyst. Now, instead of building a native Mac app from scratch, developers can simply build their existing iPad apps as native Mac applications.

If you're an SDK developer, you might be trying to figure out how to build your framework so that it supports Mac Catalyst in addition to other supported architectures while remaining backwards compatible. You want your SDK's users to still be able to build their projects even if they're on older versions of Xcode. If this is the case, building your framework using Apple's newly introduced XCFramework won't be enough. To achieve this, you will need to build each architecture using xcodebuild individually and then build them into a single fat binary using lipo.

Update 07/24/20: if your app uses any Swift at all, the following approach won't work since there is no Swift x86_64h runtime.

Download the oldest supported version of Xcode you wish to support

As of November 2019, Apple still supports Xcode 10. Thus, you may want to continue supporting it until it gets dropped. Since Mac Catalyst is only on Xcode 11, you will want to build for any other sdk targets with Xcode 10 (e.g: "iphonesimulator", "iphoneos", etc...). Download the old Xcode version and rename it to include the version such as: Xcode10.1

Also rename your most current version (e.g: Xcode11.0)

Create or Modify a Build Script

For the building step of a static framework, a common way to do this is to use "Run Script" which can be found in the "Build Phases" tab of your static framework target. e.g:

With the following strategy, you should be able to run the script entirely from the command-line (no longer needing the extra target in Xcode or the "Run Script" at all).

If you don't have a build script yet, create one in your ${SRCROOT} directory. This is the directory containing the Xcode project itself.

touch build_fat_framework.sh	

Start with the below code. Notice here that:

  • we are building the framework as a static library ( staticlib )
  • we are building the framework with the configuration set to Debug to expose potential debug symbols
  • you should replace the BUILD_SCHEME and BUILD_PROJECT with the appropriate values from your project
#!/bin/bash
set -e

WORKING_DIR=$(pwd)
DERIVED_DATA_RELATIVE_DIR=temp
BUILD_CONFIG="Debug" 
BUILD_TYPE="staticlib"
BUILD_SCHEME="MyFramework"
BUILD_PROJECT="MyFrameworkProject.xcodeproj"

Next, we define the two specific versions of Xcode that we will need. Notice here that we are specifying each respective Xcode application in the two paths.

# NOTE: Once Apple drops support for Xcode 10, we can edit this to use same xcodebuild version for all three build commands

XCODEBUILD_OLDEST_SUPPORTED=/Applications/Xcode10.1.app/Contents/Developer/usr/bin/xcodebuild
XCODEBUILD_11_0=/Applications/Xcode11.0.app/Contents/Developer/usr/bin/xcodebuild

Finally, build the frameworks as follows. Note that:

  • the architecture for Catalyst is x86_64h
  • SYMROOT specifies where the framework will be outputted. Here, it is an automatically generated directory called temp
# For backwards compatible bitcode we need to build iphonesimulator + iphoneos with 3 versions behind the latest.
#       However variant=Mac Catalyst needs to be be Xcode 11.0

$XCODEBUILD_OLDEST_SUPPORTED -configuration ${BUILD_CONFIG} MACH_O_TYPE=${BUILD_TYPE} -sdk "iphonesimulator" ARCHS="x86_64" -project ${BUILD_PROJECT} -scheme ${BUILD_SCHEME} SYMROOT="${DERIVED_DATA_RELATIVE_DIR}/"
$XCODEBUILD_OLDEST_SUPPORTED -configuration ${BUILD_CONFIG} MACH_O_TYPE=${BUILD_TYPE} -sdk "iphoneos" ARCHS="armv7 armv7s arm64 arm64e i386"  -project ${BUILD_PROJECT} -scheme ${BUILD_SCHEME} SYMROOT="${DERIVED_DATA_RELATIVE_DIR}/"
$XCODEBUILD_11_0 -configuration ${BUILD_CONFIG} ARCHS="x86_64h" -destination 'platform=macOS,variant=Mac Catalyst' MACH_O_TYPE=${BUILD_TYPE} -project ${BUILD_PROJECT} -scheme ${BUILD_SCHEME} SYMROOT="${DERIVED_DATA_RELATIVE_DIR}/"

Once each framework is built, we need to combine them into a single "fat" framework. We define the paths for each output framework:

USER=$(id -un)
DERIVED_DATA_DIR="${WORKING_DIR}/${DERIVED_DATA_RELATIVE_DIR}"

# Use Debug configuration to expose symbols
CATALYST_DIR="${DERIVED_DATA_DIR}/Debug-maccatalyst"
SIMULATOR_DIR="${DERIVED_DATA_DIR}/Debug-iphonesimulator"
IPHONE_DIR="${DERIVED_DATA_DIR}/Debug-iphoneos"

CATALYST_OUTPUT_DIR=${CATALYST_DIR}/MyFramework.framework
SIMULATOR_OUTPUT_DIR=${SIMULATOR_DIR}/MyFramework.framework
IPHONE_OUTPUT_DIR=${IPHONE_DIR}/MyFramework.framework

Next, we define the path for our final framework and create a universal directory:

UNIVERSAL_DIR=${DERIVED_DATA_DIR}/Debug-universal

rm -rf "${UNIVERSAL_DIR}"
mkdir "${UNIVERSAL_DIR}"

Generate the fat binary using lipo.

echo "> Making Final MyFramework with all Architectures. iOS, iOS Simulator(x86_64), Mac Catalyst(x86_64h)"
lipo -create -output "$UNIVERSAL_DIR"/"${BUILD_SCHEME}" "${IPHONE_OUTPUT_DIR}"/"${BUILD_SCHEME}" "${SIMULATOR_OUTPUT_DIR}"/"${BUILD_SCHEME}" "${CATALYST_OUTPUT_DIR}"/"${BUILD_SCHEME}"

At this point, you may want to automatically generate the framework structure and put your fat binary into it. You can see how we did this here. Otherwise, your fat binary is finished and can be found in the Debug-universal directory. Enjoy building your fat frameworks!