ソースコード非公開のライブラリを、SPMとXcFrameworkで配布する話
ソースコードを公開しないでライブラリを提供したいことはまれにあります。
このようなケースではiOSの場合、 XcFramework
を使ってソースコード非公開のライブラリのパッケージを配布することができます。
Appleの基本情報は下記のURLにあります。
- 開発者 Document:Distributing Binary Frameworks as Swift Packages
- WWDC 20 Video:Distribute binary frameworks as Swift packages
SPM
上記のAppleのドキュメントから引用すると、最終的に下記のようなPackage.swiftファイルを作成して配布することになります。
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "MyLibrary",
platforms: [
.macOS(.v10_14), .iOS(.v13), .tvOS(.v13)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "MyLibrary",
targets: ["MyLibrary", "SomeRemoteBinaryPackage", "SomeLocalBinaryPackage"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "MyLibrary"
),
.binaryTarget(
name: "SomeRemoteBinaryPackage",
url: "https://url/to/some/remote/xcframework.zip",
checksum: "The checksum of the ZIP archive that contains the XCFramework."
),
.binaryTarget(
name: "SomeLocalBinaryPackage",
path: "path/to/some.xcframework"
)
.testTarget(
name: "MyLibraryTests",
dependencies: ["MyLibrary"]),
]
)
products に、 library を記述し、 targets に binaryTarget として xcframework を配置するのが肝になります。
Xcode
プロジェクト
XcodeでXcFramework
を作成することができます。
メニューから「File」→「New」→「Project...」を選ぶと下記のようなダイアログが表示されますので、 Framework を選択します。
アプリのProjectを作成する手順と同じ感じで進むと、XcFramework
プロジェクトを作成することができます。
Build Settings
XcFramework
を作成するためには、Build Settings を変更します。(後述のBuild Scriptで指定できる記事も見かけましたが、試してみたところエラーになるようでした)
Build Libraries for Distribution という項目がありますので、ここを YESに変更します。
このオプションについては、Build Settings Reference に記載があります。
※ Build Settings Referenceより引用
これによると、
ライブラリが配布用にビルドされていることを確認します。Swiftでは、ライブラリの進化とモジュールインターフェースファイルの生成のサポートが可能になります。
(By DeepL) ということらしいです。
「ライブラリの進化(library evolution)」とは分かりにくいと言うか誤訳に近いと思いますが、つまり「バイナリ互換性」のことを言っています。ABI stabilityと言う表現の方がしっくりくる方も多いのではないでしょうか。Library Evolution in Swiftを読むと、まさにこのような記述されています。
※ Library Evolution in Swiftより引用
ソースコード
通常はこのあたりで 次項に記述する Build Script を走らせればXcFramework
が完成するはずなのですが、特定の依存ライブラリがある場合は問題が起こります。
例えば XcFramework
プロジェクト内で SwiftNIO を import しようとすると下記のようなwarningがでます。
warningなので、このままBuildできそうですが、実際にBuildして XcFramework
として使用しようとすると
Module 'Framework B' was not compiled with library evolution support; using it means binary compatibility forFramework B' can not be guaranteed`
と怒られます。うまく動作しません。
実はXcodeでは、これをfixする提案が表示されます。
この @_implementationOnly とはなんでしょうか?🤔
どうやらソースコードが有効な依存ライブラリがある場合に、ABI stabilityが得られないので library evolution がサポートされないようです。
これを解消するために、@_implementationOnly を指定して依存ライブラリを XcFramework
内部に隠蔽して閉じ込め、安定して動作させるもののようです。
[2022/11/8追記]
@_implementationOnlyを利用する場合、importしているclassやprotocolを使っているコードは public
にすることができません。
これは考えてみれば当たり前で、「依存ライブラリを内部に隠蔽して閉じ込め」ているので、public
で公開することはできないのです。
Build Script
Build Scriptは下記のようなものになります。
実機向けのBuildと、エミュレータ向けのBuildを行い、その後それらから XcFramework
を生成することで非常に使い勝手の良いバイナリーフレームワークを提供することができます。
#!/bin/sh
rm -r build
mkdir build
FRAMEWORK_NAME=HogeKit
PROJECT_NAME=HogeKit
DEVICE=device
SIMULATOR=simulator
DEVICE_FRAMEWORK=$DEVICE.xcarchive/Products/Library/Frameworks/$PROJECT_NAME.framework
SIMULATOR_FRAMEWORK=$SIMULATOR.xcarchive/Products/Library/Frameworks/$PROJECT_NAME.framework
# Build each XCFramework
xcodebuild archive -scheme $PROJECT_NAME -destination 'generic/platform=iOS' SKIP_INSTALL=NO -archivePath build/$DEVICE
xcodebuild archive -scheme $PROJECT_NAME -destination 'generic/platform=iOS Simulator' SKIP_INSTALL=NO -archivePath build/$SIMULATOR
# Combine XCFramework
xcodebuild -create-xcframework -framework build/$DEVICE_FRAMEWORK -framework build/$SIMULATOR_FRAMEWORK -output build/$FRAMEWORK_NAME.xcframework
名前空間
最後に名前空間の問題についてふれておきます。
XcFramework
のPackage名を HogeKit
として
import HogeKit
とする場合、例えば
enum HogeKit {
static func makeXXX() {
}
}
のような関数を用意することもあるかと思います。
しかし、これはなぜかうまく動作しません。
ライブラリ名と同一名称の enum / class などをモジュールのトップで定義すると、名前空間がうまく解決できずエラーになるようです。
これは、フルの関数名が、 HogeKit.HogeKit.makeXXX()
となるためのようです。
enum Hoge {
static func makeXXX() {
}
}
と定義すれば、 Hoge.makeXXX()
つまり、HogeKit.Hoge.makeXXX()
は呼び出せるようになります。