Oakmini エンジニアリングノート

クラウドMacでXcodeを固定して再現可能なビルドを作る

CI/CD ·約 11 分

クラウドMacでXcodeを固定して再現可能なビルドを作る

1台のクラウドMacで安定版ブランチ、日常開発ブランチ、アップグレード検証ブランチを同時に運用する場合、見落とされやすい変数はコードではなく、現在有効になっているXcodeです。あるジョブがグローバル設定を切り替えると、後続のパイプラインが別のSwiftコンパイラやSDKを知らないうちに使い始める可能性があります。その結果、ローカルではビルドできるのにリモートでは失敗したり、同じコミットから翌日には異なるビルド結果が生成されたりします。

ツールチェーンをプロジェクトの依存関係として扱う

Xcodeは、単にマシン上にあるアプリとして扱うべきではありません。言語ランタイムと同様に、使用するバージョンを明示的に記録する必要があります。新しいインストールによって古いディレクトリを直接上書きせず、バージョン付きのアプリケーションディレクトリを維持してください。

/Applications/Xcode_16.1.app
/Applications/Xcode_16.2.app
/Applications/Xcode_16.3.app

まず、インストール済みのXcodeと現在のグローバル設定を確認します。

find /Applications -maxdepth 1 -name 'Xcode*.app' -print
xcode-select --print-path
xcodebuild -version
xcrun --find swift
swift --version

少なくとも、Xcodeのバージョン、ビルド番号、Swiftのバージョン、プロジェクトで使用するSDKを記録します。「最新版を使用」とだけ書いても、過去のビルドは再現できません。また、失敗の原因がコードなのか、ツールチェーンの変更なのかも判断できません。

バージョン固定の目的はアップグレードを拒むことではなく、アップグレードを観測可能でロールバック可能なエンジニアリング上の変更にすることです。

適用範囲に応じてXcodeを選択する

Xcodeの選択方法は、タスクの適用範囲に応じて決めます。グローバルなデフォルト設定は手動管理に向いており、ジョブ単位の環境変数は自動化に適しています。

方法 適用範囲 推奨される用途 並行実行時のリスク
xcode-select ホスト全体 1人での管理、共通のデフォルトバージョン 他のジョブに影響する
DEVELOPER_DIR 現在のプロセスと子プロセス CI、スクリプト、並行プロジェクト 低い
コマンド直前の一時的な代入 1つのコマンド 簡易検証 低い

グローバルなデフォルトを変更する場合は、次のコマンドを実行します。

sudo xcode-select --switch /Applications/Xcode_16.2.app/Contents/Developer

CIでは、このコマンドを繰り返し実行しないでください。代わりに、各ジョブの開始時に次のように設定します。

export DEVELOPER_DIR="/Applications/Xcode_16.2.app/Contents/Developer"
xcodebuild -version
xcrun --sdk iphoneos --show-sdk-version

DEVELOPER_DIRは子プロセスにも引き継がれるため、後続のxcrunswiftxcodebuildは、すべて同じDeveloperディレクトリからツールを解決します。パイプラインごとに異なるパスを設定できるため、グローバルな状態を奪い合うこともありません。

事前検証スクリプトでサイレントなバージョンドリフトを防ぐ

ログにバージョンを出力するだけでは不十分です。事前検証では、バージョンが一致しない時点でジョブを即座に終了させ、十数分かけてコンパイルした後に問題が判明する事態を防ぐ必要があります。次のスクリプトはXcode 16.2を固定し、アプリケーションディレクトリ、実際のバージョン、SDK、Swiftのパスを確認します。

#!/bin/bash
set -euo pipefail

XCODE_APP="/Applications/Xcode_16.2.app"
EXPECTED_VERSION="16.2"

test -x "$XCODE_APP/Contents/Developer/usr/bin/xcodebuild"
export DEVELOPER_DIR="$XCODE_APP/Contents/Developer"

ACTUAL_VERSION="$(xcodebuild -version | awk 'NR == 1 {print $2}')"
test "$ACTUAL_VERSION" = "$EXPECTED_VERSION"

echo "developer=$DEVELOPER_DIR"
echo "xcode=$ACTUAL_VERSION"
echo "swift=$(xcrun --find swift)"
echo "sdk=$(xcrun --sdk iphoneos --show-sdk-version)"

xcodebuild -checkFirstLaunchStatus

このスクリプトを、たとえばci/verify-toolchain.shとしてリポジトリに配置し、バージョン要件もコードと一緒にレビューできるようにします。アップグレード時には、スクリプトとプロジェクトのドキュメントを同時に更新してください。ホスト上で手作業を行うよりも、信頼できる変更履歴を残せます。

