🎃

GitHub Private repo で XCFramework を配布する

に公開

ソースコードを開示せずにユーザーにXCFrameworkを配布する一番の方法はXCFrameworkをSwift Package Managerで配布することです。

この記事は、配布する側の立場で陥りやすい罠とその回避方法をまとめたものです。
やってみたけどハマってしまった場合に読んでみたり、罠にハマる前に勉強しておきたい方は読んでみてください。

また、CocoaPodsからSwift Package Managerに乗り換えようとしてハマっている方は前作をまず読むのがおすすめです。

https://zenn.dev/d_date/articles/cd4ce3b2b5c29d

今回の記事は、Private RepositoryにXCFrameworkをホストする必然性が出てきた方に向けた内容です。

XCFrameworkを配布する方法

XCFrameworkを作成する手順は省略しますが、Swift Package ManagerがPrivate Repositoryにアクセスするためには、ユーザーがそのリポジトリにアクセスできるようにしておく必要があります。

XCFrameworkを配布するためには、以下の選択肢が思い浮かびます。

a. △ 直接Commitする
b. × Git LFS( Large File Storage )を使う
c. ○ Releaseにアップロードする
d. × Package Registryを使う

まずaについては、容量の大きいバイナリを直接Commitするのは避けるべきです。
Gitの履歴が肥大化してしまい、クローンやフェッチに時間がかかるようになってしまいます。それだけではなく、GitHubのリポジトリサイズ制限に引っかかる可能性もあります。

次にbについて、これも推奨しません。
Git LFSはGitの拡張機能であり、GitHubのFreeプランでは月間10GBまでしか使えません。また通信量にも制限や課金が発生します。

dについては、Swift Package ManagerのPackage Registryにはまだ対応していません。
したがって、cのReleaseにアップロードする方法を採用します。

Releaseにアップロードする

Releaseにアップロードするには、GitHubのリポジトリのReleaseタブから行います。

事前にコミットにtagを付けておき、そのtagに対応するReleaseを作成し、AssetsにXCFrameworkをドラッグ&ドロップするだけです。
Releaseにアップロードした後、Releaseのページにアクセスすると、アップロードしたXCFrameworkのURLを取得できます。

これをPackage.swiftのbinaryTargetに指定します。

import PackageDescription

let package = Package(
  name: "MySDK",
  platforms: [.iOS(.v14)],
  products: [
    .library(
      name: "MySDK",
      targets: ["MySDK"])
  ],
  dependencies: [],
  targets: [
    .binaryTarget(
      name: "MySDK",
      url: "https://api.github.com/repos/<ORG>/<REPO>/releases/assets/<ASSET_ID>.zip",
      checksum: "<CHECKSUM>"
    )
  ]
)

<ORG>は組織名、<REPO>はリポジトリ名、<ASSET_ID>はReleaseにアップロードしたXCFrameworkのAsset IDです。<CHECKSUM>はXCFrameworkのSHA256チェックサムです。
チェックサムは以下のコマンドで計算できます。

swift package compute-checksum SDK.xcframework.zip

のはずですが、実際にPackage.swiftに指定してみるとchecksumが間違っていると怒られます。その時正しいチェックサムを教えてくれるので、それを指定すると確実です。

さて、ここで404 Not Foundエラーが発生する場合があります。
ReleaseのAssetは認証が必要なため、Swift Package Managerがアクセスできない場合があります。
この場合はnetrcを使って認証情報を提供する必要があります。

.netrcを使って認証情報を提供する

netrcファイルは、ユーザーのホームディレクトリに配置されるテキストファイルで、FTPやHTTPクライアントが使用する認証情報を保存します。
Swift Package Managerもこのファイルを参照して認証情報を取得します。
netrcファイルの例は以下の通りです。

machine github.com
login <USERNAME>
password <TOKEN>

<USERNAME>はGitHubのユーザー名です。
<TOKEN>はPersonal Access Tokenを利用できますが、今回はGitHub Appを使う方法を紹介します。Personal Access Tokenは、GitHubのSettings > Developer settings > Personal access tokensから作成できますが、今回はGitHub Appを使う方法を紹介します。

[!WARNING] PATは簡単に作成できますが、漏洩するとリポジトリに不正アクセスされる可能性があるのと、有効期限が切れる前にローテーションする必要があり面倒です。

