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

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

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

Appleの基本情報は下記のURLにあります。
- 開発者 Document：[Distributing Binary Frameworks as Swift Packages](https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages)
- WWDC 20 Video：[Distribute binary frameworks as Swift packages](https://developer.apple.com/videos/play/wwdc2020/10147)

## SPM

上記のAppleのドキュメントから引用すると、最終的に下記のようなPackage.swiftファイルを作成して配布することになります。

```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** を選択します。


![](https://storage.googleapis.com/zenn-user-upload/60aa08820ea6-20220920.png)

アプリのProjectを作成する手順と同じ感じで進むと、`XcFramework`プロジェクトを作成することができます。

![](https://storage.googleapis.com/zenn-user-upload/5dde34f829ca-20220920.png)

### Build Settings

`XcFramework`を作成するためには、**Build Settings** を変更します。（後述のBuild Scriptで指定できる記事も見かけましたが、試してみたところエラーになるようでした）
**Build Libraries for Distribution** という項目がありますので、ここを **YES**に変更します。

![](https://storage.googleapis.com/zenn-user-upload/d5ea5baa6286-20220920.png)

このオプションについては、[Build Settings Reference](https://developer.apple.com/documentation/xcode/build-settings-reference/) に記載があります。

![](https://storage.googleapis.com/zenn-user-upload/cab995651438-20220920.png)

※ [Build Settings Reference](https://developer.apple.com/documentation/xcode/build-settings-reference/)より引用

これによると、

> ライブラリが配布用にビルドされていることを確認します。Swiftでは、ライブラリの進化とモジュールインターフェースファイルの生成のサポートが可能になります。

(By [DeepL](https://www.deepl.com/ja/translator)) ということらしいです。

「**ライブラリの進化(library evolution)**」とは分かりにくいと言うか誤訳に近いと思いますが、つまり「**バイナリ互換性**」のことを言っています。ABI stabilityと言う表現の方がしっくりくる方も多いのではないでしょうか。[Library Evolution in Swift](https://www.swift.org/blog/library-evolution/)を読むと、まさにこのような記述されています。

![](https://storage.googleapis.com/zenn-user-upload/5540594b2d6a-20220920.png)

※ [Library Evolution in Swift](https://www.swift.org/blog/library-evolution/)より引用

### ソースコード

通常はこのあたりで 次項に記述する **Build Script** を走らせれば`XcFramework`が完成するはずなのですが、特定の依存ライブラリがある場合は問題が起こります。

例えば `XcFramework` プロジェクト内で [SwiftNIO](https://github.com/apple/swift-nio) を import しようとすると下記のようなwarningがでます。

![](https://storage.googleapis.com/zenn-user-upload/67c1dfe283c2-20220920.png)

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する提案が表示されます。

![](https://storage.googleapis.com/zenn-user-upload/23e511ce225b-20220920.png)

この **@_implementationOnly** とはなんでしょうか？🤔

どうやらソースコードが有効な依存ライブラリがある場合に、ABI stabilityが得られないので **library evolution** がサポートされないようです。
これを解消するために、**@_implementationOnly** を指定して依存ライブラリを `XcFramework` 内部に隠蔽して閉じ込め、安定して動作させるもののようです。

[2022/11/8追記]

@_implementationOnlyを利用する場合、importしているclassやprotocolを使っているコードは `public` にすることができません。
これは考えてみれば当たり前で、「依存ライブラリを内部に隠蔽して閉じ込め」ているので、`public` で公開することはできないのです。

## Build Script

Build Scriptは下記のようなものになります。
実機向けのBuildと、エミュレータ向けのBuildを行い、その後それらから `XcFramework` を生成することで非常に使い勝手の良いバイナリーフレームワークを提供することができます。

```bash
#!/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` として

```swift
import HogeKit
```

とする場合、例えば

```swift
enum HogeKit {
    static func makeXXX() {

    }
}
```

のような関数を用意することもあるかと思います。
しかし、これはなぜかうまく動作しません。

ライブラリ名と同一名称の enum / class などをモジュールのトップで定義すると、名前空間がうまく解決できずエラーになるようです。
これは、フルの関数名が、 `HogeKit.HogeKit.makeXXX()` となるためのようです。

```swift
enum Hoge {
    static func makeXXX() {

    }
}
```

と定義すれば、 `Hoge.makeXXX()` つまり、`HogeKit.Hoge.makeXXX()` は呼び出せるようになります。

## 参考

- [[Apple Developer] Distributing Binary Frameworks as Swift Packages](https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages)
- [[WWDC 20] Distribute binary frameworks as Swift packages](https://developer.apple.com/videos/play/wwdc2020/10147)
- [[forums.swift.org] Update on implementation-only imports](https://forums.swift.org/t/update-on-implementation-only-imports/26996) 
- [[forums.swift.org] How to enable library evolution support for swift package](https://forums.swift.org/t/how-to-enable-library-evolution-support-for-swift-package/47191)