プロジェクトで特定のSDKバージョンが必要な場合は、厳密な一致判定も追加できます。XcodeのメジャーバージョンからSDKを推測してはいけません。同じシリーズでも、リリースによって異なるSDKが同梱される場合があります。

ビルドコマンドも明示する

事前検証に合格したら、固定したエントリーポイントからビルドを実行します。

export DEVELOPER_DIR="/Applications/Xcode_16.2.app/Contents/Developer"

xcodebuild \
  -workspace Example.xcworkspace \
  -scheme Example \
  -configuration Release \
  -derivedDataPath "$PWD/.derived-data" \
  CODE_SIGNING_ALLOWED=NO \
  build

ここでは、workspace、scheme、ビルド構成、Derived Dataのパスを明示しています。CODE_SIGNING_ALLOWED=NOが適しているのは、署名を必要としないコンパイル検証だけです。アーカイブや実機向けビルドでは、このオプションをそのまま流用せず、プロジェクトで既に運用している安全な署名フローを使用してください。

新バージョンは並行検証してから切り替える

新しいXcodeをインストールした後も旧バージョンを残し、独立したジョブでアップグレードを検証します。初回セットアップは、管理者が次のコマンドで実行できます。

sudo DEVELOPER_DIR="/Applications/Xcode_16.3.app/Contents/Developer" \
  xcodebuild -runFirstLaunch

その後、同じコミットを旧バージョンと新バージョンの両方で実行し、コンパイルの成否、テスト数、警告の変化、成果物のアーキテクチャ、アーカイブのメタデータを比較します。最初から旧ディレクトリを削除してはいけません。依存関係やコンパイルオプションに互換性がなかった場合、すぐにロールバックできなくなります。

アップグレードの受け入れ検証では、プロジェクト形式が自動的に書き換えられていないかも確認します。新しいXcodeがプロジェクトファイルを変更した場合、その差分は独立したコミットとしてレビューし、アプリケーションコードと混在させないでください。これにより、ツールによる自動移行、依存関係の更新、実際の機能変更を切り分けられます。

よくある落とし穴とリリース前チェックリスト

1つ目の落とし穴は、xcode-selectだけを切り替え、ジョブ環境に残っているDEVELOPER_DIRを見落とすことです。環境変数のほうが優先されるため、ログに表示されたグローバルパスと、ジョブが実際に使用するパスが同じとは限りません。2つ目は、アプリケーションを単にXcode.appと命名することです。アップグレードで上書きされると、以前のジョブがどのバージョンを使用していたか判断できません。3つ目は、コンパイラだけを確認し、SDKとxcrunのパスを検証しないことです。その結果、意図しないツールがビルドに混入します。

リリース前に、次の順序で確認してください。

  1. Xcodeのアプリケーションディレクトリに明確なバージョン番号が含まれ、旧バージョンも引き続き使用できる。
  2. 期待するバージョンがリポジトリに記録され、人の記憶に依存していない。
  3. 各CIジョブが個別にDEVELOPER_DIRを設定している。
  4. 事前検証でXcode、Swift、SDK、ツールのパスをすべて確認している。
  5. ビルドコマンドでworkspace、scheme、ビルド構成を明示している。
  6. 新バージョンをデフォルトにするのは、コンパイル、テスト、成果物の検証に合格した後である。
  7. 並行実行するジョブがグローバルなxcode-selectを変更しない。

これらの手順を実施すれば、クラウドMacのツールチェーン状態をログから追跡できるようになります。ビルド結果に差異が生じた場合も、まずバージョンドリフトを除外し、その後で依存関係、コード、ジョブ設定を調査できます。複数の変数を相手に、手当たり次第の試行錯誤をする必要はありません。

よくある質問

異なるXcodeを使うCIジョブを同時に実行できますか?

可能です。各ジョブ内でDEVELOPER_DIRを個別に設定し、実行中にシステム全体のxcode-selectを変更しない構成にします。

xcodebuild -versionの確認だけで十分ですか?

不十分です。SDK、Swift、DEVELOPER_DIRに加え、xcrunが返す実行ファイルのパスも確認すると、ツールチェーンの混在を検出できます。

新しいXcodeへ安全に移行する手順は何ですか?

バージョン付きの名前で追加し、初回準備とプロジェクトの事前検査を実行します。旧版を残したままビルドとテストを確認してから既定値を変更します。

専有のMac mini物理ノードが必要ですか?

利用可能な構成やノード、固定の課金期間を確認して、記事の手順を継続的に使えるクラウドMac環境で実践しましょう。

Mac miniを今すぐレンタル