エンジニアリングノート

並列 Xcode ビルドの DerivedData ロック競合を解消する

並列 Xcode ビルドの DerivedData ロック競合を解消する

同じクラウド Mac で 2 本の Xcode パイプラインを同時に実行した際、断続的に発生する database is locked、インデックスデータベースの破損、あるいは「ローカルでは再現しない」リンクエラーは、コード変更が原因ではないことが少なくありません。まず、2 つのジョブがデフォルトの ~/Library/Developer/Xcode/DerivedData を共有していないか確認してください。このディレクトリには単なるキャッシュだけでなく、ビルドデータベース、インデックス、モジュールキャッシュ、ジョブによって継続的に書き換えられる中間成果物も格納されています。同時に読み書きできる共有キャッシュとして扱えば、いずれ失敗するのは避けられません。

まず競合の発生箇所を特定する

失敗を確認しても、すぐにすべてのキャッシュを削除してはいけません。最初に、失敗したジョブの完全な xcodebuild コマンド、開始時刻、作業ディレクトリ、プロセス一覧を保存します。そのうえで、ログから lockeddatabaseunable to attachmalformed、重複した成果物に関するメッセージを検索します。

pgrep -alf 'xcodebuild|XCBuildService|swift-frontend'
find "$HOME/Library/Developer/Xcode/DerivedData" -name build.db -print
grep -Eini 'locked|database|malformed|multiple commands produce' build.log

2 つのジョブで -derivedDataPath が同じ場合、またはどちらもこのパラメータを明示的に指定していない場合は、最優先で対処すべきリスクが見つかったことになります。パイプラインのタイムアウト時に外側のスクリプトだけが終了し、xcodebuild やコンパイルの子プロセスが残ってディレクトリへの書き込みを続けていないかも確認してください。

リトライが成功しても、問題が解決した証拠にはなりません。競合が起きる時間帯が短くなり、2 回目のビルドが偶然成功することはありますが、書き込み先を共有する関係は残ったままです。

ロック競合と通常のコンパイルエラーを区別する

ソースコードのコンパイルエラーは、通常、同じファイルの同じ行で安定して発生します。一方、ロック競合は並列処理のタイミングによって変化しやすく、依存関係の解析、モジュール生成、リンクの各段階で不規則に現れます。並列ジョブを停止すると連続して成功し、並列実行を再開すると失敗する場合は、アプリケーションコードを変更する前にディレクトリの所有関係を監査するべきです。

ジョブごとに独立したワークスペースを割り当てる

確実な方法は、リポジトリのチェックアウト先、DerivedData、結果バンドル、一時ディレクトリのすべてにジョブ固有の識別子を含めることです。識別子にはパイプラインのジョブ番号を使用でき、ローカル検証時にはプロセス番号で代用できます。

set -euo pipefail

JOB_KEY="${CI_JOB_ID:-local-$$}"
ROOT="${RUNNER_TEMP:-$HOME/ci-work}/$JOB_KEY"
DERIVED_DATA="$ROOT/DerivedData"
RESULT_BUNDLE="$ROOT/TestResults.xcresult"

mkdir -p "$DERIVED_DATA"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -derivedDataPath "$DERIVED_DATA" \
  -resultBundlePath "$RESULT_BUNDLE" \
  build-for-testing

test -d "$RESULT_BUNDLE"

パスはそのジョブ自身が作成し、削除もそのジョブだけが行う必要があります。プロジェクト名だけを一意なディレクトリ名として使用してはいけません。同じプロジェクトの異なる 2 つのブランチでも衝突するためです。クリーンアップスクリプトでも、名前に App を含む DerivedData をすべて削除するような曖昧なマッチングは避けてください。

依存関係の解決とコンパイルを分離する

DerivedData を分離しても、複数のジョブが同じ依存関係のチェックアウトディレクトリを同時に変更していると、問題はパッケージ解決の段階へ移ります。先に依存関係を解決し、その後の正式なビルドでは依存関係のバージョンが自動的に変更されないようにします。レビュー済みのロックファイルをリポジトリにコミットし、パイプラインが暗黙に更新しないようにしなければなりません。

