CopyButton

2026年5月1日金曜日

スマホのリモート操作で iOS アプリをビルド&TestFlight 配信する【SSH + build keychain・2026年版】

この記事は「外出先のスマホからでも自宅 Mac の Xcode を叩いて iOS アプリを TestFlight に上げたい」エンジニア向け。スマホ → 母艦ノートPC(WSL2)→ 別マシンの Mac に多段 ssh で繋ぎ、archive → IPA → TestFlight アップロードまで 1 コマンドで完結させる手順。codesign が SSH で詰まる理由と回避策(専用 build keychain)、配信スクリプト 3 本の中身も全部書く。

ノートPCのターミナル・Mac mini・スマホが連携してアプリを配信している作業デスクの写真

この記事でできること

  • 外出先のスマホから(または WSL2 のターミナルから)SSH 1 行で TestFlight に新ビルドが上がる
  • Mac は Xcode と xcodebuild の実行環境としてだけ使い、普段は触らない
  • App Store Connect API Key.p8)認証で 2 段階認証ダイアログを出さない
  • VPS や GitHub Actions を使わずに自宅 LAN 内で完結(Mac mini や旧 MacBook の余生活用にも向く)

4つのアプローチ比較

構成 SSH + 自前スクリプト
(本記事)
Mac で Xcode 操作 GitHub Actions (macos) fastlane 主導
月額コスト 0 円(自宅 Mac の電気代のみ) 0 円 macOS runner は分単価高め 0 円(自宅 Mac)
エディタ WSL2 の好みのもの(VS Code / vim 等) Xcode 任意 任意
配信 1 回の所要時間 3〜6 分(archive + IPA + altool) 5〜10 分(GUI 経由) push 後 10〜15 分(CI 起動含む) 3〜6 分
秘匿情報の置き場 Mac ローカル(~/.appstoreconnect Mac ローカル GitHub Secrets Mac ローカル / Match

構成図

WSL2(コード編集マシン)
エディタ + git push + ssh の起点
▼ git push
▼ ssh + bash scripts/upload.sh
Mac(Xcode 専用 archive サーバ)
build.keychain-db / xcodebuild / altool
▼ xcrun altool --upload-app
App Store Connect / TestFlight

使ったもの

なぜ setup_build_keychain.sh が必要か

普通に Mac の Terminal で xcodebuild archive すれば通るのに、SSH 経由だと codesign が「キーチェーンを開けません」で落ちるのが本記事のスタートライン。

原因: login keychain は SSH ログインで unlock しても codesign から触れない。GUI ログインセッションに紐づいた security agent が居ないため、ACL 上の「アプリ確認なしに使ってよい」リストが働かず、対話プロンプトを出そうとして失敗する。

解決策は 専用の build keychain を作って Apple Distribution 証明書を複製、ACL を緩めて codesign から GUI 確認なしに使える状態にする。これを scripts/setup_build_keychain.sh として 1 度だけ Mac の Terminal(GUI セッション)で実行する。

以下のコード中の placeholder は自分の環境の値に置き換えてください({kcpw} は build keychain をローカルでアンロックするための任意文字列で、外部秘匿情報ではありません)。

{user}                        自分のユーザ名(Mac の login user)
{mac_addr}                    Mac の IP/ホスト名(例: 192.168.x.y)
{app_name}                    アプリ名(Xcode scheme / archive 名と一致)
{bundle_id}                   Bundle ID(例: com.example.MyApp)
{team_id}                     Apple Developer Team ID(10 文字)
{kcpw}                        build keychain ローカル鍵(任意の文字列)
{provisioning_profile_name}   Provisioning Profile 名
#!/bin/bash
set -euo pipefail

KCNAME="build.keychain"
KCPATH="$HOME/Library/Keychains/${KCNAME}-db"
KCPW="{kcpw}"                  # build keychain アンロック用(ローカル container 鍵。任意の文字列)
EXPORTPW="$(uuidgen)"          # .p12 一時暗号用(本スクリプト中だけ使用)

if [[ -f "$KCPATH" ]]; then
    echo "build keychain は既に存在: $KCPATH"; exit 1
fi

# 1. build keychain 作成
security create-keychain -p "$KCPW" "$KCNAME"
security set-keychain-settings -lut 21600 "$KCNAME"
security unlock-keychain -p "$KCPW" "$KCNAME"

# 2. login keychain から identity を export(GUI で「常に許可」を選ぶ)
TMPP12="$(mktemp -t build-cert).p12"
trap 'rm -f "$TMPP12"' EXIT
security export -k "$HOME/Library/Keychains/login.keychain-db" \
    -t identities -f pkcs12 -P "$EXPORTPW" -o "$TMPP12"

# 3. build keychain へ import + codesign / productbuild が触れるよう -T 指定
security import "$TMPP12" -k "$KCNAME" -P "$EXPORTPW" \
    -T /usr/bin/codesign -T /usr/bin/security \
    -T /usr/bin/productbuild -T /usr/bin/productsign

# 4. ACL で GUI プロンプトを抑制
security set-key-partition-list \
    -S 'apple-tool:,apple:,codesign:' -s -k "$KCPW" "$KCNAME"

# 5. search list の先頭に追加
ORIGINAL_LIST="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g' | tr '\n' ' ')"
security list-keychains -d user -s "$KCNAME" $ORIGINAL_LIST

security find-identity -v -p codesigning "$KCNAME"

ポイントは 4 番目の set-key-partition-list。ここで apple-tool: / apple: / codesign: をアクセス許可リストに入れて、codesign 系ツールが GUI 確認なしに鍵を使える状態にする。これが SSH 経由でも証明書アクセスできる正体。

archive.sh — Release archive を非対話で生成

2 本目のスクリプト。SSH 経由で xcodebuild archive を叩く。実行前に build keychain を unlock して、6 時間自動ロックを設定する。

#!/bin/bash
set -euo pipefail

APP_NAME="{app_name}"          # ← 自分のアプリ名に置き換え
KCPW="{kcpw}"                  # ← setup_build_keychain.sh で使ったのと同じローカル鍵

ARCHIVE_PATH="${ARCHIVE_PATH:-/tmp/${APP_NAME}.xcarchive}"
BUILD_KCPW="${BUILD_KCPW:-${KCPW}}"
WORKSPACE="${WORKSPACE:-$(pwd)/${APP_NAME}.xcworkspace}"

if [[ ! -f "$HOME/Library/Keychains/build.keychain-db" ]]; then
    echo "✗ build.keychain-db が無い。setup_build_keychain.sh を先に。" >&2
    exit 1
fi

# build keychain unlock(6h 自動ロック)
security unlock-keychain -p "$BUILD_KCPW" build.keychain
security set-keychain-settings -lut 21600 build.keychain

# archive
rm -rf "$ARCHIVE_PATH"
LOG=/tmp/archive.log
xcodebuild \
    -workspace "$WORKSPACE" \
    -scheme "$APP_NAME" \
    -configuration Release \
    -destination 'generic/platform=iOS' \
    -archivePath "$ARCHIVE_PATH" \
    archive \
    > "$LOG" 2>&1 || true

if grep -q "ARCHIVE SUCCEEDED" "$LOG"; then
    echo "✓ ARCHIVE SUCCEEDED -> $ARCHIVE_PATH"
else
    echo "✗ ARCHIVE FAILED (log: $LOG)"
    grep -B1 -A4 -E 'error:|errSec|FAILED' "$LOG" | tail -30
    exit 1
fi

ログを /tmp/archive.log に逃がして、成否を ARCHIVE SUCCEEDED 文字列で判定する形。xcodebuild の標準出力は冗長なので SSH 越しに見るのは消耗する。

ExportOptions.plist — Manual signing を明示

archive から IPA をエクスポートするときの設定ファイル。Auto signing で何度かハマったので、Manual signing で profile 名と Team ID を明示する形に固定した。git にコミットしても秘匿情報は含まれない。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>app-store-connect</string>
    <key>teamID</key>
    <string>{team_id}</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>{bundle_id}</key>
        <string>{provisioning_profile_name}</string>
    </dict>
    <key>uploadSymbols</key><true/>
    <key>compileBitcode</key><false/>
    <key>stripSwiftSymbols</key><true/>
</dict>
</plist>

Bundle ID とプロファイル名は自分の値に置き換える。プロファイル名は Apple Developer ポータルで作成したもの。

upload.sh — IPA エクスポート + altool アップロード

3 本目。archive を再利用して IPA を吐き、xcrun altool で TestFlight に投げる。altoolApp Store Connect API Key 認証だと 2 段階認証ダイアログが出ない。

#!/bin/bash
set -euo pipefail

APP_NAME="{app_name}"          # ← 自分のアプリ名に置き換え
KCPW="{kcpw}"                  # ← setup_build_keychain.sh で使ったのと同じローカル鍵

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ARCHIVE_PATH="${ARCHIVE_PATH:-/tmp/${APP_NAME}.xcarchive}"
EXPORT_DIR="${EXPORT_DIR:-/tmp/${APP_NAME}-export}"
EXPORT_OPTIONS="${EXPORT_OPTIONS:-$SCRIPT_DIR/ExportOptions.plist}"
ALTOOL_ENV="${ALTOOL_ENV:-$PROJECT_ROOT/Config/altool.env}"
BUILD_KCPW="${BUILD_KCPW:-${KCPW}}"

# Config/altool.env から ASC_KEY_ID / ASC_ISSUER_ID をロード
source "$ALTOOL_ENV"

# archive が無ければ自動で archive.sh を呼ぶ(再利用が効く)
if [[ ! -d "$ARCHIVE_PATH" ]]; then
    bash "$SCRIPT_DIR/archive.sh"
fi

# IPA 出力(exportArchive も再署名するので keychain unlock)
security unlock-keychain -p "$BUILD_KCPW" build.keychain

rm -rf "$EXPORT_DIR"
xcodebuild -exportArchive \
    -archivePath "$ARCHIVE_PATH" \
    -exportPath "$EXPORT_DIR" \
    -exportOptionsPlist "$EXPORT_OPTIONS" \
    > /tmp/export.log 2>&1 || true

IPA_PATH="$(find "$EXPORT_DIR" -maxdepth 1 -name '*.ipa' | head -n1)"
[[ -f "$IPA_PATH" ]] || { echo "✗ IPA 生成失敗"; exit 1; }

# TestFlight アップロード
xcrun altool --upload-app -f "$IPA_PATH" -t ios \
    --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"

Config/altool.env.gitignore 済みで、中身は 2 行だけ。

ASC_KEY_ID=ABC123XYZ4
ASC_ISSUER_ID=12345678-90ab-cdef-1234-567890abcdef

.p8 ファイル本体は ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8 に置く(altool がこのパスを自動で見に行く)。

WSL2 から 1 行で TestFlight に上げる

ここまで揃えると、WSL2 側の運用は 1 行で済む。

# CFBundleVersion を bump して push したあとに
ssh {user}@{mac_addr} 'cd ~/path/to/{app_name} && bash scripts/upload.sh'

3〜6 分後に TestFlight に新ビルドが現れる。App Store Connect の Processing が完了したらすぐ自分の iPhone で Install 可能。

詰まりポイント

  • codesign が GUI プロンプトを出して固まる: build keychain の set-key-partition-list を忘れている。apple-tool:,apple:,codesign: を必ず指定
  • Auto signing が別チームの Development cert にフォールバック: Release は Manual signing 固定 + Team ID 明示が安全。ExportOptions.plistsigningStyle = manual
  • build keychain が 6 時間で自動ロックされて翌日のビルドが落ちる: archive.sh / upload.sh の冒頭で毎回 security unlock-keychain を呼ぶ。一度の set-keychain-settings -lut 21600 だけだと足りない
  • SPM の dynamic framework が Embed されない: RealmSwift のような source-distributed dynamic framework は project.pbxproj の Embed Frameworks に手動登録が必要。Firebase / GoogleMobileAds の XCFramework binary target は auto-embed されるので忘れがち
  • altool の 2 段階認証ダイアログ: パスワード認証だと 2FA でブロックされる。必ず App Store Connect API Key(.p8)認証を使う
  • Mac の sshd が起動していない: システム設定 → 一般 → 共有 → リモートログイン を有効化

よくある質問

Q. Mac は普段スリープでも大丈夫?

A. 配信するときだけ叩くなら Wake-on-LAN + システム設定の「ネットワークアクセスによりスリープ解除」を ON にすれば SSH パケットで起きる。常時稼働させるなら蓋を閉じても寝ない設定(caffeinate + 電源接続中はスリープしない)にしておく。

Q. Apple Distribution の証明書はどう発行する?

A. Mac の Xcode で Settings → Accounts → Apple Developer の Manage Certificates から「Apple Distribution」を発行 → login keychain に登録される。setup_build_keychain.sh を走らせるとそれが build keychain に複製される。

Q. Provisioning Profile を更新したらどうする?

A. Apple Developer ポータルでダウンロード → Mac で開く → Xcode の Signing & Capabilities で読み込まれているか確認。ExportOptions.plist の profile 名は変わらないので、スクリプト側の修正は不要。

Q. Mac mini を専用 archive サーバにしたい

A. M2 / M4 の Mac mini なら Xcode 26 + SPM の archive で 3〜5 分。常時電源 ON でも消費電力 10W 前後。Mac mini + LINE Notify でビルド完了通知を飛ばせば、外出中でも完走を把握できる。

Q. fastlane を使ったほうが早い?

A. fastlane は match による証明書同期や lane の構造化が便利。本記事のスクリプトは 「Mac 1 台で完結する個人配信」に絞っているので fastlane の機能のごく一部だけを薄く再実装した形。チーム開発になったら fastlane に乗り換える価値はある。

※本記事の手順は執筆時点(2026 年 5 月)の Xcode 26 / macOS 15 系で動作確認しています。Apple Distribution 証明書の発行手続きや App Store Connect の UI は変わる場合があります。動かない場合は コメント欄でお知らせください。

まとめ

Xcode を WSL2 から完全リモートで動かすのは build keychain と App Store Connect API Keyの 2 点を押さえれば素直に組める。シェルスクリプト 3 本(setup_build_keychain.sh / archive.sh / upload.sh)と ExportOptions.plist 1 枚で完結する。Mac は Xcode 専用の archive マシンとして奥に置いて、普段の開発は Linux の好みの環境で行える。

この記事が役に立ったら X(Twitter)でシェアしてもらえると喜びます。

関連記事

参考

※この記事は Claude Code を使った自動更新を試しています。

0 件のコメント:

コメントを投稿