同じコミットがローカルでは成功するのに、常時稼働しているクラウドMacへ移すと module file was created by a different version of the compiler が断続的に発生し、再実行すると成功することがあります。こうした障害は通常、Swiftソースがランダムに変化したためではありません。異なるツールチェーンやSDK、並列タスクが同じプリコンパイル済みモジュールキャッシュを参照したことが原因です。DerivedDataをすべて削除すれば一時的には解消できますが、最も重要な調査材料まで失われ、次の汚染も防げません。
まずモジュールキャッシュ障害か確認する
ClangはシステムヘッダーとObjective-CモジュールをPCMにコンパイルし、Swiftコンパイラも独自のモジュールキャッシュを管理します。キャッシュの生成時と現在のタスクで、コンパイラ、SDK、アーキテクチャ、またはコンパイルオプションが異なると、読み込み時に失敗する可能性があります。
まず完全なログから、次の特徴を検索します。
| ログの特徴 | 可能性が高い原因 | 最初に確認する項目 |
|---|---|---|
different version of the compiler |
Xcodeが切り替わった | xcodebuild -version |
PCM file is out of date |
SDKまたはヘッダーのタイムスタンプが変化した | 現在のSDKパス |
could not build module |
上流モジュールが破損している | 最初に失敗したモジュール |
| 同じタスクを再実行すると成功する | 並列書き込みまたは共有キャッシュ | キャッシュディレクトリの所有者 |
最後のエラーだけを切り出してはいけません。モジュールの読み込みに失敗すると数十件の連鎖エラーが発生することがあるため、ログをさかのぼり、module、.pcm、または ModuleCache が最初に現れる箇所を特定してください。
キャッシュを消去する前に、失敗ログ、Xcodeのビルド番号、実際に適用されたビルド設定を保存してください。これらの情報がなければ、復旧できても、原因がツールチェーンのずれ、並行処理による汚染、キャッシュ自体の破損のどれだったのか判断できません。
失敗タスクの比較可能な情報を保存する
VMKeepのクラウドMacでは、スクリプトが使用する想定のバージョンだけを見るのではなく、コマンドによって実際に解決されたDeveloperディレクトリを最初に記録します。
set -o pipefail
mkdir -p artifacts/diagnostics
xcode-select -p | tee artifacts/diagnostics/developer-dir.txt
xcodebuild -version | tee artifacts/diagnostics/xcode-version.txt
xcrun --sdk iphoneos --show-sdk-path | tee artifacts/diagnostics/sdk-path.txt
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-showBuildSettings \
> artifacts/diagnostics/build-settings.txt
続いて、失敗したコマンドの SDKROOT、ARCHS、DEVELOPER_DIR、CLANG_MODULE_CACHE_PATH、SWIFT_MODULE_CACHE_PATH を確認します。タスクが sudo、リモートセッション、またはスケジューラープロセスから起動される場合は、実行ユーザーと HOME も比較してください。同じスクリプトでも実行ユーザーが変われば、デフォルトのキャッシュ保存先も変わります。
ツールチェーンの入口を固定する
ビルドスクリプトでは DEVELOPER_DIR を明示的に設定し、実際のコンパイルを始める前に xcodebuild -version を一度実行する必要があります。バージョンが想定と異なる場合は即座に終了し、誤ったツールチェーンで共有キャッシュへ書き込ませてはいけません。Xcodeをアップグレードした後は、古いディレクトリを使い続けるのではなく、新しいキャッシュキーを生成してください。
キャッシュの境界を単一タスクまで狭める
信頼できるディレクトリキーには、少なくともXcodeのビルド番号、SDK、アーキテクチャ、タスク識別子を含めます。ブランチ名をそのままパスにしてはいけません。先に特殊文字を変換する必要があります。次の方法では、タスクごとに独立したDerivedDataとモジュールキャッシュを用意し、依存関係のダウンロード先は別の場所に保持します。
set -euo pipefail
JOB_KEY="$(printf '%s' "${CI_JOB_ID:-local}" | tr -cs 'A-Za-z0-9._-' '-')"
XCODE_BUILD="$(xcodebuild -version | awk '/Build version/{print $3}')"
SDK_BUILD="$(xcrun --sdk iphoneos --show-sdk-build-version)"
CACHE_KEY="${XCODE_BUILD}-${SDK_BUILD}-arm64-${JOB_KEY}"
ROOT="$HOME/BuildCaches/$CACHE_KEY"
DERIVED="$ROOT/DerivedData"
MODULES="$ROOT/Modules"
PACKAGES="$HOME/BuildCaches/SourcePackages"
mkdir -p "$DERIVED" "$MODULES/clang" "$MODULES/swift" "$PACKAGES"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination 'generic/platform=iOS' \
-derivedDataPath "$DERIVED" \
-clonedSourcePackagesDirPath "$PACKAGES" \
CLANG_MODULE_CACHE_PATH="$MODULES/clang" \
SWIFT_MODULE_CACHE_PATH="$MODULES/swift" \
build
長期運用するブランチは固有の安定したキーを再利用できますが、一時的なマージリクエストはタスク単位で分離する必要があります。実行中の2つの xcodebuild プロセスに同じモジュールディレクトリを書き込ませてはいけません。また、モジュールキャッシュを異なるツールチェーン間で復元できる汎用アーカイブとして扱わないでください。
一括初期化ではなく対象を絞って復旧する
モジュールキャッシュの障害だと確認できたら、まず対象ディレクトリを使用しているビルドプロセスを停止し、影響範囲の小さいものから順に対処します。
- 現在のタスクの
Modules/clangとModules/swiftを削除します。 SourcePackagesは残し、依存関係の解決とビルドを再実行します。- インデックスまたは中間生成物が古いモジュールを参照し続けている場合は、現在のタスクのDerivedDataも削除します。
- 1つのサードパーティーモジュールだけが失敗する場合は、キャッシュ削除を繰り返して互換性の問題を隠すのではなく、そのバイナリ生成物が現在のアーキテクチャとSDKに対応しているか確認します。
削除処理の対象は、算出したタスクディレクトリ内に限定しなければなりません。rm -rf ~/Library/Developer/Xcode/DerivedData/* を使用すると他のタスクにも同時に影響し、局所的な障害をマシン全体のコールドスタートへ拡大させるおそれがあります。
使用中のディレクトリを誤って削除しない
古いキャッシュを回収するときは、タスクロックまたは実行記録を使ってディレクトリが使用中でないことを確認し、最終使用日時に基づいて削除します。ディレクトリの作成日時だけで判断してはいけません。安定ブランチでは、以前に作成したキャッシュを継続的に再利用する場合があるためです。削除スクリプトでは、対象パスが想定したルートディレクトリ配下にあることも検証し、変数が空の場合やパスの算出に失敗した場合は即座に終了する必要があります。
キャッシュ整合性をビルドのゲート条件にする
最終的には、パイプラインが構成のずれを能動的に検出できるようにします。ビルドごとにXcodeのビルド番号、SDKのビルド番号、アーキテクチャ、キャッシュキー、実行ユーザーを保存し、タスク終了後に失敗ログと一緒にアーカイブしてください。ツールチェーンをアップグレードするときは新しい名前空間を作成し、新しい経路が安定したことを確認してから古いキャッシュを回収します。
モジュールキャッシュを復元しない検証タスクを別途用意することもできます。通常のタスクが失敗し、クリーンなタスクが成功する場合、問題はキャッシュ層に集中しています。両方とも失敗する場合は、ソースコード、依存関係、またはビルド設定の調査に戻るべきです。これにより、すべてのエラーをキャッシュのせいにすることも、成功するまで運任せで再実行することも避けられます。
モジュールキャッシュは、本質的にはコンパイラ入力の一部です。バージョン、ディレクトリの所有権、並行処理の境界をスクリプトに明記すれば、PCMの無効化はランダムな障害ではなく、原因を特定して復旧できるエンジニアリング上の事象になります。
よくある質問
PCMエラーではDerivedData全体を削除する必要がありますか?
最初から全削除する必要はありません。ログとビルド設定を保存し、対象ジョブのModuleCache.noindexだけを削除します。インデックスと生成物も不整合な場合に限り、専用DerivedDataを作り直します。
異なるXcodeバージョンでモジュールキャッシュを共有できますか?
共有は推奨しません。キャッシュキーにXcodeのビルド番号、SDK、アーキテクチャ、ジョブ識別子を含め、互換性のないPCMが再利用されないようにします。
クラウドMacでこのワークフローの検証を続ける
3種類のApple Silicon構成から、最適なメモリ、ストレージ、ノード、利用期間を選べます。