Android アプリのビルドを走らせたところ、見覚えのないエラーで止まりました。「リソースが重複している」という趣旨のメッセージです。リソースを足した覚えはありません。前の晩に動いていたプロジェクトが、朝になって一行も触っていないのに通らなくなっていました。
エラーの出ているパスを辿ると、build フォルダの中に同じ名前のファイルが何十個も並んでいました。名前の末尾に「競合コピー」と付いたものです。数えてみると80個を超えていました。
個人開発のプロジェクトを Dropbox の下に置いている方は、同じ朝を迎える可能性があります。私自身がそうでした。
先に結論として、原因は「毎回作り直されるファイル」と同期の相性でした
最初にお伝えしたいのは、コードのバグではなかったということです。
ビルドのたびに作り直されるファイルが、クラッシュ解析のプラグインにありました。ビルドが走るたびに同じパスへ書き込み直すタイプのファイルです。それを Dropbox が掴んで同期しようとし、書き込みと同期がぶつかるたびに「競合コピー」という別ファイルを残していきました。
Gradle はそのコピーも全部リソースとして拾います。同じ ID のリソースが複数見つかり、「重複」で止まります。流れとしては、これだけのことでした。
芳しくない点は、エラーメッセージがどこにも「Dropbox」と言ってくれないことです。重複と言われれば、まず自分のコードを疑います。私も最初の30分は、直近で足した画像リソースを一つずつ外していました。
切り分けは、ファイル名を数えるところから始めます
原因を疑う順番は、次の3つにしています。
- エラーに出ているパスの親フォルダを開き、同じ名前のファイルが複数ないかを見ます
- 名前に「conflicted copy」や「競合コピー」が入っていないかを検索します
- 入っていたら、そのフォルダが同期の対象かどうかを確かめます
2番目はターミナルなら一行で済みます。
# プロジェクト直下で、競合コピーの数を数える
# 英語環境では "conflicted copy"、日本語環境では "競合コピー" が名前に入ります
find . \( -name "*conflicted copy*" -o -name "*競合コピー*" \) | wc -l私の場合は80を超えていました。数が一桁なら偶然の衝突かもしれませんが、二桁なら仕組みの問題です。
直した手順は、掃除してから同期から外すだけでした
順番を間違えると、掃除した端から復活します。同期から外すのが先、掃除が後です。
まず build と .gradle を同期の対象外にします。Dropbox の公式ヘルプには、ファイルやフォルダを無視する方法が載っています。macOS で Apple の File Provider 方式を使っている場合は、次のコマンドです。
# build フォルダを Dropbox の同期から外す(File Provider 方式の macOS)
xattr -w 'com.apple.fileprovider.ignore#P' 1 "$HOME/Library/CloudStorage/Dropbox/Projects/MyApp/app/build"
xattr -w 'com.apple.fileprovider.ignore#P' 1 "$HOME/Library/CloudStorage/Dropbox/Projects/MyApp/.gradle"File Provider を使っていない従来の方式では com.dropbox.ignored という属性を使います。Windows と Linux のコマンドも含め、正確な書き方は公式ヘルプの「How to set a file or folder to be ignored」でご確認ください。自分の環境がどちらの方式かで、使う属性が変わるのです。
ここで一つ、知っておいていただきたい注意があります。無視に設定したフォルダは、手元のディスクには残りますが、Dropbox のサーバーと他の端末からは削除されます。build のように作り直せるものなら問題ありません。ですが、同じ操作を「消えると困るフォルダ」にしてしまうと、他の端末から見えなくなります。対象は生成物だけに絞ってください。
そのうえで、競合コピーを消します。
# 先に一覧を目で確認してから、消します
find . \( -name "*conflicted copy*" -o -name "*競合コピー*" \) -print
# 内容に問題がなければ -delete を付けて再実行します
find . \( -name "*conflicted copy*" -o -name "*競合コピー*" \) -delete-print で見てから -delete に進む二段構えにしているのは、パターンが広すぎて正規のファイルを巻き込む事故を避けるためです。
最後に ./gradlew clean を実行して、ビルドを通し直しました。以降、同じ朝は来ていません。
ほかの生成物のフォルダも、同じ目で棚卸しします
build と .gradle だけが特別だったわけではありません。ビルドやパッケージ管理のたびに中身が入れ替わるフォルダは、どれも同じ性質を持っています。たとえば Web の個人開発なら node_modules や .next、iOS なら DerivedData が候補です。
私は、プロジェクトを開いたら最初に「このフォルダは、消しても一コマンドで作り直せるか」を一つずつ自分に聞くようにしています。作り直せるなら同期に載せる理由がなく、載せれば競合コピーの種になります。逆に、作り直せないものが同期の外にあると、今度は端末の故障で失います。この問いの答えを、フォルダごとにメモへ書き出しておくと、迷う時間がほとんどなくなります。
エージェントにビルドを任せる前に、置き場所を決めておきます
ここからは、この失敗から引いた線引きの話です。
Gemini CLI のようなコーディングエージェントに作業を任せると、ビルドやテストの実行回数が、自分で手を動かすときより確実に増えます。生成物が毎回作り直される現場で、同期フォルダがその全部を追いかける——この組み合わせは、事故が起きる回数そのものを増やします。これは私が実際に試して確かめた話ではなく、仕組みから考えた見立てです。ただ、一度競合コピーの山を見た身としては、先に手を打っておきたい種類のリスクです。
エージェントの使い方そのものについては、Gemini CLI の導入ガイドとエラー対処ガイドにまとめています。ここでは置き場所だけを決めます。
私が決めた線引きは次の二つです。
- 同期の対象はソースと設定です。ビルド生成物は同期しません
- 同期フォルダの中でビルドする場合は、生成物のフォルダを先に無視へ設定してから回します
消えて困る物は同期、作り直せる物は同期の外。 締め切りの前夜に急いでいるときほど、この一文に立ち返るようにしています。
次にやること
まずは、いま使っているプロジェクトのフォルダで、競合コピーを数える一行だけ実行していただければと思います。0件なら、そのまま安心して使えます。1件でも出たら、build を同期から外すところから始めてください。私もそこから始めました。