クラウドMac CIでXcode Destinationを固定する方法

クラウドMac CIでXcode Destinationを固定する方法

同じ xcodebuild test コマンドをクラウドMac上で数週間にわたって実行していると、突然destinationが利用できないというエラーが発生したり、新しくインストールされたランタイム内の同名シミュレータが選択されたりすることがあります。プロジェクト自体は変わっていません。変化しているのは、Xcodeによる対象デバイスの解決結果です。安定した運用のためには、特定のUDIDをCIに固定するのではなく、アーカイブ、コンパイルチェック、シミュレータテストを明確に分け、各ジョブの開始時に対象を解決して記録します。

3種類のビルド対象を区別する

XcodeのDestinationは「今回の処理をどのプラットフォームとデバイスに対して実行するか」を指定するもので、Schemeと同じではありません。1つの共有Schemeを汎用iOSアーカイブ、特定のシミュレータでのテスト、コンパイルのみの検証に使用できますが、この3つで曖昧なコマンドを共用すべきではありません。

タスク推奨する対象具体的なUDIDが必要か
Archiveの生成generic/platform=iOSいいえ
シミュレータテスト特定のiOS Simulatorはい
コンパイル検証成果物のプラットフォームに応じて汎用または具体的な対象を選択テスト内容による

アーカイブタスクで name=iPhone 16 を指定すると、不要なシミュレータランタイム依存が生じます。反対に、テストタスクで platform=iOS Simulator だけを指定すると、Xcodeが複数の候補を検出する可能性があり、最終的な選択はインストール済みランタイムやデバイスの作成順に左右されます。

汎用対象は「このプロジェクトが対象プラットフォーム向けの成果物を生成できること」を確認するために使い、具体的な対象は「テストが指定したデバイス環境で実行できること」を確認するために使います。1つのDestinationに両方の意味を持たせないでください。

ジョブ開始前に利用可能な対象を確認する

まずXcodeのパスを固定し、その後でプロジェクト自身に利用可能な対象を出力させます。これにより、「Schemeが対象プラットフォームをサポートしていない」場合と「ホスト上に一致するデバイスがない」場合を区別でき、最初からキャッシュを削除せずに済みます。

set -euo pipefail

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
WORKSPACE="App.xcworkspace"
SCHEME="App"

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -showdestinations

出力はジョブの添付ファイルとして保存します。一覧にiOS Simulatorがまったくない場合は、simctl erase all を繰り返すのではなく、SchemeのSupported Destinations、対象プラットフォーム、テストTargetを確認してください。デバイスは存在するもののunavailableと表示される場合は、対応するruntimeが現在のXcodeで引き続きサポートされているか確認します。

ツールチェーンの識別情報も記録する

Destinationの問題は、デフォルトのXcodeが切り替わった際によく発生します。実行のたびに、少なくとも次の情報を記録してください。

xcodebuild -version
xcrun simctl list runtimes
xcrun simctl list devices available

これらの出力にはプロジェクトの秘密情報が含まれないため、ビルド診断情報として保存できます。複数のクラウドMacで並列実行している場合も、特定のツールチェーンでのみ障害が発生しているかを確認できます。

UDIDを固定せずシミュレータを動的に解決する

UDIDは現在のホスト上にあるデバイスインスタンスに属します。シミュレータの削除と再作成、ランタイムの置き換え、ワーカーノードのリセットが行われると、UDIDは変わる可能性があります。CIではデバイス名と利用可能状態を基に検索し、結果が一意であることを必須条件にします。

次のスクリプトは、simctl のJSONから利用可能な iPhone 16 を1台選択します。実際のパイプラインでは対象runtimeの識別子もパラメータとして指定し、異なる2つのOSバージョンに同名のデバイスが存在する状況を避ける必要があります。

SIM_NAME="${SIM_NAME:-iPhone 16}"

SIM_UDID="$(
  xcrun simctl list devices available -j |
  python3 -c '
