VMArm 技術ノート

クラウドMacでMetroとWatchmanのキャッシュを分離する

クラウドMacでMetroとWatchmanのキャッシュを分離する

同じクラウドMacでReact Nativeのビルドを繰り返していると、最も原因を特定しにくいのはコンパイルエラーではなく、「コードを変更したのに、生成物へ反映されていないように見える」問題です。ローカルで再実行すると正常なのに、継続的インテグレーションのジョブでは古いモジュールが参照されることがあります。リポジトリ全体を削除すると一時的に解消しても、数日後に再発します。多くの場合、Gitワークツリー、Watchmanの監視、Metroの変換キャッシュ、XcodeのDerivedDataという4層の状態を分離できていないことが根本原因です。

キャッシュ汚染が発生した層を特定する

異常が発生するたびに、すべてのキャッシュを削除してはいけません。全面的なクリーンアップは障害の発生源を見えにくくするうえ、その後のビルドも遅くします。まず、開発用パッケージにのみ含まれる文字列を変更するなど、最小限の変更で影響範囲を確認します。そのうえで、ファイルの内容、Metroのモジュールグラフ、最終的なアプリ生成物を個別に検証します。

ジョブの証跡を記録する

各ジョブでは少なくとも、コミットハッシュ、ワークスペースの絶対パス、NodeとXcodeのバージョン、Metroの起動コマンド、DerivedDataのパスを記録します。さらに、次のコマンドの出力も保存してください。

set -euo pipefail

printf 'commit=%s\n' "$(git rev-parse HEAD)"
printf 'workspace=%s\n' "$PWD"
node --version
xcodebuild -version
watchman watch-list || true
find "$PWD" -maxdepth 2 -name metro.config.js -print

コミットとソースファイルが正しいにもかかわらずJavaScriptバンドルが古い場合は、まずMetroを確認します。アーカイブ内のネイティブコードが更新されていない場合は、DerivedData、ビルド設定、実際に開いているワークスペースを確認してください。Watchmanでエラーが発生している場合や、親ディレクトリまで監視対象になっている場合は、先に監視範囲を修正します。

クリーンアップは修復手段であり、診断結果ではありません。どの層で問題が発生したかを特定してから、その層の状態だけを削除してください。

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

並列ジョブで同じチェックアウトディレクトリを使ってブランチを切り替えてはいけません。また、Watchmanが監視する同一の親ディレクトリ配下に複数のジョブを配置することも避けてください。パスには、/Users/ci/work/4821/ios-releaseのようにパイプライン番号とジョブ番号を含めることを推奨します。ジョブ終了後は、ディレクトリ全体を個別にアーカイブまたは削除できます。

ワークスペースには、参照先が頻繁に切り替わるシンボリックリンクではなく、実体のあるディレクトリを使用します。一部のスクリプトでは固定パスを「現在のビルド」へ向けていますが、Watchmanやツールチェーンが解決済みの古いパスを保持することがあります。その結果、コマンド上は新しいディレクトリへ移動しているように見えても、監視先は以前のチェックアウトのままになります。

JOB_ROOT="/Users/ci/work/${PIPELINE_ID}/${JOB_ID}"
WORKSPACE="${JOB_ROOT}/repo"
DERIVED_DATA="${JOB_ROOT}/DerivedData"
METRO_CACHE="${JOB_ROOT}/metro-cache"

mkdir -p "$WORKSPACE" "$DERIVED_DATA" "$METRO_CACHE"
export TMPDIR="${JOB_ROOT}/tmp/"
mkdir -p "$TMPDIR"

ディレクトリ名には安定したASCII文字のみを使用し、空白や動的なシンボリックリンクは避けます。ジョブごとにルートディレクトリを分けることで、ディスク使用量やクリーンアップの責任範囲も監査しやすくなります。

Watchmanの監視範囲を限定する

ワークスペースへ移動した後にwatchman watch-project "$WORKSPACE"を実行し、返されたwatchが想定したプロジェクトルートと一致していることを確認します。共有の親ディレクトリまで監視範囲が引き上げられる場合は、通常、リポジトリの境界が明確でないか、親ディレクトリに境界判定へ影響する設定があります。

リポジトリのルートに.watchmanconfigを配置し、JavaScriptの依存関係グラフに含まれない大容量ディレクトリを除外します。