PACKAGES="$ROOT/SourcePackages"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$PACKAGES"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -derivedDataPath "$DERIVED_DATA" \
  -clonedSourcePackagesDirPath "$PACKAGES" \
  -disableAutomaticPackageResolution \
  build

キャッシュが必要な場合は、複数のジョブに同じ作業中のディレクトリを書き換えさせるのではなく、ダウンロード結果または検証済みの不変スナップショットをキャッシュしてください。キャッシュキーには少なくとも、ロックファイルのダイジェスト、Xcode のバージョン、ターゲットアーキテクチャを含めます。キャッシュがヒットした場合も、ロックファイルが変更されていないことを検証する必要があります。

判定可能な受け入れチェック表を作る

修正の成否を 1 回の成功だけで判断してはいけません。同じコミットから複数回のジョブを同時に開始し、ディレクトリ、プロセス、成果物の境界を確認します。

| 確認項目 | 合格条件 | 失敗時の対応 | |---|---|---| | DerivedData のパス | 並列ジョブごとに一意 | ジョブ識別子とディレクトリの組み立てを修正 | | 依存関係のロックファイル | ビルド前後でダイジェストが一致 | 自動解決を禁止し、スクリプトを確認 | | 残存プロセス | ジョブ終了後にそのジョブのコンパイルプロセスが残っていない | タイムアウトと終了シグナルの処理を修正 | | 結果バンドル | ジョブごとに独立して生成され、読み取り可能 | `resultBundlePath` を分離 | | クリーンアップ範囲 | 現在のジョブのルートディレクトリだけを削除 | ワイルドカードと共有ディレクトリの削除を廃止 |

少なくとも、まったく同じビルドを 2 本同時に実行し、その後で異なるブランチのビルドも 1 組実行することを推奨します。前者では書き込み先の分離を検証し、後者では作業ディレクトリ、モジュールキャッシュ、成果物のパスが依然としてプロジェクト名を基準に共有されている問題を検出します。

証拠を残して安全に回収する

失敗したジョブの現場をすぐに破棄してはいけません。まず、ビルドログ、結果バンドル、Xcode のバージョン、ロックファイルのダイジェスト、ディスクの空き容量、ジョブのパスをアーカイブします。ソースコード、環境変数、ログに含まれるトークンは、事前にマスキングしなければなりません。成功したジョブは、成果物が独立したリリースディレクトリへコピーされたことを確認してからクリーンアップできます。

クリーンアップ処理はジョブのルートディレクトリに紐付け、パスのプレフィックス検証も追加するのが安全です。ジョブが強制終了された場合、後続の回収処理ではジョブ識別子に基づいて残存ディレクトリを処理し、マシン全体の開発者用ディレクトリを空にしてはいけません。NUMACS で複数のパイプラインを実行する場合も、コンソールで現在選択できる構成を確認し、並列数に応じてジョブ数の上限を設定してください。並列度を上げても、書き込み先の分離の代わりにはなりません。

最終的な目標は、エラーの発生を「減らす」ことではなく、明確な不変条件を確立することです。各並列ジョブの書き込み主体は 1 つだけであり、ビルド中は依存関係のバージョンが変化せず、失敗時の状況を追跡でき、クリーンアップがほかのジョブへ影響しないこと。この 4 点を満たして初めて、DerivedData は不規則な障害の原因ではなく、管理可能なビルドデータへ戻ります。

よくある質問

並列ジョブごとに DerivedData を分ける必要がありますか?

必要です。同時に書き込むジョブには -derivedDataPath で固有のディレクトリを割り当て、同じパスの再利用は直列実行に限定します。

分離するとビルドキャッシュの効果は失われますか?

失われません。依存パッケージのダウンロードや検証済みの読み取り専用成果物を別層で再利用し、書き込み対象だけを分離できます。

database is locked は再実行だけで対処できますか?

再実行は競合を隠すだけです。共有書き込み先を廃止し、残留している xcodebuild プロセスも確認する必要があります。

Numacs クラウドMac

ビルド作業を専用の物理ワークステーションへ

2種類の Apple Silicon 構成を、専用の物理マシンとして提供します。仮想マシンではなく、日単位、週単位、月単位、四半期単位でレンタルできます。シンガポール、東京、ソウル、香港のノードに対応しています。

デバイスを選んで注文