Swift Package Managerで、Private RepositoryのRelease AssetにアップしたXCFrameworkをbinaryTargetに指定して利用する

Swift Package Manager(以下、SPM)も登場しある程度の年数が経ち、実務でも導入例が増えてきているのではないでしょうか？
しかし、有名どころのFrameworkであってもSPM非対応なものも多く残っているのが現状です。

# SPMで、XCFrameworkを活用する

SPMは、XCFramework形式のバイナリビルドに依存することが可能なので、自前のPackageの依存対象にBinary targetとして、SPM非対応FrameworkのXCFramework形式のバイナリビルドを登録することで、擬似的にSPM上で管理することが可能になります。

XCFrameworkの指定は、ローカル参照 or サーバーからバイナリーをホスティングする2択が選べます。
サーバーからバイナリをホスティングする場合は、XCFrameworkを含むZipファイルを作成し公開する必要があります。
Package.swiftは下記のようになります。

```Package.swift
import PackageDescription

let package = Package(
    name: "AppPackage",
    products: [
        .library(
            name: "HogeUtil",
            targets: ["HogeUtil"]),
    ],
    targets: [
        .binaryTarget(
            name: "HogeUtil",
            url: "https://hogehogefugafuga.com/HogeUtil.xcframework.zip",
            checksum: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        ),
    ]
)
```

非公開のFrameworkの場合はGit-lfsやAWS S3など特定の環境からアクセスできる場所にバイナリを配置して、取得するようにする必要があります

## Private repositoryのRelease assetは、従来使えなかった

SPM未対応の非公開のFrameworkをもしGithubでリポジトリ管理しているなら、Release assetにXCFrameworkのZipアーカイブをアップして、アップしたファイルのDonwload urlを活用出来たら自前でホスティングする必要もなくなるので楽じゃないか！と考えるかもしれません。
しかし、Githubの認証周りが上手くできずPrivate repositoryのRelease Assetを活用したbinaryTargetの指定は出来ませんでした。

https://forums.swift.org/t/spm-binary-dependency-in-private-github-release/52514


フォーラムでも上記の内容が上がっており、手元で実際に試してもCheckout時に404エラーとなり、認証ができずSPM経由でバイナリーを落とすことが出来ませんでした

### 先駆者たちはこうやっている

https://qiita.com/mesummery/items/a5f754d3b6d1e107fffb

こちらの記事では、リポジトリ本体にXCFrameworkに組み込んで配布することを提唱しています。
上記に倣うのであれば、Private RepositoryとしてXCFrameworkを依存させた配布用パッケージを用意し、同リポジトリにXCFrameworkのバイナリを含めることで実現できそうです。
ただ、100MB以上のファイルはGithubに置くことができないため、Frameworkのビルド後のバイナリサイズが小さい前提になってしまいます。

# なぜPrivate repositoryのRelease assetを利用できなかったのか？

大きく2つの問題がありました。

### ダウンロードURLがそのままでは利用できない

Release assetにアップされたファイルのURLをそのままhttpリクエストするとステータスコード302となります。
browser download urlは、zipそのものがホスティングされたURLではなく、asset urlへのリダイレクトが裏で行われているようです。
SPMのURLSessionでは200のみを採用していたため、リダイレクトするURLの場合は、リダイレクト先のリソースの取得が出来ていませんでした。
また、SPMではbinaryTargetのURLはzipの拡張子が含まれている必要があります。
zip拡張子がない場合は、``unsupported extension for binary target 'TestBinaryTarget'; valid extensions are: 'zip'``となります

https://forums.swift.org/t/spm-support-basic-auth-for-non-git-binary-dependency-hosts/37878/58

これらの問題があったため、Release assetのURLをそのまま利用することが出来ませんでした。

### Accept: application/octet-streamヘッダーが追加されていなかった
Github APIの仕様に則ったヘッダーがSPM経由でバイナリーダウンロードを行おうとした際に、リクエストに付与されていなかった問題がありました。
現在は、下記PRの対応によってリクエストに付与されるようになっています。

https://github.com/apple/swift-package-manager/pull/3795

### Xcode13.3以降で問題が解決していた

以前、実現できなかった経験があり、現在も出来ないものだと思っていました。
実は、Xcode13.3以降で実現できるように修正されていたようです。