netrcファイルを作成したら、~/.netrcに配置します。

GitHub Appを使って認証情報を提供する

GitHub Appは、GitHubのAPIにアクセスするためのアプリケーションです。
GitHub Appを作成し、必要な権限を設定します。

Settings > Developer settings > GitHub Appsから新しいGitHub Appを作成します。

GitHub Appを作成したら、Client IDPrivate Keyを取得します。App IDは今回は利用しません。
スクロールしていくと、Private Keyを生成するボタンがあります。
Private KeyはPEM形式でローカルに保存されます。

次に、GitHub Appをインストールします。
左側のメニューからInstall Appを選択し、インストールしたいリポジトリを選択します。
このリポジトリはSDKを利用する側と、SDKを配布する側の両方にインストールする必要があります。

Xcode Cloudで利用する

Xcode CloudでSwift Package Managerを利用する場合、netrcファイルをクローンしたタイミングで配置する必要があります。そのために、 ci_post_clone.shスクリプトを利用します。

環境変数の設定

スクリプトでは、環境変数からGitHub AppのPrivate Key、Client ID、Installation IDを取得し、JWTを生成してGitHub APIにアクセスし、インストールトークンを取得します。

本来は、Installation IDはAPIを都度叩いて取得しますが、実はこれは固定値なので、環境変数として直接設定しています。

Xcode Cloudの(Shared) Environment VariablesにPEMを入れる場合はBase64エンコードして改行コードを削除したものを設定します。そのまま入れるとPrivate Keyが見つからないという罠に陥ります。

base64 -i private_key.pem | pbcopy

JWTを生成してインストールトークンを取得する

以下のようにJWTを生成します。JWTの有効期限は10分以内に設定します。

b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }

pem_file="$(mktemp)"
trap 'rm -f "$pem_file"' EXIT

echo "$PRIVATE_KEY" | base64 -d > "$pem_file" 2>/dev/nul

tr -d '\r' < "$pem_file" > "${pem_file}.clean" && mv "${pem_file}.clean" "$pem_file"

grep -q "BEGIN .*PRIVATE KEY" "$pem_file"
openssl pkey -in "$pem_file" -noout >/dev/null 2>&1

now=$(date -u +%s)
iat=$((now - 60))
exp=$((now + 540))

header='{"alg":"RS256","typ":"JWT"}'
payload=$(cat <<JSON
{"iat":${iat},"exp":${exp},"iss":"${CLIENT_ID}"}
JSON
)

h_b64=$(printf %s "$header"  | b64url)
p_b64=$(printf %s "$payload" | b64url)
sig_b64=$(printf '%s.%s' "$h_b64" "$p_b64" | openssl dgst -binary -sha256 -sign "$pem_file" | b64url)
JWT="${h_b64}.${p_b64}.${sig_b64}"

Access Tokenを取得する

APIを叩いてAccess Tokenを取得します。

resp_with_code=$(
  curl -sS -X POST \
    -w "\n%{http_code}" \
    -H "Authorization: Bearer ${JWT}" \
    -H "Accept: application/vnd.github+json" \
    "https://api.github.com/app/installations/${INSTALLATION_ID}/access_tokens"
)

resp_body="${resp_with_code%$'\n'*}"

GH_TOKEN=$(printf %s "$resp_body" | jq -r '.token // empty')
EXPIRES_AT=$(printf %s "$resp_body" | jq -r '.expires_at // empty')

netrcファイルを作成する

取得したAccess Tokenを使ってnetrcファイルを作成します。この時ユーザー名はx-access-tokenを指定します。

{
  echo "machine github.com login x-access-token password ${GH_TOKEN}"
  echo "machine api.github.com login x-access-token password ${GH_TOKEN}"
} > "$HOME/.netrc"
chmod 600 "$HOME/.netrc"

Gitの設定を変更する

Remote URLにAccess Tokenを含めるようにGitの設定を変更します。

git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "https://github.com/"

おまけ

post_clone.shスクリプトの最後に以下を追加しておくと、CI上でSwift Package Managerを利用する場合にPackage PluginやMacroのFingerprint Validationをスキップすることができます。

defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES
defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES

[!NOTE] IDESkipPackagePluginFingerprintValidatationは IDESkipPackagePluginFingerprintValidation ではありません。タイプミスに注意してください。

Discussion