エンジニアリングガイド

リモートクラウドMacのXcodeビルドキャッシュ最適化

リモートクラウドMacのXcodeビルドキャッシュ最適化

リリースの2日前に急きょリモートMacを1台追加。コードの取得は数分で終わったのに、初回のXcodeビルドはなかなか完了しない。2回目は速くなったものの、ブランチを切り替えると古いリソースが残ったり、インデックスに異常が出たり、依存関係の解決が再実行されたりする。このような問題は、マシン性能よりも、DerivedData、Swiftパッケージのダウンロード、ビルド結果がデフォルトのディレクトリに混在していることに起因する場合が多い。これではキャッシュがヒットしたかどうかを判断できず、利用期間が終了する前に必要なデータだけを選んで移行することも難しい。

比較可能なビルド基準を定める

キャッシュを最適化する前に、まず条件を固定する。xcodebuild -version、macOSのバージョン、コミットハッシュ、Scheme、ビルド構成、ターゲットプラットフォームを記録する。検証中に依存関係の更新、Xcodeの切り替え、コンパイルオプションの変更を同時に行うと、ビルド時間を正しく比較できない。

少なくとも、次の3パターンを検証する。

  1. プロジェクト専用のDerivedDataを削除してから、コールドビルドを実行する。
  2. コードを変更せず、同じコマンドでウォームビルドを実行する。
  3. 通常のSwiftファイルを1つ変更し、増分ビルドを実行する。

各ビルドについて、開始時刻、終了時刻、終了コード、結果バンドルを保存する。GUIの進捗バーは状況の把握には役立つが、厳密な比較には向かない。自動記録には一貫してxcodebuildを使用する。

キャッシュの目的は、特定の1回だけ最速に見せることではない。同じ入力に対して、予測可能な所要時間と結果を得られるようにすることである。

キャッシュを役割ごとに分離する

作業ディレクトリには、ソースコード、DerivedData、Swiftパッケージのダウンロード、結果とログの4種類のパスを用意することを推奨する。複数のプロジェクトで同じDerivedDataを共有せず、DebugとReleaseも互いに上書きしないようにする。プロジェクト数が多い場合は、Xcodeのメジャーバージョンやコミット、ブランチを示す識別子も追加できる。

ディレクトリ 保存内容 推奨方針
Source Gitワークツリー プロジェクトごとに分離
DerivedData インデックスと中間生成物 バージョン、Scheme、構成ごとに分離
SourcePackages Swiftパッケージのダウンロードとチェックアウト ロックファイルが変わっていない場合に再利用
Results 結果バンドルとログ ビルド時刻ごとに保管

Swiftパッケージ用ディレクトリは再利用できるが、依存関係のロックファイルが一致していることが前提となる。ロックファイルが変わった場合は、まず依存関係の解決処理を正常に完了させる。古いチェックアウトディレクトリを新しいプロジェクトへ直接コピーしてはならない。DerivedDataはツールチェーンの影響を受けやすいため、Xcodeを切り替えたら既存ディレクトリを上書きして使い続けず、新しいディレクトリを作成する。

固定したコマンドでビルドする

次のスクリプトでは、主要なパスを明示している。ワークスペース、Scheme、ターゲットプラットフォームは、実際のプロジェクトに合わせて置き換えること。結果バンドルのパスは実行前に存在していてはならないため、スクリプト内で同名のディレクトリを先に削除する。

#!/bin/bash
set -euo pipefail

ROOT="${HOME}/BuildWorkspace"
SCHEME="Application"
CONFIGURATION="Release"
DERIVED="${ROOT}/DerivedData/${SCHEME}-${CONFIGURATION}"
PACKAGES="${ROOT}/SourcePackages"
RESULT="${ROOT}/Results/${SCHEME}.xcresult"
LOG="${ROOT}/Results/${SCHEME}.log"

mkdir -p "${DERIVED}" "${PACKAGES}" "${ROOT}/Results"
rm -rf "${RESULT}"

xcodebuild \
  -workspace "${ROOT}/Source/Application.xcworkspace" \
  -scheme "${SCHEME}" \
  -configuration "${CONFIGURATION}" \
  -destination "generic/platform=iOS" \
  -derivedDataPath "${DERIVED}" \
  -clonedSourcePackagesDirPath "${PACKAGES}" \
  -resultBundlePath "${RESULT}" \
  build | tee "${LOG}"

最初にターミナルでxcodebuild -listを実行し、対象のSchemeがワークスペースから実際に参照できることを確認する。ワークスペースではなくプロジェクトファイルを使用する場合は、-workspace-projectに変更する。両方を同時に指定してはならない。

キャッシュディレクトリ用の安定した識別子を生成する

