How to build Swift Package as XCFramework

As of Oct 2020, the only way to distribute Swift Packages in a binary format is to create an XCFramework and then wrap it in a Swift Package as a binary target. That is alright in itself, except there is no obvious way to create XCFramework from Swift Package source in the first place.

According to Apple Documentation, one should first build frameworks for different platforms using xcodebuild archive and then use xcodebuild -create-xcframework to wrap those frameworks into XCFramework. The problem is, this does not work with Swift Package schemes. The generated archive is not a framework and thus it is incompatible with xcodebuild -create-xcframework. Check this gist for the detailed example.

In an attempt to work around that problem, one might try using swift package generate-xcodeproj to generate Xcode project first and then follow the steps from the Apple Documentation to archive the frameworks and wrap them into XCFramework, however that fails hopelessly when a package contains resources and even crashes if a package has other binary dependencies. Generated Xcode project neither generates the resources accessor Bundle.module, nor does it create resource bundle. Binary dependencies fail gracefully - not.

Consequently, one will probably reach out to the community in hope of find a solution and stumble upon an amazing tool swift-create-xcframework, but only to find out that it is built on top of the same code as swift package generate-xcodeproj and thus not supporting resources and crashing on binary dependencies.

Finally, when hope is all but lost, I am reaching out here. Is there a solution to this problem? Has anybody found a way to build XCFrameworks from Swift Packages without maintaining parallel Xcode project files?

This should help as a starting point: https://github.com/apple/swift-package-manager/pull/2981#issuecomment-710282803

Please keep in mind that frameworks can only contain a single module, so if you are trying to ship a package product that contains multiple targets, this approach won't work.

1 Like

Thanks for the suggestion @NeoNacho, but it doesn't work, at least not with iOS as a destination. The output of the xcodebuild archive command is not a *.framework, just object file(s).

You script, modified for iOS, expects that xcodebuild archive creates tmp/iOS.xcarchive/Products/usr/local/lib/Emoji.framework, but that does not happen. What happens is that following is created: tmp/iOS.xcarchive/Products/Users/srdan/Objects/Emoji.o

NAME="Emoji"

mkdir -p tmp

xcodebuild archive -workspace $NAME.xcworkspace -scheme $NAME \
		-destination "generic/platform=iOS" \
		-archivePath "tmp/iOS" \
		SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES

MODULES_PATH="tmp/iOS.xcarchive/Products/usr/local/lib/$NAME.framework/Modules"
mkdir -p $MODULES_PATH

DERIVED_DATA="`ls -d $HOME/Library/Developer/Xcode/DerivedData/$NAME-*`"
BUILD_PRODUCTS_PATH="$DERIVED_DATA/Build/Intermediates.noindex/ArchiveIntermediates/$NAME/BuildProductsPath"

cp -a $BUILD_PRODUCTS_PATH/Release-iphoneos/$NAME.swiftmodule $MODULES_PATH

Alright, I think I found a solution. In order for xcodebuild to output a framework, Swift Package product has to be a dynamic library!

Here is a script that rewrites the Pacakge.swift file, builds the framework for different platforms and then wraps those into a single XCFramework.

#!/bin/bash

set -x
set -e

# Pass scheme name as the first argument to the script
NAME=$1

# Build the scheme for all platforms that we plan to support
for PLATFORM in "iOS" "iOS Simulator"; do

    case $PLATFORM in
    "iOS")
    RELEASE_FOLDER="Release-iphoneos"
    ;;
    "iOS Simulator")
    RELEASE_FOLDER="Release-iphonesimulator"
    ;;
    esac

    ARCHIVE_PATH=$RELEASE_FOLDER

    # Rewrite Package.swift so that it declaras dynamic libraries, since the approach does not work with static libraries
    perl -i -p0e 's/type: .static,//g' Package.swift
    perl -i -p0e 's/type: .dynamic,//g' Package.swift
    perl -i -p0e 's/(library[^,]*,)/$1 type: .dynamic,/g' Package.swift

    xcodebuild archive -workspace . -scheme $NAME \
            -destination "generic/platform=$PLATFORM" \
            -archivePath $ARCHIVE_PATH \
            -derivedDataPath ".build" \
            SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES

    FRAMEWORK_PATH="$ARCHIVE_PATH.xcarchive/Products/usr/local/lib/$NAME.framework"
    MODULES_PATH="$FRAMEWORK_PATH/Modules"
    mkdir -p $MODULES_PATH

    BUILD_PRODUCTS_PATH=".build/Build/Intermediates.noindex/ArchiveIntermediates/$NAME/BuildProductsPath"
    RELEASE_PATH="$BUILD_PRODUCTS_PATH/$RELEASE_FOLDER"
    SWIFT_MODULE_PATH="$RELEASE_PATH/$NAME.swiftmodule"
    RESOURCES_BUNDLE_PATH="$RELEASE_PATH/${NAME}_${NAME}.bundle"

    # Copy Swift modules
    if [ -d $SWIFT_MODULE_PATH ] 
    then
        cp -r $SWIFT_MODULE_PATH $MODULES_PATH
    else
        # In case there are no modules, assume C/ObjC library and create module map
        echo "module $NAME { export * }" > $MODULES_PATH/module.modulemap
        # TODO: Copy headers
    fi

    # Copy resources bundle, if exists 
    if [ -e $RESOURCES_BUNDLE_PATH ] 
    then
        cp -r $RESOURCES_BUNDLE_PATH $FRAMEWORK_PATH
    fi

done

xcodebuild -create-xcframework \
-framework Release-iphoneos.xcarchive/Products/usr/local/lib/$NAME.framework \
-framework Release-iphonesimulator.xcarchive/Products/usr/local/lib/$NAME.framework \
-output $NAME.xcframework

It would be great if it were possible to do this without rewriting the package file, but I couldn't find a way to make xcodebuild treat package libraries as dynamic libraries.

Finally, I need to figure out a way to build the whole dependency graph. If only xcodebuild could build subdependencies...

1 Like
Terms of Service

Privacy Policy

Cookie Policy