プライベートリポジトリから XCFramework を配布する
ビルド時間改善のために XCFramework をプライベートに配布したいケースがある。このページでは XCFramework の作成方法ではなく配布方法に絞って言及する。自分の場合は Realm の XCFramework を自前でビルドした上でここに記述した方法で自作アプリに配布している。
前提
XCFramework 配布用の GitHub リポジトリがある
該当の GitHub リポジトリがプライベートである
該当の GitHub リポジトリ上の Release の Asset に XCFramework の zip が添付されている
Package.swift の記述
GitHub リポジトリの Release Asset にアクセスするために Asset ID が必要になる。自分は以下のようなスクリプトを用意して binaryTarget を取得している。
code:generate-binary-targets.sh
set -euo pipefail
TAG="$1"
TMP_DIR=$(mktemp -d)
trap 'rm -rf "${TMP_DIR}"' EXIT
gh release download "${TAG}" --repo tasuwo/MyLibrary --pattern "*.xcframework.zip" --dir "${TMP_DIR}"
ASSETS=$(gh release view "${TAG}" --repo tasuwo/MyLibrary --json assets)
echo "${ASSETS}" | jq -c '.assets[] | select(.name | test(".*\\.xcframework\\.zip$"))' |
while read -r ASSET; do
NAME=$(echo "${ASSET}" | jq -r '.name')
URL=$(echo "${ASSET}" | jq -r '.apiUrl')
CHECKSUM=$(swift package compute-checksum "${TMP_DIR}/${NAME}")
cat << EOS
.binaryTarget(
name: "$(basename "${NAME}" .xcframework.zip)",
url: "${URL}.zip",
checksum: "${CHECKSUM}"
),
EOS
done
実行結果は例えば以下のような感じ。
code:swift
.binaryTarget(
name: "MyLibrary",
checksum: "5bb5593f607028779542fc06924e46dab584e0a322d7c578e6aff845d984bed5"
)
方法
Swift Package Manager にプライベートリポジトリ上の Asset をダウンロードさせる方法は以下の二種類。前者をローカル PC 上、後者を CI 上で使う。
Keychain 上にトークンを保管しておく
.netrc を配置し、トークンを記述しておく
Keychainにトークンを保存しておく
Swift Package Manager は Keychain 上のインターネットパスワードを必要に応じて参照する。
1. GitHub 上でSettings > Developer Settings > Personal access tokens > Tokens (classic) と遷移する
2. Generate new token (classic) からトークンを新規作成する
Note: 任意で良い
Expiration: 任意で良い
Select scopes: repo にチェック
3. 生成したトークンをメモしておく
4. ローカル PC 上で Keychain を開く
5. Keychain にトークンを保存する
アカウント名: 自分のGitHubユーザーID
パスワード: メモしておいたトークン
6. Xcode を開く
開いた際にKeychainアクセス許可を求められるので、許可する
7. GitHub Release から XCFramework が DL できるようになる
Tips: ダウンロードに失敗する or アクセス許可が求められない
XCFramework の取得失敗の結果がキャッシュされてしまっている可能性がある。Xcode を終了し以下の artifacts 配下からキャッシュを削除し、再起動してみるのが良い。
~/Library/Caches/org.swift.swiftpm/artifacts
~/Library/Developer/Xcode/DerivedData/{プロジェクト名}/SourcePackages/artifacts
.netrc にトークンを保存しておく
CI 上で Keychain へのアクセス許可を求められても許可できないので、.netrc を利用する。ここにトークンを直接記述しても良いけど、複数人のチームで開発している場合は誰のトークンを記述する?という問題がある。そんな場合には GitHub Apps を利用するのがおすすめ。
GitHub Apps を作成する
1. Settings > Developer Settings > GitHub Apps から Create する
name: my-ci (適当で良い)
Webhook: チェックを外す
Permissions: Repository permissions > Contents を Read-only にする
2. App ID をメモっておく
3. 画面下部から Generate private key を実施して、秘密鍵をローカルにインストールする
4. 画面左から「Install App」を選択し、インストール先のアカウントとリポジトリを選ぶ
XCFramework を配布しているリポジトリのみを選ぶのがおすすめ
CI 上でトークンを作成する
Xcode Cloud のワークフローの環境変数に以下を設定しておく。
table:env
変数名 値
GITHUB_APP_ID GitHub App の App ID
GITHUB_APP_PRIVATE_KEY インストールした秘密鍵をワンラインにしたもの
GITHUB_APP_INSTALLATION_ID GitHub App の Installation ID
ci_post_clone.sh を以下のようにして ci_scripts ディレクトリ以下に配置しておく。Xcode Cloud で実行すると、うまくいけば .netrc が作成されトークンが適宜発行される。
code:ci_post_clone.sh
brew install jq
rm ~/.netrc
fi
#
# JWT トークンの作成
#
#
now=$(date +%s)
iat=$((${now} - 60))
exp=$((${now} + 600))
b64enc() { openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n'; }
header_json='{
"typ":"JWT",
"alg":"RS256"
}'
header=$( echo -n "${header_json}" | b64enc )
payload_json="{
\"iat\":${iat},
\"exp\":${exp},
\"iss\":\"${GITHUB_APP_ID}\"
}"
payload=$( echo -n "${payload_json}" | b64enc )
# GITHUB_APP_PRIVATE_KEY には cat key.pem | sed -e :loop -e 'N; $!b loop' -e 's/\n/\\n/g' でエスケープされた改行文字を付与しておく
private_key=$(echo "${GITHUB_APP_PRIVATE_KEY}" | sed 's/\\n/\n/g')
header_payload="${header}"."${payload}"
signature=$(
openssl dgst -sha256 -sign <(echo -n "${private_key}") <(echo -n "${header_payload}") | b64enc
)
JWT="${header_payload}"."${signature}"
#
# GitHub App インストールとしての認証
# MyLibrary の箇所は XCFramework アップロード先のリポジトリ名にする
#
#
response=$(
curl -s -o response.json -w "%{http_code}" -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${JWT}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-d '{"repositories":"MyLibrary","permissions":{"contents":"read"}}' )
status_code=$(echo "${response}" | tail -n1)
if (( "${status_code}" >= 200 && "${status_code}" < 300 )); then
token=$(jq -r .token response.json)
rm response.json
fi
echo "machine api.github.com" > ~/.netrc
echo "login x-access-token" >> ~/.netrc
echo "password ${token}" >> ~/.netrc
chmod 600 ~/.netrc