キャッシュキーには、少なくともXcodeのバージョン、依存関係ロックファイルのダイジェスト、Scheme、ビルド構成を含める。ブランチ名を追加してもよいが、ブランチ名だけをキーにしてはならない。同じブランチでも依存関係が変更されている可能性があるためだ。

XCODE_KEY="$(xcodebuild -version | shasum -a 256 | cut -c1-12)"
PACKAGE_KEY="$(shasum -a 256 Package.resolved | cut -c1-12)"
printf '%s-%s-%s-%s
' "${XCODE_KEY}" "${PACKAGE_KEY}" "${SCHEME}" "${CONFIGURATION}"

ワークスペース内に複数のPackage.resolvedがある場合は、実際のビルドで使用されるファイルを明示的に選択する。ロックファイルが見つからないときに固定の空キーを生成してはならない。依存関係の状態が異なるビルドが、同じディレクトリに格納されてしまう。

フェーズごとに不要な再ビルドを特定する

ウォームビルドでも時間がかかる場合は、まずどのフェーズに時間を要しているかを確認する。依存関係の解決が繰り返されるなら、ロックファイルがスクリプトによって書き換えられていないか、パッケージディレクトリの権限が正しいかを調べる。コンパイルが毎回実行されるなら、コンパイル条件、自動生成ファイル、ビルドフェーズのスクリプトを確認する。リンク時に異常がある場合は、バージョン情報や埋め込みコンテンツが毎回変更されていないかを確認する。

特に多いのは、スクリプトフェーズで入力と出力が宣言されていないケースだ。スクリプトが実行のたびに生成ファイルのタイムスタンプを書き換えると、その後続ターゲットは再ビルドが必要だと判定される。対策として、内容が変わった場合にのみファイルを置き換えるようスクリプトを修正し、Xcodeのビルドフェーズに正確な入力パスと出力パスを指定する。

「すべてのキャッシュを消去する」ことを定型手順にしてはならない。それで分かるのはキャッシュが汚染されていたかどうかだけであり、汚染源までは特定できない。まずプロジェクトのDerivedDataだけを別の場所へ移動し、それでも問題が続く場合にSwiftパッケージ用ディレクトリを処理する。この順序なら診断に必要な証拠を残せるうえ、毎回すべての依存関係をダウンロードし直す必要もない。

利用期間終了前に本当に必要なデータを保存する

データを移行する前に、実行中のビルドを停止し、ログの書き込みが完了していることを確認する。そのうえで、依存関係のロックファイル、必要なSwiftパッケージのダウンロード、結果バンドル、プロジェクト用スクリプトをまとめる。ソースコードはリポジトリへコミット済みの内容を正とし、ワークツリーに残っている未コミットの変更は個別に確認する。

DerivedData全体を長期間保存することは推奨しない。通常は容量が大きく、インデックス、オブジェクトファイル、特定のツールチェーンに依存する中間生成物が含まれている。次回異なるバージョンのXcodeを使う場合は、再生成したほうが信頼性が高い。機密性の高い証明書、秘密鍵、トークン、一時的な環境ファイルをキャッシュアーカイブへ混在させてはならない。Shellの履歴、一時キーチェーン、プロジェクトが生成した認証情報ファイルを先に削除し、その後でエクスポート対象の一覧を確認する。

最後に、ツールチェーンのバージョン、キャッシュキー、最後に成功したコミット、ビルドコマンド、エクスポート済みのパス、明示的にエクスポートしなかったパスを簡潔に記録しておく。次のクラウドMacでは、この記録に沿って復元したほうが、ホームディレクトリ全体をコピーするよりも環境を再現しやすい。

よくある質問

異なるXcodeバージョンでDerivedDataを共有できますか?

共有しない方が安全です。Xcodeバージョン、プロジェクト、Scheme、構成ごとに分け、ツールチェーン変更後はDerivedDataを作り直してください。

利用終了前に書き出す価値が高いキャッシュは何ですか?

依存関係のロックファイル、再利用可能なSwiftパッケージのダウンロード、結果バンドル、診断ログを優先します。オブジェクトファイルと索引は新しい環境で再生成する方が確実です。

ビルドが遅いときは全キャッシュを削除すべきですか?

キャッシュ破損の切り分けには使えますが、常用すべき方法ではありません。依存関係の解決、コンパイル、リンク、テストを個別に計測し、該当するディレクトリだけを整理します。

ArmMacs Cloud Mac

プロジェクト期間に合わせて専用物理ノードを利用

固定チップ、メモリ、ストレージ、利用期間、販売中のノードを選択し、在庫がある場合は提供手続きに進みます。

機種を選んで注文