開発タスクメモ

クラウドMac CIのファイル時刻ずれを調査する

クラウドMac CIのファイル時刻ずれを調査する

コードを変更していないにもかかわらず、クラウドMacで2回目のXcodeビルドを実行すると、多数のターゲットが再びコンパイルされることがあります。同じコミットでも、新しいワークスペースに切り替えると正常な増分ビルドに戻ります。この種の問題は、必ずしもDerivedDataが原因とは限りません。ソースコード、生成ファイル、キャッシュ内の成果物に誤った更新時刻が付いている可能性もあります。キャッシュを削除すれば症状は一時的に解消できますが、同期や復元の手順を変えなければ、時刻のずれは再びワークスペースに入り込みます。

原因がタイムスタンプのずれか確認する

まず、コミット、Scheme、Xcodeのパス、ビルドパラメータを固定し、同じビルドを2回続けて実行します。1回目は中間成果物をすべて生成させ、2回目にどのタスクが引き続き実行されるかを確認します。同じSwiftコンパイル、リソース処理、スクリプトフェーズが繰り返し実行される場合は、クリーンなワークスペースと再利用したワークスペースの結果を比較します。

調査時は、ビルド全体の所要時間だけを見てはいけません。繰り返し実行されるタスクの入力パス、スクリプトの出力、生成先ディレクトリを重点的に記録し、ビルド開始前にファイルを書き換えているツールがないか確認します。代表的な兆候は次のとおりです。

増分ビルドが依存しているのは、ファイルの内容だけではありません。入力と出力の時系列関係が崩れると、ビルドシステムは完了済みのタスクを繰り返し実行が必要だと判断する可能性があります。

未来時刻のファイルと異常なディレクトリをスキャンする

最初にシステム時刻とタイムゾーンを確認し、現在時刻より5分以上先の時刻が設定されたファイルをスキャンします。5分の許容範囲を設けることで、ごく短い計測誤差を除外しながら、明確な時刻のずれを検出できます。

#!/bin/zsh
set -euo pipefail

root="${1:-$PWD}"
limit=$(( $(date +%s) + 300 ))

find "$root" -type f -print0 |
while IFS= read -r -d '' file; do
  modified=$(stat -f '%m' "$file")
  if (( modified > limit )); then
    printf '%s\t%s\n' \
      "$(date -r "$modified" '+%Y-%m-%d %H:%M:%S %z')" \
      "$file"
  fi
done

スキャン範囲には、少なくともソースコード、プロジェクトファイル、スクリプト、リソース、コード生成ディレクトリ、復元後のキャッシュを含めます。最初からユーザーディレクトリ全体をスキャンすると、パッケージマネージャーのキャッシュ、ログ、システムファイルによって大量のノイズが生じるため避けてください。検出された各ファイルに対して stat -x 文件路径 も実行し、更新時刻、変更時刻、所属するボリュームを確認します。

異常が1つのディレクトリに集中している場合は、通常、特定のダウンロード、展開、同期、コード生成のいずれかの処理まで原因を絞り込めます。リポジトリ全体に分散している場合は、ワークスペースのコピー方法と、ジョブ開始前に実行される初期化スクリプトを確認します。

発生源に応じて修復方法を選ぶ

原因ごとに必要な対処は異なります。すべてを一律に touch で覆い隠してはいけません。

現象 主な発生源 推奨される対処
少数のソースファイルが未来時刻になっている アーカイブまたは同期元の異常 ファイルを再取得し、取得元マシンの時刻を確認する
生成ファイルの時刻が実行のたびに更新される ジェネレーターが無条件に上書きしている 内容が変わった場合にのみアトミックに置き換える
キャッシュディレクトリ内の時刻関係が乱れている 復元時に誤ったメタデータが保持された そのキャッシュ一式を破棄し、キャッシュキーを再構築する
ワークスペース全体の時刻がコピー時刻に近い コピーオプションによってメタデータが変更された チェックアウト方法を統一し、複数の同期方式を混在させない
出力が入力より古い状態のまま変化しない キャッシュアーカイブ内の時刻が誤っている キャッシュを再生成し、マニフェストを検証する

