ある週、夜間に走らせていた Antigravity のエージェントが、同じタスクなのに三日に一度くらいの割合で失敗するようになりました。ログを見ると pod install の途中でロックファイルが壊れていたり、xcodebuild が「存在するはずのファイルが見つからない」と言って止まっていたりします。手元で同じコマンドを叩くと、何事もなく通る。エラーの場所も毎回少しずつ違う。こういう「再現しない失敗」は、たいていコードではなく環境のほうに原因があります。
私自身、iOS のアプリを App Store と Google Play で個人開発として長く運用していて、作業フォルダはずっと Dropbox で複数の Mac に同期しています。便利な反面、この同期がエージェントの無人実行と相性が悪い瞬間があることに、しばらく気づけませんでした。原因は、同期デーモンとビルドが同じディレクトリを同時に触っていたことでした。
同じコマンドなのに、成功と失敗が日替わりで入れ替わる
最初に疑うべきは、自分のコードでも Antigravity のエージェント本体でもありません。「失敗の場所が毎回ずれる」「手元では再現しない」「特定の時間帯に偏る」——この三つが揃ったら、まず環境の非決定性を疑います。
クラウド同期フォルダの上でビルドすると、次の二つのプロセスが同じファイルを取り合います。
プロセス やっていること 触るディレクトリ
ビルド(xcodebuild / pod install) 中間生成物を秒単位で大量に作成・削除 build/, Pods/, DerivedData/, .swiftpm/
同期デーモン(Dropbox 等) 変化したファイルを検知してアップロード・再ダウンロード 同期対象フォルダ全体
ビルドが書いた瞬間のファイルを同期デーモンがアップロードし始め、その途中でビルドが同じファイルを上書きすると、同期側は「両方を残す」と判断して衝突コピー を作ります。Podfile.lock (○○ のコンフリクトしたコピー).lock のようなファイルが生まれ、次のビルドがそれを掴むと整合性が崩れます。人間が対話的に作業しているときは、たまたまその一瞬に当たらないので気づきにくいのですが、エージェントを一日に何度も無人で回すと、衝突に当たる確率が無視できなくなります。
同期デーモンとビルドが同じディレクトリを取り合っている
ここで大事なのは、これは Antigravity のバグでも CocoaPods のバグでもない、ということです。どのエージェント・どのビルドツールを使っていても、高頻度で書き換わるディレクトリをファイル同期の対象に含めている限り 、同じ競合が起きます。
特に壊れやすいのは次のような場所です。.swiftpm/xcuserdata のように Xcode が裏で頻繁に書き換えるメタデータ、Pods/ のように一括で再生成される依存ツリー、build/ や DerivedData/ のような中間生成物。これらは「いつ消えて再生成されてもよい」性質のもので、別の Mac と共有する価値がそもそもありません。共有したいのはソースコードであって、ビルドの残骸ではないはずです。
.gitignore では守れない(Git と同期は別物)
ここで多くの人がつまずくのが、「.gitignore に書いてあるから大丈夫」という思い込みです。.gitignore は Git のコミット対象を制御するだけで、Dropbox や iCloud の同期デーモンは .gitignore をまったく読みません。Git にとって無視されているディレクトリでも、同期デーモンは律儀にアップロードし続けます。
つまり対策は、Git とは別のレイヤー——同期デーモン自身に「このフォルダは同期しないで」と伝えること——で打つ必要があります。macOS の Dropbox には、ファイル単位で同期から除外する拡張属性が用意されています。
揮発するディレクトリを同期から外す — xattr で印をつける
Dropbox(macOS 版)は、拡張属性 com.dropbox.ignored が 1 になっているファイル・ディレクトリを同期対象から外します。プロジェクトのルートで次のスクリプトを一度走らせると、揮発ディレクトリをまとめて除外できます。
#!/bin/bash
# mark-volatile-ignored.sh — Xcode プロジェクトの揮発ディレクトリを Dropbox 同期から外す
set -euo pipefail
# プロジェクトルートで実行する想定
TARGETS = (
"Pods"
"build"
"DerivedData"
".swiftpm"
)
# *.xcodeproj / *.xcworkspace 内のユーザー固有メタデータも対象に加える
while IFS = read -r -d '' p ; do
TARGETS += ( " $p " )
done < <( find . -type d \( -name "xcuserdata" -o -name "*.xcuserdatad" \) -print0 )
for t in "${ TARGETS [ @ ]}" ; do
if [ -e " $t " ]; then
xattr -w com.dropbox.ignored 1 " $t "
echo "ignored: $t "
fi
done
設定できたかどうかは、次のコマンドで確認します。1 が返ればそのディレクトリは同期から外れています。
xattr -p com.dropbox.ignored Pods
# => 1
iCloud Drive を使っている場合は仕組みが違い、フォルダ名の末尾に .nosync を付けるか、ビルド出力先を同期対象の外(たとえば ~/Library/Developer/Xcode/DerivedData)に逃がすのが確実です。私はビルド成果物を最初から同期フォルダの外に出す方針にしていますが、CocoaPods の Pods/ のようにプロジェクト直下に置かざるを得ないものは、上の xattr での除外で対処しています。
クリーンビルドで印が消える問題を Run Script で塞ぐ
ここに落とし穴がもう一つあります。xattr の印はディレクトリに付いているので、クリーンビルドでそのディレクトリごと作り直されると、印も一緒に消えます 。一度設定して安心していると、次のクリーンビルドの後にまた衝突コピーが復活する、という現象に悩まされます。
対策は、ビルドのたびに印を付け直すことです。Xcode のターゲットに Run Script フェーズを追加し、ビルドの早い段階で除外属性を再適用します。
# Build Phases > Run Script("Based on dependency analysis" は外す)
# 生成直後のディレクトリに毎回 ignored 属性を貼り直す
for d in "${ PODS_ROOT :- $SRCROOT / Pods }" " $SRCROOT /.swiftpm" " $SRCROOT /build" ; do
[ -e " $d " ] && /usr/bin/xattr -w com.dropbox.ignored 1 " $d " 2> /dev/null || true
done
CocoaPods を使っているなら、Podfile の post_install フックに同じ処理を入れておくと、pod install の直後にも確実に印が付きます。
# Podfile
post_install do |installer|
system ( "xattr -w com.dropbox.ignored 1 \" #{ installer. sandbox . root } \" " )
end
こうしておくと、依存を入れ直すたびに Pods/ が同期対象から外れた状態で再生成されるので、pod install 中にロックファイルが衝突コピー化する事故が起きにくくなります。
エージェントを走らせる前に「衝突コピー」を検査する
除外を徹底しても、過去に作られた衝突コピーがフォルダの片隅に残っていることがあります。エージェントがそれを掴むと、また同じ失敗を踏みます。そこで、Antigravity のエージェントにタスクを渡す前に、衝突コピーが一つも無いことを確認するプリフライト を挟みます。スケジュール実行のいちばん最初のステップに置くのが効果的です。
#!/bin/bash
# preflight-no-conflicts.sh — 衝突コピーが残っていたらエージェントを走らせない
set -euo pipefail
# Dropbox の衝突コピー / iCloud の重複表現を拾う
HITS = $( find . \
\( -iname "*conflicted copy*" \
-o -iname "*コンフリクトしたコピー*" \
-o -iname "* 2.lock" \
-o -iname "*.lock.sb-*" \) \
-not -path "*/.git/*" 2> /dev/null )
if [ -n " $HITS " ]; then
echo "🛑 衝突コピーを検出しました。エージェントを中止します:"
echo " $HITS "
exit 1
fi
echo "✅ クリーン — エージェント実行を許可"
ポイントは、exit 1 で止める ことです。無人実行では「怪しかったら進む」ではなく「怪しかったら止める」を既定値にしておかないと、壊れた状態のままコミットや push まで突き進んでしまいます。私はこの種の自動処理では、判断に迷う状況を見つけたら処理を続行せず、必ず手前で落とすようにしています。失敗を早い段階で表面化させたほうが、後始末がずっと軽く済みます。
何を同期に残し、何を外すか
最後に、線引きの基準を整理しておきます。迷ったときは「別の Mac と共有して意味があるか」「消えても再生成できるか」の二つで判断します。
パス 同期 理由
ソースコード(.swift, .kt, .xcodeproj の設定本体) 残す 共有する価値があり、手で書いた成果物
Podfile / Package.swift / *.lock 残す 依存の固定情報。ただしビルド中の一時上書きに注意
Pods/ / .build/ / build/ 外す 再生成可能。高頻度で書き換わり衝突の温床
DerivedData/ 外す(できれば同期外へ移設) 中間生成物。共有する意味がない
xcuserdata / *.xcuserdatad 外す 個人のエディタ状態。Mac ごとに異なって当然
*.lock を「残す」に入れているのは少し悩ましいところです。依存のバージョン固定情報なので共有したい一方で、ビルド中にいちばん衝突しやすいファイルでもあります。私は Pods/ ごと同期から外し、Podfile.lock はリポジトリのルート(揮発ディレクトリの外)に置いて Git で共有する、という分け方に落ち着きました。同期デーモンとビルドの責任範囲を物理的に分けてしまうのが、いちばん壊れにくい構成だと感じています。
まず一つのアプリで試すなら
複数のアプリを一度に直そうとすると、どの変更が効いたのか分からなくなります。まずは一つのプロジェクトで、次の順に小さく試すことをお勧めします。
Pods/ と .swiftpm に xattr -w com.dropbox.ignored 1 を付け、xattr -p で 1 が返ることを確認します。
その状態で夜間のエージェント実行を数日観察し、失敗の頻度が下がるかを見ます。ここで効果が確認できてから次へ進みます。
効果が出たら Run Script とプリフライトを足し、クリーンビルドや過去の衝突コピーにも崩されない土台にします。
一つのアプリで手応えが得られたら、同じ手順を他のアプリへ広げていくのが安全です。私自身、最初に綺麗な壁紙アプリ一本で確かめてから、残りのプロジェクトへ同じ設定を展開しました。
クラウド同期は個人開発の強い味方ですが、ビルドという秒単位で状態が変わる作業とは、触る場所を分けてあげる必要があります。同期に任せる領域とビルドに任せる領域の境界を一度きちんと引いておくと、エージェントに安心して夜を任せられるようになります。