Hi all,
I'm attempting to generate an XCFramework that must maintain ABI stability. The framework is created from an SPM using the following script:
#!/bin/bash
set -x
set -e
# Pass scheme name as the first argument to the script
NAME=$1
cd ..
# Build the scheme for all platforms that we plan to support
for PLATFORM in "iOS" "iOS Simulator"; do
echo "$PLATFORM"
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 \
-configuration Release \
-derivedDataPath ".build/$RELEASE_FOLDER" \
SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
FRAMEWORK_PATH="$ARCHIVE_PATH.xcarchive/Products/usr/local/lib/$NAME.framework"
MODULES_PATH="$FRAMEWORK_PATH/Modules"
HEADERS_PATH="$FRAMEWORK_PATH/Headers"
mkdir -p $MODULES_PATH
mkdir -p $HEADERS_PATH
BUILD_PRODUCTS_PATH=".build/$RELEASE_FOLDER/Build/Intermediates.noindex/ArchiveIntermediates/$NAME/BuildProductsPath"
INTERMEDIATE_BUILD_FILES_PATH=".build/$RELEASE_FOLDER/Build/Intermediates.noindex/ArchiveIntermediates/$NAME/IntermediateBuildFilesPath/$NAME.build/$RELEASE_FOLDER"
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
fi
# Copy ObjC Headers
if [ -d "$HEADERS_PATH" ]
then
for EXPORTED_MODULE in "ModuleA" "ModuleB" "ModuleC" "ModuleD"; do
cp -r "$INTERMEDIATE_BUILD_FILES_PATH/$EXPORTED_MODULE.build/$EXPORTED_MODULE.modulemap" $HEADERS_PATH
EXPORTED_HEADER="$INTERMEDIATE_BUILD_FILES_PATH/$EXPORTED_MODULE.build/Objects-normal/arm64/$EXPORTED_MODULE-Swift.h"
cp $EXPORTED_HEADER $HEADERS_PATH
HEADER_TO_MODIFY="$HEADERS_PATH/$EXPORTED_MODULE-Swift.h"
sed -i '' 's/@import MyFramework;/#import < MyFramework\/ModuleA-Swift.h>/' $HEADER_TO_MODIFY
sed -i '' 's/@import ModuleH;/#import < MyFramework\/ModuleH-Swift.h>/' $HEADER_TO_MODIFY
sed -i '' 's/@import ModuleC;/#import < MyFramework\/ModuleC-Swift.h>/' $HEADER_TO_MODIFY
sed -i '' 's/@import ModuleA;/#import < MyFramework\/ModuleA-Swift.h>/' $HEADER_TO_MODIFY
done
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
The issue arises when I remove the flag BUILD_LIBRARY_FOR_DISTRIBUTION=YES
and add the flag -allow-internal-distribution
to xcodebuild -create-xcframework
. While this resolves the problem, it results in the generated module not being ABI stable. However, when attempting this approach, it raises an error regarding arm64-apple-ios-private.swiftinterface
with no such file or module
as soon as it encounters an import statement for ModuleX.
The package structure is as follows:
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "MyFramework",
platforms: [
.iOS(.v17),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MyFramework", type: .dynamic,
targets: [
"MyFramework",
"ModuleA",
"ModuleB",
"ModuleC",
"ModuleD",
"ModuleE",
"ModuleF",
"WolfSSLC",
"ModuleG",
"ModuleH",
"ModuleI",
"ModuleJ",
"ModuleK",
"ModuleL",
"Common",
"Logging",
]
),
.library(name: "MyFrameworkDevelopment", type: .dynamic, targets: ["MyFrameworkDevelopment"]),
],
dependencies: [
.package(url: "https://…”)
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(name: "MyFrameworkDevelopment", dependencies: [
"MyFramework",
"MockServer",
]),
.target(name: "MyFramework", dependencies: [
"Logging",
"ModuleA",
"ModuleG",
"ModuleC",
"ModuleK",
"ModuleL",
"CoreBluetoothWrapper",
]),
.testTarget(name: "MyFrameworkTests", dependencies: [
"MyFramework",
"ModuleI",
"TestHelpers",
"MyFrameworkDevelopment",
]),
.testTarget(
name: "MyFrameworkAcceptanceTests",
dependencies: [
"MyFramework",
"ModuleI",
"MockServer",
"TestHelpers",
]
),
.target(
name: "ModuleA",
dependencies: [
"ModuleH",
"ModuleG",
"ModuleJ",
"ModuleK",
"ModuleL",
"Logging",
"ModuleB",
"ModuleI",
.product(name: "X509", package: "ISC_SDK_iOS_Swift_Certificates"),
]
),
.testTarget(
name: "ModuleATests",
dependencies: [
"ModuleA",
"TestHelpers",
"MockServer",
"WolfSSLC",
.product(name: "X509", package: "ISC_SDK_iOS_Swift_Certificates"),
]
),
.target(
name: "ModuleB",
dependencies: [
"ModuleI",
"ModuleH",
"ModuleG",
"Logging",
"Common",
]
),
.testTarget(
name: "ModuleBTests",
dependencies: [
"ModuleB",
"ModuleI",
"TestHelpers",
]
),
.target(
name: "ModuleC",
dependencies: [
"ModuleI",
"CoreBluetoothWrapper",
]
),
.testTarget(
name: "ModuleCTests",
dependencies: [
"ModuleC",
"ModuleI",
"TestHelpers",
]
),
.target(
name: "ModuleD",
dependencies: [
"ModuleI",
]
),
.testTarget(
name: "ModuleDTests",
dependencies: [
"ModuleD",
"ModuleI",
"TestHelpers",
]
),
.target(
name: "ModuleE",
dependencies: [
"ModuleD",
"Logging",
]
),
.testTarget(
name: "ModuleETests",
dependencies: [
"ModuleE",
"TestHelpers",
]
),
.target(
name: "ModuleF",
dependencies: [
"ModuleE",
"ModuleJ",
"ModuleL",
"Logging",
"WolfSSLC",
]
),
.testTarget(
name: "ModuleFTests",
dependencies: [
"ModuleF",
"TestHelpers",
]
),
.target(
name: "WolfSSLC",
dependencies: [
"libwolfssl",
]
),
.binaryTarget(
name: "libwolfssl",
path: "libwolfssl.xcframework"
),
.target(
name: "ModuleG",
dependencies: [
"ModuleE",
"ModuleF",
"Logging",
]
),
.testTarget(
name: "ModuleGTests",
dependencies: [
"ModuleG",
"TestHelpers",
]
),
.target(
name: "ModuleH",
dependencies: [
"Common",
]
),
.testTarget(
name: "ModuleHTests",
dependencies: ["ModuleH", "TestHelpers"]
),
.target(
name: "ModuleI",
dependencies: [
"CoreBluetoothWrapper",
"Common",
]
),
.target(
name: "CoreBluetoothWrapper"
),
.testTarget(
name: "ModuleITests",
dependencies: [
"ModuleI",
"TestHelpers",
]
),
.target(
name: "ModuleJ",
dependencies: [
"InstallationAuth",
"ModuleK",
"ModuleL",
.product(name: "X509", package: "ISC_SDK_iOS_Swift_Certificates"),
"Logging",
]
),
.testTarget(
name: "ModuleJTests",
dependencies: [
"ModuleJ",
"TestHelpers",
"ModuleK",
"ModuleL",
]
),
.target(
name: "InstallationAuth",
dependencies: [
"ModuleK",
"ModuleL",
]
),
.testTarget(
name: "InstallationAuthTests",
dependencies: [
"InstallationAuth",
"TestHelpers",
"ModuleK",
"ModuleL",
]
),
.target(
name: "ModuleK",
dependencies: ["Logging"]
),
.testTarget(
name: "ModuleKTests",
dependencies: [
"ModuleK",
"TestHelpers",
]
),
.target(name: "ModuleL"),
.testTarget(
name: "ModuleLTests",
dependencies: ["ModuleL"]
),
.target(name: "Logging"),
.target(name: "Common"),
.testTarget(
name: "CommonTests",
dependencies: [
"Common",
"TestHelpers",
]
),
.testTarget(
name: "LoggingTests",
dependencies: ["Logging"]
),
.target(
name: "MockServer",
dependencies: [
"ModuleL",
.product(name: "X509", package: "ISC_SDK_iOS_Swift_Certificates"),
],
path: "Tests/MockServer",
resources: [.process("Certs")]
),
.target(
name: "TestHelpers",
dependencies: [
"ModuleI",
"MyFramework",
"MockServer",
"ModuleF",
"WolfSSLC",
],
path: "Tests/TestHelpers",
resources: [.process("WolfSSL/Resources")]
),
]
)
The generated xcframework structure is as follows:
MyLibrary.xcframework
├── Info.plist
├── ios-arm64
│ └── MyLibrary.framework
│ ├── Headers
│ │ ├── ModuleH-Swift.h
│ │ ├── ModuleH.modulemap
│ │ ├── ModuleC-Swift.h
│ │ ├── ModuleC.modulemap
│ │ ├── ModuleA-Swift.h
│ │ ├── ModuleA.modulemap
│ │ ├── MyLibrary-Swift.h
│ │ └── MyLibrary.modulemap
│ ├── Info.plist
│ ├── Modules
│ │ └── MyLibrary.swiftmodule
│ │ ├── arm64-apple-ios.abi.json
│ │ ├── arm64-apple-ios.swiftdoc
│ │ └── arm64-apple-ios.swiftmodule
│ └── MyLibrary
└── ios-arm64_x86_64-simulator
└── MyLibrary.framework
├── Headers
│ ├── ModuleH-Swift.h
│ ├── ModuleH.modulemap
│ ├── ModuleC-Swift.h
│ ├── ModuleC.modulemap
│ ├── ModuleA-Swift.h
│ ├── ModuleA.modulemap
│ ├── MyLibrary-Swift.h
│ └── MyLibrary.modulemap
├── Info.plist
├── Modules
│ └── MyLibrary.swiftmodule
│ ├── arm64-apple-ios-simulator.abi.json
│ ├── arm64-apple-ios-simulator.swiftdoc
│ ├── arm64-apple-ios-simulator.swiftmodule
│ ├── x86_64-apple-ios-simulator.abi.json
│ ├── x86_64-apple-ios-simulator.swiftdoc
│ └── x86_64-apple-ios-simulator.swiftmodule
├── MyLibrary
└── _CodeSignature
└── CodeResources
It's worth mentioning that the library must be compatible with both Objective-C and Swift, and Modules A, C, and H are imported into the MyLibrary module as @_exported
modules, which is why I included the headers and module maps.
Thank you in advance for your assistance.