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?
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.
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
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...
@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.
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.
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?
@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?
/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)
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
@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 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?
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.