I’m looking into building a closed source XCFramework from a local Swift package that has dependencies on other packages, which can later be distributed via Swift Package Manager. In initial discussions, we thought xcodebuild does not support linking the dependencies externally, it always includes them statically in the built framework. It's my understanding this is because we're asking xcodebuild to build a framework from a local Swift Package. Is there another way this can be achieved?
To explain in more detail: I have built a closed source SDK for other developers to integrate in their apps, currently distributed as an XCFramework. The interesting thing about the SDK is it has dependencies on other libraries, which need to be resolved when adding this SDK as a dependency to an app. The SDK’s dependencies should not be baked into our XCFramework. CocoaPods has worked well for that but we want to instead use SPM.
The current project setup is an iOS framework Xcode project and an app Xcode workspace. The framework project is included in the app workspace and is in the same repo as the app, which allows me to modify the framework source code then run the app to test it. The framework project can also be opened independently and built to verify it doesn’t have any errors, but to verify it’s working I run it with the app. To distribute a new release I use xcodebuild to create an XCFramework and then deploy that. For this to work with CocoaPods I had to add a Podfile to the app directly as well as the framework directory so both have the dependencies available. This means I have an xcworkspace for the framework and not just a xcodeproj. I specify the framework workspace file in the xcodebuild command.
To switch to a setup that utilizes Swift Package Manager, I created a Package.swift in the iOS framework project’s directory that specifies its dependencies, removed CocoaPods integration including deleting the workspace file, removed the framework project from the app’s workspace, added the Package as a local package to the app project, and added the framework directory via + > Add Files to “App” which adds the package to the top of the sidebar, making its source code available to edit within the app workspace. Everything is working when I run the app. Xcode properly resolves the dependencies for the local package and I can run the app to develop it.
Now to create an XCFramework I run the following command in the framework directory (which contains the Package.swift):
xcodebuild archive -workspace . -scheme FrameworkName -configuration Release -destination 'generic/platform=iOS' -archivePath './build/FrameworkName.framework-iphoneos.xcarchive' SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES ENABLE_USER_SCRIPT_SANDBOXING=NO
This succeeds however the dependencies have been linked statically thus included in our SDK. We need to only include the code from our framework and link to external dependencies, like it does with our current CocoaPods setup.
I'm wondering what options there are to achieve this. Even if I need to change the project setup locally, for example to continue using a framework project/workspace instead of a local Swift package. It seems I just need xcodebuild to be able to create an XCFramework which can then be distributed with its own Package.swift file that specifies its dependencies.
If it's not possible to link the dependencies externally, could you help me to understand the implications of including them statically? I don't know what problems could arise as a result of that or other concerns this would bring. Thanks!
To get Swift Package Manager to produce a framework for you, you need to mark your library as dynamic in your Package.swift file. Put type: .dynamic
in your arguments when defining the library product.
Then you can follow the steps to create the XCFramework as documented here: https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle
Note that you will need to manually copy your Swift interface files (.swiftmodule files) into the framework after it's archived. This is due to archives generally only containing runtime artifacts, but in this case you'd like to distribute the framework for other developers to build with.
Here is an example script that does this extra step. You will need to do this for each platform that you include in your XCFramework:
xcodebuild archive -workspace Emoji -scheme Emoji \
-destination "generic/platform=iOS" \
-archivePath "tmp/iOS" \
SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
MODULES_PATH="tmp/iOS.xcarchive/Products/usr/local/lib/Emoji.framework/Modules"
mkdir -p $MODULES_PATH
cp -a $BUILD_PRODUCTS_PATH/Release-iphoneos/Emoji.swiftmodule $MODULES_PATH