🍎

ソースコード非公開のライブラリを、SPMとXcFrameworkで配布する話

2022/09/20に公開

ソースコード非公開のライブラリを、SPMとXcFrameworkで配布する話

ソースコードを公開しないでライブラリを提供したいことはまれにあります。
このようなケースではiOSの場合、 XcFramework を使ってソースコード非公開のライブラリのパッケージを配布することができます。

Appleの基本情報は下記のURLにあります。

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 を記述し、 targetsbinaryTarget として 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 として使用しようとすると

text
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 内部に隠蔽して閉じ込め、安定して動作させるもののようです。

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() は呼び出せるようになります。

参考

Discussion