# Private repositoryのRelease assetを活用する

SPM対応済みのFrameworkを使うよりは、手間がかかります。
バイナリーを活用したビルド高速化などの恩恵もあるので、ケースによっては有用になるかもしれません。

## 前提条件

- 特定のFrameworkがGithub上で、リポジトリ管理されている
- 同リポジトリのTag, Releaseが発行されている
- 最新のRelease assetにXCFrameworkのZipアーカイブがアップされている

上記の条件まで進んでいる状態とします。

## 最新のリリースを取得する

Github APIを用いて、利用したいFrameworkの最新のReleaseを取得します

https://docs.github.com/ja/rest/releases/releases#get-the-latest-release

```shell:curl
curl \
  -H "Accept: application/vnd.github+json" \ 
  -H "Authorization: Bearer <YOUR-TOKEN>" \
  https://api.github.com/repos/OWNER/REPO/releases/latest
```

```json:response
{
  "url": "https://api.github.com/repos/<OWNER>/<REPO NAME>/releases/<RELEASE ID>",
  ~~~ 中略 ~~~
  "assets": [
    {
      "url": "https://api.github.com/repos/<OWNER>/<REPO NAME>/releases/assets/<ASSET ID>",
      "name": "<FRAMEWORK NAME>.xcframework.zip",
      ~~~ 中略 ~~~
    }
  ]
}
```

レスポンスの中に含まれるアセットの情報のうち、利用したいXCFrameworkの``url``を記録しておきます。

## XCFrameworkのZipアーカイブのChecksumを取得する

Release assetにアップされている``<FRAMEWORK NAME>.xcframework.zip``をダウンロードします。
ダウンロード後、ZipアーカイブのChecksumを取得するために下記コマンドを実行します。

```shell
swift package compute-checksum <FRAMEWORK NAME>.xcframework.zip
```
出力したchecksumは記録しておきます。

## Package.swiftにbinary targetを追加する

下記のように``binaryTarget``を追加します。

```Package.swift
import PackageDescription

let package = Package(
    name: "AppPackage",
    products: [
        .library(
            name: "HogeUtil",
            targets: ["HogeUtil"]),
    ],
    targets: [
        .binaryTarget(
            name: "HogeUtil",
            // 末尾にZipの拡張子を付与 
            url: "https://api.github.com/repos/<OWNER>/<REPO NAME>/releases/assets/<ASSET ID>.zip",
            checksum: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        ),
    ]
)
```

- アセットのURLの末尾に``.zip``を付与したURLを``binaryTarget``のURLに指定
- 取得済みの``checksum``の値をchecksumに指定する

## .netrcの作成

SPMは、``.netrc``ファイルによる接続設定がサポートされています。

https://github.com/apple/swift-package-manager/pull/2833

SPM経由で、private repositoryへアクセスするために下記のように``.netrc``ファイルを作成します。
Githubのアクセストークンを記載するので、別途アクセストークンを用意しておきましょう。

https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token

```shell: ない場合は作成
$ touch ~/.netrc
```

```.netrc
machine api.github.com login <GITHUB USER NAME> password <ACCESS Token>
```

## あとはよしなに...

以上の設定後、Private RepositoryのRelease assetにアップしたXCFrameworkを取得し、SPM経由で利用することが可能になります。
手間はかかりますが、Private Repositoryで完結することができるようになったので、機密性の高いFrameworkであってもSPMで扱いやすくなったと思います。

# 今後の検証課題

- CI上での認証情報設定
    - ``.netrc``周り
- ビルド速度の向上に寄与できるか

# 終わりに

まだまだ、私も検証段階なので、まずは方法の紹介でした。
実用に耐え得るのかは、今後調査していきたいと思いますので、定期的に結果はアップデートしたいと思います。

SPMの活路が1つ広がったことを知れたので、今後Cocoapods Carthageを脱却するためのテクニックの1つとして活用していきたいと思います。

# 参考

https://medium.com/geekculture/xcode-13-3-supports-spm-binary-dependency-in-private-github-release-8d60a47d5e45

https://forums.swift.org/t/spm-support-basic-auth-for-non-git-binary-dependency-hosts/37878/56