{
  "ignore_dirs": [
    ".git",
    "DerivedData",
    "build",
    "artifacts"
  ]
}

このファイルを変更した後は、現在のプロジェクトに対する監視を再登録します。並列ジョブが稼働するマシンでwatchman watch-del-allを常用してはいけません。他のジョブの監視まで削除されるためです。安全な方法は、各ワークスペースを独立した監視ルートにし、ジョブ終了時に次のコマンドだけを実行することです。

watchman watch-del "$WORKSPACE" || true

このコマンドで対象が監視ルートではないと表示された場合は、推測したパスを一括削除せず、先にwatchman watch-listを確認してください。

MetroとXcodeのキャッシュを明示的に管理する

Metroのキャッシュはジョブまたはコードの基準状態にひも付け、システムの一時ディレクトリに残った出所不明の履歴ファイルへ依存しないようにします。起動スクリプトではキャッシュの保存先を明示的な引数として渡してください。具体的な引数名はMetroのバージョンによって異なる可能性があるため、プロジェクトで固定しているバージョンのヘルプ出力を基準にします。コマンド引数とプロジェクト設定のどちらを使う場合でも、最終的な目的は、複数のジョブでデフォルトディレクトリを共有せず、キャッシュを${METRO_CACHE}へ保存することです。

--reset-cacheは、変換キャッシュの破損を確認した後の一時的な復旧にのみ使用します。すべてのジョブで毎回リセットする必要があるなら、分離設計がまだ不十分です。Xcodeについても、DerivedDataの保存先をジョブのディレクトリへ固定します。

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$DERIVED_DATA" \
  build

Pods、Swiftパッケージ、JavaScript依存関係では、検証済みのダウンロードキャッシュを利用できます。ただし、「ダウンロードキャッシュ」と「ビルド状態」は分離する必要があります。前者はロックファイルのキーに基づいて再利用できますが、後者は単一のジョブが専有すべきです。キャッシュを復元した後は、少なくともロックファイルのハッシュを照合し、ブランチ名だけでキャッシュをヒットさせないでください。

一括リセットではなく層ごとにクリーンアップする

異常が発生した場合は、影響が最も小さい層から対処します。まず現在のMetroプロセスを停止し、そのジョブのMetroキャッシュだけを削除して再起動します。それでも解消しなければ、現在のワークスペースに対するWatchmanの監視を削除して再登録します。ネイティブコードのコンパイル結果に不整合がある場合に限り、そのジョブのDerivedDataを削除します。リポジトリの再チェックアウトを検討するのは最後です。

クリーンアップ前に、対象ディレクトリを使用中のプロセスが残っていないことを確認します。

pgrep -af "$WORKSPACE" || true
lsof +D "$JOB_ROOT" 2>/dev/null | head -n 50 || true

ジョブの終了スクリプトでは、ワークスペースのパスが想定したルートディレクトリ配下にあることも検証し、変数が空の場合にユーザーディレクトリを誤って削除しないようにします。caseを使用してパスのプレフィックスを許可リストで確認してから、rm -rf -- "$JOB_ROOT"を実行できます。

検証時には、同じコミットを続けて2回ビルドします。2回目はダウンロードキャッシュを再利用できますが、ワークスペース、Metroの状態、DerivedDataについては引き続きジョブ単位の境界を維持してください。2回の生成物が異なる場合は、すぐにクリーンアップ範囲を広げるのではなく、生成ファイル、環境変数、ビルドログを先に比較します。このような手順を整備すれば、一度のキャッシュ問題を修復するだけでなく、次に差異が発生した際に原因を説明できる十分な証跡も残せます。

よくある質問

すべてのビルドでMetroのreset-cacheが必要ですか?

必要ありません。通常はジョブごとに専用キャッシュを用意し、モジュールグラフや変換キャッシュの破損を確認した場合だけリセットします。

複数ジョブで同じWatchman監視ルートを共有できますか?

推奨しません。入れ子にならない作業領域を用意し、監視ルートを個別に登録して、終了したジョブの監視だけを削除します。

専用物理Macノード

次回のXcodeビルドに、365日稼働のクラウドMacを選択

チップ、メモリ、ストレージ、請求期間、ノードのリージョンを確認して、設定に進みます。実際の利用可能状況はコンソールのリアルタイム表示をご確認ください。

クラウドMacの構成を選択