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?

13 Likes

This should help as a starting point: Add option to define custom scheme for generated xcode project by PycKamil Ā· Pull Request #2981 Ā· apple/swift-package-manager Ā· GitHub

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.

3 Likes

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
1 Like

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...

11 Likes

@Srdan_Rasic Do you know how/where to copy the headers in case of C/ObjC library after creating the archive?
I am referring to your # TODO: Copy headers in the script file.

Have you found a way to build the whole dependency graph by any chance?

1 Like

I find all the #import statements in my umbrella header and parse out the file name, find it in the package directory tree, and then copy it to the headers dir in the framework (had to create the headers dir too).

This could probably be more succinct, but I am no bash expert.

perl -lne 'print $1 if /\<'${NAME}'\/(\S+.h)/' $NAME/$NAME.h | \
    xargs -I {} find . -name "{}" -print | \
    xargs -I {} cp {} $HEADERS_PATH/.
cp $NAME/$NAME.h $HEADERS_PATH/.

Has anyone figured out how to distribute a binary to Linux folks? An XCFramework is only good for my iOS, macOS, tvOS, etc users, how can I distribute via SPM to my linux users via a binary?

Swift on Linux is not ABI stable and thus distributing libraries as binaries isnā€™t going to work reliably.

Share and Enjoy

Quinn ā€œThe Eskimo!ā€ @ DTS @ Apple

2 Likes

@NeoNacho would you have an advice on how to build all dependencies in a dependency graph into XCFrameworks?

The script that I posted above can build a single dependency, however all of its dependencies get statically linked, so building those dependencies individually makes no sense. Is there a way to disable that static linking?

1 Like

@Srdan_Rasic Thanks for creating the script that outputs a framework from a Swift Package.

When I run the script I'm getting an error during the signing step.
I attached the output below. Would you know how to fix this?

Thanks.

Signing Identity: "-"

/usr/bin/codesign --force --sign - /Users/user/Documents/etudes/MyProject/.build/Build/Intermediates.noindex/ArchiveIntermediates/MyProject/IntermediateBuildFilesPath/UninstalledProducts/iphonesimulator/MyProject_MyProject.bundle

/Users/user/Documents/etudes/MyProject/.build/Build/Intermediates.noindex/ArchiveIntermediates/MyProject/IntermediateBuildFilesPath/UninstalledProducts/iphonesimulator/MyProject_MyProject.bundle: bundle format unrecognized, invalid, or unsuitable
Command CodeSign failed with a nonzero exit code

** ARCHIVE FAILED **

The following build commands failed:
CompileSwift normal x86_64
CompileSwift normal i386
CompileSwift normal arm64
CompileSwift normal x86_64
CompileSwift normal arm64
CodeSign /Users/user/Documents/etudes/MyProject/.build/Build/Intermediates.noindex/ArchiveIntermediates/MyProject/IntermediateBuildFilesPath/UninstalledProducts/iphonesimulator/MyProject_MyProject.bundle
(6 failures)

Does the project compile in Xcode? Maybe check your code signing identities have been set in Xcode.

I managed to create the XC framework now. However when I import it into a client project, I get the following error:

Failed to build module 'MyProject' from its module interface; it may have been damaged or it may have triggered a bug in the Swift compiler when it was produced

Any advice on how to fix this issue?

Thanks.

@Alfredo that is a very broad question so it's really hard to answer! I would recommend trying to dig deeper though and post an update with what you tried. One reason could be to see whether you have old Objective-C runtime support (see ios - Admob Framework class not detected by Xcode interface builder - Stack Overflow for a similar XCFramework ObjC runtime issue) in the framework and check that your access levels are valid. Have you followed the recommended steps to create the XCFramework?

@LemonCupcake Let me clarify my scenario. I have a Swift Package project which contains 9 Swift packages. This project has only one scheme and this is the name provided to the script written by @Srdan_Rasic as input.
There is no Objective-C code in this setup at all, and no external dependencies either.
The script runs successfully and produces an XC framework.
If I create a brand new project in Xcode, and include this XC framework as a dependency, I get the error below when I add the line to import the framework.
Please let me know if additional details are required.

Thanks.

Failed to build module 'MyProject' from its module interface; it may have been damaged or it may have triggered a bug in the Swift compiler when it was produced

@Alfredo could you please check the contents of x.swiftinterface inside the frameworks x.swiftmodule folder? Do you only import Swift here?

Also, what exactly do you mean by "9 Swift packages" and "no external dependencies"? Do you mean the .dependencies modifier passes an empty array?

@Alfredo One solution seems to add @_implementationOnly import for the modules from the third-party SDKs. This should fix your XCframework error if you aren't using anything from that SDK as part of your module's public interface.
source

Hello, This solution here suggests that xcodeproj or xcodeworkspace exists. However, ideally, they don't. We have package specification in package manifest.
So when I do this:
xcodebuild archive
-scheme MyLibrary
-destination "generic/platform=iOS"
-archivePath "archives/MyScheme-iOS"
-allowProvisioningUpdates
SKIP_INSTALL=NO
BUILD_LIBRARY_FOR_DISTRIBUTION=YES

It indeed archives the project, but no resources are in framework, no swiftmodule files. How can I build a package, that is not backed up by xcodeproj or workspace into working framework?

Specify the package directory as a workspace. If building from the package directory, specify -workspace .

xcodebuild archive 
-workspace .
-scheme MyLibrary 
-destination "generic/platform=iOS" 
-archivePath "archives/MyScheme-iOS" 
-allowProvisioningUpdates 
SKIP_INSTALL=NO 
BUILD_LIBRARY_FOR_DISTRIBUTION=YES

Adding -workspace did not help. Still no resources copied to framework and no swiftmodule files. No difference with -workspace . or without it.
It seems so that I will have to create xcodeproj, put all files there as well. This is a bit unfortunate.
It seems that SPM is only supporting delivery of already compiled xcframeworks, but is not helping much to create them.