クラウドMacにSSHでログインでき、xcodebuild -versionが正常にバージョンを返していても、最初の無人ジョブがライセンス確認、追加コンポーネントのインストール、またはツールパスの切り替えで停止することがあります。厄介なのは、この種の問題が新しいノードの追加時、Xcodeのアップグレード時、runnerの再構築後に初めて表面化し、GUIを一度手動で開くと一時的に隠れてしまう点です。より確実なのは、初回起動時の初期化を独立したノード引き渡し手順として扱い、実際のビルドジョブに環境を修復させないことです。
ノード初期化とビルドジョブを分離する
ノード初期化ではシステム状態の変更と管理者権限が必要になり、Xcodeの各バージョンにつき一度だけ実行します。一方、ビルドジョブには再現性と非対話性が求められ、可能な限り権限昇格コマンドを使用すべきではありません。この2つを同じCIスクリプトに混在させると、複数のジョブが同時にコンポーネントをインストールする、無人実行中のsudoが入力待ちになる、失敗原因がコードと未完了の環境設定のどちらにあるか判断できない、という3つの問題が発生します。
runnerのライフサイクルは、次の3段階に分けることを推奨します。
- Xcodeをインストールまたは切り替え、ライセンス承認と初回コンポーネントの初期化を完了する。
- 読み取り専用の事前検査を実行し、実際に使用されるDeveloperディレクトリとツールのバージョンを記録する。
- 事前検査に合格してから、runnerをジョブ受付可能な状態にする。
「Xcodeを開ける」ことは受け入れ条件ではありません。本来の受け入れ条件は、runnerユーザーが非対話型Shellでコンパイルツールを特定し、SDKを照会し、最小限のプロジェクト解析を完了できることです。
実際に使用するXcodeパスを固定する
マシンに複数のXcodeバージョンがある場合、グローバルなxcode-selectだけに依存しないでください。同じ物理ノード上の他のジョブにも影響し、バージョン切り替え後には「ログに表示されたバージョンと実際に呼び出されたバージョンが異なる」という誤解も生じやすくなります。CIでは、ジョブ単位でDEVELOPER_DIRを固定する方法が適しています。
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
printf 'developer_dir=%s
' "$DEVELOPER_DIR"
xcodebuild -version
xcrun --find clang
xcrun --find simctl
xcodebuild -showsdks
バージョン番号を含むアプリケーションディレクトリを使用する場合は、そのパスをrunnerの設定またはバージョン管理された環境ファイルに記述し、複数のスクリプトへ複製しないでください。Xcodeの更新時には1か所だけを変更し、事前検査でパスを出力させます。これにより、ログを調査するときは最初にツールチェーンを特定し、その後でプロジェクト自体を分析できます。
GUIの状態をShell検証の代わりにしない
リモートデスクトップセッションの環境変数、デフォルトShell、権限コンテキストは、runnerサービスと異なる場合があります。すべての検査は、ビルドを実行するユーザーと同じユーザーで行う必要があります。特に、アプリケーションディレクトリの存在だけを確認するのではなく、xcrun --findの結果が想定したDEVELOPER_DIR配下にあることを確認してください。
初回起動処理を一度だけ完了する
ノードの準備段階では、まず初回起動状態を確認し、管理者権限を持つ初期化フローでライセンス承認とコンポーネントの準備を完了します。
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
if ! xcodebuild -checkFirstLaunchStatus; then
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
fi
xcodebuild -checkFirstLaunchStatus
重要なのはコマンドそのものではなく、どこで実行するかです。コミットのたびに起動されるビルドスクリプトへ含めるべきではありません。runFirstLaunchは共有コンポーネントを変更する可能性があります。2つのジョブが同時に実行すれば時間を無駄にするだけでなく、一方のジョブが未完了の状態を読み取るおそれもあります。
ライセンス承認も、ダイアログをクリックして済ませるべきではありません。自動化環境では明示的にコマンドを実行して終了コードを確認し、失敗した場合はノードによるジョブ受付を停止してください。管理者コマンドが対話的な入力を必要とする場合はノードの引き渡し段階で処理し、バックグラウンドのrunnerに入力待ちの処理を残してはいけません。
アトミックロックで同時初期化を防ぐ
自動スケーリング時や複数のサービスを同時に起動するときは、初期化スクリプトが一度だけ実行される設計でも、並行して呼び出される可能性があります。macOSの標準ツールだけで、ディレクトリ作成によるアトミックロックを実装できます。
set -euo pipefail
lock_dir="/tmp/xcode-bootstrap.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
echo "Xcode bootstrap is already running" >&2
exit 75
fi
cleanup() {
rmdir "$lock_dir" 2>/dev/null || true
}
trap cleanup EXIT
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
xcodebuild -checkFirstLaunchStatus
同じファイルシステム上ではmkdirがアトミックに動作するため、成功できるプロセスは1つだけです。終了コード75は、ノードを永続的なビルド障害として扱うのではなく、一時的な失敗として後で再試行するようスケジューリング層に伝えるために利用できます。
異常終了によって残された古いロックにも対処する必要があります。ノードの起動フローで初期化プロセスが動作していないことを確認してから削除するか、再起動後に消去される場所へロックディレクトリを配置します。ロックを見つけただけで削除すると、実行中の初期化を壊す可能性があります。
正式なジョブ受付前に事前検査ゲートを設ける
初回起動状態の確認に合格したら、システムを変更しない受け入れ検査をもう一度実行します。最低限、Xcodeの識別情報、SDKの照会、シミュレーターツール、プロジェクトのエントリーポイントを対象にしてください。
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
xcodebuild -checkFirstLaunchStatus
xcodebuild -version
xcodebuild -showsdks >/tmp/xcode-sdks.txt
xcrun simctl list runtimes
xcodebuild -list -workspace "Example.xcworkspace"
最後の行は、リポジトリで実際に使用するworkspaceまたはprojectに置き換えてください。完全なarchiveを直接実行するよりも高速でありながら、プロジェクトのエントリーポイントが存在しない、依存関係を解決するための前提条件が満たされていない、runnerの作業ディレクトリが誤っている、といった問題を早期に検出できます。
失敗時に残すべき証跡
診断ログには、少なくともDEVELOPER_DIR、xcodebuild -version、xcrun --find clang、xcrun simctl list runtimes、失敗したコマンドの終了コードを残してください。環境変数にはトークン、署名用パスワード、リポジトリの認証情報が含まれる可能性があるため、すべてをログへ出力してはいけません。
一般的な確認順序は、最初にパス、次に初回起動状態、続いて必要なSDKとruntimeを確認し、最後にプロジェクト設定へ進みます。Xcodeのバージョンを切り替えた場合は、旧バージョンで初期化が済んでいても、新しいDeveloperディレクトリに対して同じ手順を再実行してください。完了後は初期化スクリプトのバージョンとXcodeのバージョンを一緒に記録しておけば、runnerの再構築時にも同じ順序で復元でき、誰かが過去にどの画面を手動で開いたかに依存せずに済みます。
よくある質問
画面上でXcodeを開けるのに、CIでは初回起動エラーになるのはなぜですか?
対話セッションとrunnerが別のXcodeパスや環境を参照している可能性があります。CIが使うDEVELOPER_DIRを指定してcheckFirstLaunchStatusを実行してください。
各CIジョブでsudo xcodebuild -runFirstLaunchを実行してよいですか?
推奨しません。ノード準備時またはXcode切り替え後に一度だけ実行し、通常ジョブでは読み取り専用の事前検査に限定します。
Xcodeのバージョンを変更したら再初期化が必要ですか?
必要です。新しいDeveloperディレクトリごとに、初回起動状態、ツールコンポーネント、Simulator runtimeを改めて確認します。
独占利用できるApple Siliconビルドマシンが必要ですか?
Xcodeビルド、iOS CI、リモート開発、自動化タスク向けに、独占利用の物理ノードを日単位、週単位、月単位、または四半期単位でレンタルできます。