import json, sys
name = sys.argv[1]
data = json.load(sys.stdin)
matches = []
for runtime, devices in data["devices"].items():
    for device in devices:
        if device.get("isAvailable") and device.get("name") == name:
            matches.append((runtime, device["udid"]))
if len(matches) != 1:
    for runtime, udid in matches:
        print(f"{runtime} {udid}", file=sys.stderr)
    raise SystemExit(f"expected one simulator named {name}, found {len(matches)}")
print(matches[0][1])
' "$SIM_NAME"
)"

printf 'Resolved simulator: %s (%s)\n' "$SIM_NAME" "$SIM_UDID"

重要なのは、候補が一意でなければ即座に失敗させることです。配列の先頭要素を暗黙に選べばジョブは続行できますが、環境の曖昧さが再現困難なテスト結果に変わってしまいます。

テストコマンドとアーカイブコマンドを分離する

UDIDを取得したら、まずデバイスを起動し、初期化が完了するまで待ってからテストを実行します。bootstatus -b を使用すると、システムサービスの準備が整う前にテストが開始されるのを防げます。

xcrun simctl boot "$SIM_UDID" 2>/dev/null || true
xcrun simctl bootstatus "$SIM_UDID" -b

xcodebuild test \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -destination "platform=iOS Simulator,id=$SIM_UDID" \
  -resultBundlePath "Artifacts/TestResults.xcresult"

一方、アーカイブではシミュレータを一切参照しません。

xcodebuild archive \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration Release \
  -destination "generic/platform=iOS" \
  -archivePath "Artifacts/App.xcarchive"

この分離により、権限の境界も明確になります。テスト段階ではシミュレータの状態を管理し、アーカイブ段階ではプロジェクト、署名設定、出力パスだけを扱います。両方の処理を同じSetMiniクラウドMac上で実行する場合でも、ログは2つの独立したセクションに分けてください。

解決結果を検証可能な記録にする

コマンドが成功するだけでは不十分です。パイプラインでは、Scheme、DEVELOPER_DIR、デバイス名、UDID、runtime、処理の種類、結果バンドルのパスをプレーンテキストまたはJSONに記録します。問題を調査するときは、コードの変更を検討する前に、まずこれらの事実を比較します。

終了処理では、このジョブが起動したデバイスだけをシャットダウンすることを推奨します。

cleanup() {
  if [[ -n "${SIM_UDID:-}" ]]; then
    xcrun simctl shutdown "$SIM_UDID" 2>/dev/null || true
  fi
}
trap cleanup EXIT

共有ノード上の全シミュレータを無条件にシャットダウンしないでください。また、erase all を通常のクリーンアップとして使用すべきではありません。前者は並列ジョブを中断させ、後者は状態汚染の原因を覆い隠すうえ、次回の初期化時間を増加させます。

最終確認項目は5つにまとめられます。アーカイブには汎用対象を使用する、テストには今回動的に解決したUDIDを使用する、候補が一意であることを必須とする、実行前にXcodeとruntimeを記録する、終了時にはこのジョブが所有するデバイスだけを処理する、という5点です。これらを徹底すれば、Destinationは暗黙的な環境状態ではなく、検査可能で再現可能なビルド入力になります。

よくある質問

iOSアプリのアーカイブに特定のシミュレータは必要ですか?

必要ありません。アーカイブにはgeneric/platform=iOSを使い、更新で変化するシミュレータから処理を切り離します。

シミュレータのUDIDをCIに固定保存してもよいですか?

推奨しません。ランタイムの再作成でUDIDが変わるため、ジョブ開始時に機種名とランタイムから解決し、実際の値をログへ残します。

同名のシミュレータが複数ある場合はどうしますか?

利用可能なデバイスと対象ランタイムで絞り込みます。それでも複数残る場合は、候補を表示してジョブを失敗させるべきです。

専有物理ノード

いつでも接続できるクラウドMacに開発環境を移行

2種類のApple Silicon構成と4つのデータセンターから選択できます。リソースは他の利用者と共有されず、実際の利用状況はコンソールでリアルタイムに確認できます。

料金プランを選ぶ