Gitで管理しているソースコードの場合、通常はリポジトリ全体の時刻を書き換えるのではなく、未コミットの変更がないことを確認してから、影響を受けたディレクトリを再チェックアウトする方法が最も安全です。生成ファイルについては、ジェネレーターから一時ファイルへ先に書き出し、内容を比較したうえで置き換えます。

generate_config > Config.generated.swift.tmp
if ! cmp -s Config.generated.swift.tmp Config.generated.swift; then
  mv Config.generated.swift.tmp Config.generated.swift
else
  rm Config.generated.swift.tmp
fi

この方法なら、内容が変わっていない場合は元ファイルの更新時刻も変化せず、そのファイルに依存するコンパイルタスクが無意味に起動することを防げます。

キャッシュと同期の境界を確認する

キャッシュを復元する前に、ジョブ間で再利用できるディレクトリを明確にします。ビルド成果物、モジュールキャッシュ、パッケージマネージャーのキャッシュではライフサイクルが異なるため、追跡不能な1つの巨大なアーカイブにまとめるべきではありません。キャッシュのマニフェストには、少なくともキャッシュキー、Xcodeのバージョン、アーキテクチャ、生成コマンド、ファイル数を記録します。復元後はタイムスタンプの事前チェックを行ってから、メインのビルドフェーズへ進めます。

同期ツールが更新時刻を保持するかどうかも明確にする必要があります。時刻を保持すれば増分判定に役立ちますが、取得元マシンの時刻が誤っている場合は、その誤りもそのまま伝播します。保持しなければ、すべてのファイルが更新されたばかりに見える可能性があります。重要なのは特定のオプションを固定的に選ぶことではなく、チームで検証済みの方式を1つだけ採用し、その方式をジョブスクリプトに組み込むことです。

コードをアーカイブで受け渡す場合は、まず隔離したディレクトリへ展開します。未来時刻のスキャン、ファイル数の確認、Gitステータスの確認を終えてから、正式なワークスペースへアトミックに切り替えます。古い中間成果物が残っているディレクトリへ直接上書きしてはいけません。

時刻の基準確認をジョブの入口に組み込む

最終的には、依存関係の解決とXcodeビルドより前にチェックを実行します。未来時刻のファイルが見つかった場合は即座に失敗させ、出力するパスの数を制限します。時刻を自動修正してビルドを続行してはいけません。自動修正によって障害の証拠が失われるためです。

各ジョブでは、次の情報を記録することを推奨します。

  1. date と systemsetup -gettimezone の出力。
  2. 現在のコミット、Xcodeのパス、SDK情報。
  3. 未来時刻のファイル数と、先頭から数件のパス。
  4. キャッシュキーと復元結果。
  5. 1回目と2回目のビルドで実行されたタスクの差分。

修復後は、同じコミットで3回続けて実行します。1回目でキャッシュを構築し、2回目で増分ビルドの効果を検証し、3回目で結果が偶然ではないことを確認します。2回目と3回目でも同じタスクが繰り返される場合は、再び全体を削除するのではなく、そのタスクの入力ファイルをたどって調査を続けます。タイムスタンプ管理の目的は、すべてのファイルを同じ時刻にそろえることではありません。ソースコード、生成物、キャッシュの間に、説明可能で検証可能な時系列関係を保つことです。

よくある質問

未来時刻のファイルを見つけたらリポジトリ全体にtouchを実行してよいですか?

推奨しません。全ファイルの更新時刻が変わり、大規模な再コンパイルを招きます。原因を確認し、異常なファイルだけを修復するか対象ディレクトリを再取得してください。

クリーンビルドは正常なのに増分ビルドだけ遅くなるのはなぜですか?

増分ビルドは既存の出力と依存関係を比較します。未来時刻の入力や不整合なキャッシュがあると、依存対象が毎回古いと判定されることがあります。

専用開発環境

次の開発タスク用にクラウドMacを構成

Oak M4、Oak M4 Plus、Oak M4 Proから選び、チームの所在地に合わせて5つの物理ノードからレンタル構成を設定できます。

クラウドMacを構成