check-bin-obj-clash について
複数の MSBuild プロジェクトが同じ出力先や中間出力先を使う問題を診断するスキルです。ビルドログの調査から競合の確認、修正案の特定までを支援します。
主な機能
- 01MCP ツールでバイナリログを調査
- 02出力パスと中間パスを比較
- 03非ビルド評価を除外
- 04競合の一般的な原因を特定
- 05パスやビルド設定の修正案を提示
活用シーン
- 01NuGet 復元時のファイル存在エラーを調査
- 02断続的な並列ビルド失敗を追跡
- 03ビルド出力の上書きや欠落を診断
MCP 対応
MCP サーバーの構造化ツールでバイナリログを調べ、プロジェクト評価、プロパティ、ファイルの重複書き込みを確認します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill check-bin-obj-clashインストール後、Claude Code で/check-bin-obj-clashを入力して読み込まれたか確認します。
詳細
OutputPath と IntermediateOutputPath の競合を検出する
概要
このスキルは、複数の MSBuild プロジェクトの評価で同じ OutputPath または IntermediateOutputPath が共有されている場合の特定に役立ちます。これは、次のようなビルド失敗の一般的な原因です。
- 並列ビルド中のファイルアクセス競合
- 出力ファイルの欠落や上書き
- 断続的なビルド失敗
- 「ファイルは使用中です」というエラー
Cannot create a file when that file already existsなどの NuGet 復元エラー — これは、複数のプロジェクトでproject.assets.jsonの書き込み先となる同じIntermediateOutputPathが共有されていることを強く示しています
競合は、次の間で発生する可能性があります。
- 同じ出力ディレクトリを共有する別々のプロジェクト
- マルチターゲットビルド(例:
TargetFrameworks=net8.0;net9.0)で、パスにターゲットフレームワークが含まれていない場合 - 複数のソリューションビルドで、同じプロジェクトが単一のビルド内で異なるソリューションからビルドされる場合
注: BuildProjectReferences=false のプロジェクトインスタンスは、競合の分析時に無視してください。これらはメタデータの照会(GetTargetPath 経由)のみを行う P2P 参照解決ビルドであり、出力ディレクトリには実際に書き込みません。
このスキルを使用するタイミング
次のいずれかが見られた場合は、ただちにこのスキルを実行してください。
- NuGet の復元中に発生する
Cannot create a file when that file already exists The process cannot access the file because it is being used by another process- 再試行すると成功する断続的なビルド失敗
- 出力ファイルの欠落や予期しない上書き
ステップ 1:バイナリログを生成する
binlog-generation スキルを使用して、正しい命名規則でバイナリログを生成します。
主なワークフロー — binlog MCP
MCP サーバーは、テキストログを解析せずに .binlog を調べるための構造化ツールを公開しています。binlog をテキストファイルに再生する代わりに、ツールを直接呼び出してください。利用可能なツールがわからない場合は、まず MCP の tools/list を呼び出してください。
重要な制約:
.binlogファイルはバイナリ形式です。cat、head、stringsを使ったり、直接読み取ったりしないでください。照会には MCP ツールのみを使用してください。- 調査しながら、判明した内容をまとめてください。 調査に使える時間をすべて費やさないでください。十分な根拠が得られたら、結論を提示してください。
ステップ 2:概要を確認してプロジェクトを一覧表示する
MCP の概要ツールとプロジェクトツールを使用してビルドを把握し、参加したすべてのプロジェクトを一覧表示します。
ステップ 3:評価とグローバルプロパティを確認する
MCP の evaluations および evaluation_global_properties ツールを使用して、プロジェクトごとのすべての評価を確認します。次の点を調べてください。
- 同じプロジェクトに対する複数の評価(マルチターゲットまたは複数のビルド構成を示します)
- 評価間で異なるグローバルプロパティ(
TargetFramework、Configuration、RuntimeIdentifier、SolutionFileName、PublishReadyToRunなど)
ステップ 4:各評価の出力パスを取得する
MCP のプロパティツールを使用して、各プロジェクト評価の OutputPath、IntermediateOutputPath、BaseOutputPath、BaseIntermediateOutputPath を照会します。
ステップ 5:二重書き込みを確認する
利用可能であれば、MCP の double_writes ツールを使用します。複数のプロジェクトインスタンスによって書き込まれたファイルを直接検出できます。
ステップ 6:競合を特定する
すべての評価にわたって OutputPath と IntermediateOutputPath の値を比較します。
- パスを正規化する — 絶対パスに変換し、区切り文字を正規化します
- パスごとにグループ化する — 同じ OutputPath または IntermediateOutputPath を共有する評価を特定します
- ビルド以外の評価を除外する —
BuildProjectReferences=falseのインスタンス(P2P クエリ)を除外します - 競合を報告する — 評価が 2 件以上あるグループは、競合を示します
フォールバックワークフロー — テキストログの再生(MCP を利用できない場合)
MCP サーバーを起動できない場合にのみ使用してください。
binlog を診断用テキストログに再生し、MCP ツールが表示するものと同じシグナルを grep します。
dotnet msbuild build.binlog -noconlog -fl "-flp:v=diag;logfile=full.log"
次に、競合のシグナルを抽出します。
- プロジェクトと評価 — 評価の開始を一覧表示し、プロジェクトごとに数を数えます。
grep 'Evaluation started' full.log | grep -oiE '"[^"]+\.[a-z]+proj"' | sort | uniq -c。完全な引用符付きパスで照合することで、異なるディレクトリにある同名プロジェクトを区別し、パス内の空白にも対応できます。カウントが 2 以上のパスは、複数回評価されています(マルチターゲットまたは追加のグローバルプロパティ)。grep -cだけではログ全体の評価数しか得られないため、プロジェクトごとの重複は特定できません。 - 出力パス —
grep -iE 'OutputPath[[:space:]]*=|IntermediateOutputPath[[:space:]]*=|BaseOutputPath[[:space:]]*=|BaseIntermediateOutputPath[[:space:]]*=' full.log | sort -u。または、プロジェクトを直接クエリします。dotnet msbuild MyProject.csproj -getProperty:OutputPath(およびIntermediateOutputPath、BaseIntermediateOutputPath)。 - 区別に使うグローバルプロパティ —
grep -iE 'TargetFramework|Configuration|Platform|RuntimeIdentifier|SolutionFileName|PublishReadyToRun' full.log。パスに影響するものと、冗長なインスタンスを分岐させるだけのものについては、確認するグローバルプロパティの表を参照してください。 - 裏付けとなる証拠(任意) —
grep 'Target "CopyFilesToOutputDirectory"' full.logとgrep 'SkipUnchangedFiles' full.logを実行すると、2 つ目のインスタンスが同じパスに書き込んでいる(または、書き込みがスキップされて隠れている)ことを確認できます。CoreCompileにかかる時間が長い場合と約 0 ms の場合を比べると、実際のビルドと冗長なインスタンスを区別できます。
次に競合を特定します。 パスを絶対パスに正規化し、評価を OutputPath と IntermediateOutputPath ごとにグループ化して、BuildProjectReferences=false(P2P クエリ)を除外します。さらに、OutputPath についてのみ、MSBuildRestoreSessionId の復元評価も除外します。残った評価が 2 つ以上あるグループは競合です。
よくある原因と修正方法
TargetFramework がパスに含まれないマルチターゲット
問題: プロジェクトで TargetFrameworks を使用していますが、OutputPath がフレームワークごとに異なりません。
<OutputPath>bin\$(Configuration)\</OutputPath>
修正方法: TargetFramework をパスに含めます。
<OutputPath>bin\$(Configuration)\$(TargetFramework)\</OutputPath>
または、これを自動的に処理する SDK の既定値を使用します。
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
<AppendTargetFrameworkToIntermediateOutputPath>true</AppendTargetFrameworkToIntermediateOutputPath>
プロジェクト間で出力ディレクトリを共有(AppendTargetFramework では修正できません)
問題: 複数のプロジェクトで、同じ BaseOutputPath または BaseIntermediateOutputPath が明示的に設定されています。
<BaseOutputPath>..\SharedOutput\</BaseOutputPath>
<BaseIntermediateOutputPath>..\SharedObj\</BaseIntermediateOutputPath>
<BaseOutputPath>..\SharedOutput\</BaseOutputPath>
<BaseIntermediateOutputPath>..\SharedObj\</BaseIntermediateOutputPath>
重要: AppendTargetFrameworkToOutputPath=true を設定しても、競合は解消されません。.NET は project.assets.json(NuGet の復元出力)やその他の NuGet 関連ファイルなど、一部のファイルを TargetFramework のサフィックスなしで IntermediateOutputPath に直接書き込みます。
そのため、並列復元中に Cannot create a file when that file already exists のようなエラーが発生します。
修正方法: 各プロジェクトで一意の BaseIntermediateOutputPath を使用する必要があります。プロジェクト間で中間出力ディレクトリを共有しないでください。
<BaseIntermediateOutputPath>..\obj\ProjectA\</BaseIntermediateOutputPath>
<BaseIntermediateOutputPath>..\obj\ProjectB\</BaseIntermediateOutputPath>
または、各プロジェクトのディレクトリ内に obj を配置する SDK の既定値をそのまま使用してください。
RuntimeIdentifier ビルドの競合
問題: パスに RID が含まれていない状態で、複数の RID を対象にビルドしています。
修正方法: RuntimeIdentifier がパスに含まれるようにします。
<AppendRuntimeIdentifierToOutputPath>true</AppendRuntimeIdentifierToOutputPath>
同じプロジェクトをビルドする複数のソリューション
問題: 1 回のビルドで、同じプロジェクトを含む複数のソリューションが呼び出されます(たとえば、MSBuild タスクまたはコマンドライン経由)。各ソリューションのビルドでは、出力パスに影響しない異なる Solution* グローバルプロパティを使用して、プロジェクトが個別に評価・ビルドされます。
検出方法: 同じプロジェクトの評価間で SolutionFileName と CurrentSolutionConfigurationContents を比較します。値が異なる場合は、複数のソリューションによるビルドが行われています。例:
| プロパティ | ソリューション A の評価 | ソリューション B の評価 |
|---|---|---|
SolutionFileName | BuildAnalyzers.sln | Main.slnx |
CurrentSolutionConfigurationContents | 1 つのプロジェクトエントリ | 約 49 個のプロジェクトエントリ |
OutputPath | bin\Release\netstandard2.0\ | bin\Release\netstandard2.0\ ← 競合 |
例: リポジトリのビルドスクリプトが BuildAnalyzers.sln の後に Main.slnx をビルドし、両方のソリューションに SharedAnalyzers.csproj が含まれています。どちらのビルドも bin\Release\netstandard2.0\ に出力を書き込みます。最初のビルドではコンパイルが行われますが、2 回目はコンパイルをスキップしながら CopyFilesToOutputDirectory を実行します。
修正方法: 次のような方法があります。
- ソリューションを統合する - 1 回のビルドで、各プロジェクトが 1 つのソリューションからのみビルドされるようにします
- 異なる構成を使用する - 出力パスが異なる
Configuration値で各ソリューションをビルドします - 重複するプロジェクトを除外する - ソリューションフィルターまたは条件付きのプロジェクト包含を使用して、同じプロジェクトが 2 回ビルドされるのを防ぎます
冗長なプロジェクトインスタンスを作成する追加のグローバルプロパティ
問題: 出力パスに影響しない追加のグローバルプロパティ(例:PublishReadyToRun=false)によって個別の MSBuild プロジェクトインスタンスが作成され、同じソリューション内でプロジェクトが複数回ビルドされます。これらのプロパティにより、MSBuild はインスタンス間で結果をキャッシュできず、ターゲットが冗長に実行されます。
検出方法: 同じソリューション内にある同じプロジェクトの評価間でグローバルプロパティを比較します(SolutionFileName が同じであることを確認してください)。異なるものの、パスの違いには寄与しないプロパティを探します。
| プロパティ | 評価 A(Razor.slnx から) | 評価 B(Razor.slnx から) |
|---|---|---|
PublishReadyToRun | (未設定) | false |
OutputPath | bin\Release\netstandard2.0\ | bin\Release\netstandard2.0\ ← 競合 |
追加のプロパティが何の効果も持たないプロジェクトでは、特に無駄が生じます(例:ReadyToRun コンパイルを使用しない netstandard2.0 クラスライブラリにおける PublishReadyToRun)。
修正方法: 次のような方法があります。
- 追加のグローバルプロパティを削除する - どの親ターゲット/タスクがプロパティを追加しているかを調べ、不要なプロジェクトに渡されないようにします
RemoveGlobalPropertiesメタデータを使用する -ProjectReference項目にRemoveGlobalProperties="PublishReadyToRun"を指定し、参照先プロジェクトのビルド前にプロパティを取り除きます- プロパティに条件を付ける - 実際に使用するプロジェクトにのみプロパティを設定します(例:クラスライブラリではなく、実行可能プロジェクトにのみ設定します)
追加のグローバルプロパティを使った明示的な <MSBuild> ビルド/発行(同一プロジェクトまたは別プロジェクト)
問題: ターゲットが <MSBuild> タスクを使い、追加のグローバルプロパティを指定してプロジェクトをビルドまたは発行しています。最もよくあるのは「ビルド時に発行する」ターゲットです。問題のある呼び出しは、ターゲットプロジェクト自体にある場合も、それを使用する別のプロジェクト(たとえば、ツールを発行するテストプロジェクトやレイアウトプロジェクト)にある場合もあります。
<Target Name="PublishOnBuild" AfterTargets="Build">
<MSBuild Projects="$(MSBuildProjectFullPath)" Targets="Publish" Properties="_IsPublishing=true" />
</Target>
<MSBuild Projects="..\tool\tool.csproj" Targets="Publish" Properties="_IsPublishing=true" />
いずれの場合も、ターゲットプロジェクトの別インスタンス(path + {_IsPublishing=true})が作られます。このインスタンスは、ソリューション/グラフがすでにビルドしているインスタンスと同じ OutputPath/IntermediateOutputPath を共有します。両方が同じファイルに書き込みます。NativeAOT の場合、これには *.sourcelink 中間ファイルも含まれるため、並列ビルド時に SourceLinkWriter/「ファイル使用中」エラーが発生します。
検出方法: 上記の主要なワークフローに従ってください。evaluations ツールと evaluation_global_properties ツールにより、同じ OutputPath/IntermediateOutputPath を共有し、_IsPublishing のようなパスに影響しない発行フラグだけが異なる、ターゲットプロジェクトの評価が2つあることが分かります。また、double_writes ツールは、その結果として発生する共有ファイルへの書き込みを直接検出します。(a)と(b)を区別するには、overview/projects ツールでビルドツリーを確認し、追加の {_IsPublishing=true} 評価がどのプロジェクトの下で実行されるかを見てください。(a)ではターゲットプロジェクト自体、(b)では <MSBuild> タスクを呼び出したコンシューマープロジェクトです。
修正方法: 呼び出しの場所によって異なります。
- 同一プロジェクト(a): プロジェクトが自身にプロパティを設定するため、
RemoveGlobalPropertiesでそのプロパティを削除することはできません。フラグを静的な(グローバルではない)プロパティに設定し、ターゲットサイクルを防ぐガードを設けたうえで、DependsOnTargets/CallTargetを使って同じインスタンス内でターゲットを実行します。発行がエントリーポイントの場合にも対応する必要があります。
<PropertyGroup>
<_PublishWasInvokedDirectly Condition="'$(_IsPublishing)' == 'true'">true</_PublishWasInvokedDirectly>
<_IsPublishing>true</_IsPublishing>
</PropertyGroup>
<Target Name="PublishOnBuild"
AfterTargets="Build"
DependsOnTargets="Publish"
Condition="'$(_PublishWasInvokedDirectly)' != 'true'" />
- 別プロジェクト(b): コンシューマーは、パスに影響しないグローバルプロパティを指定してプロデューサーを別インスタンスとして起動してはいけません。プロデューサー自身のビルドの一部として発行させ(プロデューサーのプロジェクトで(a)の修正を適用)、その後、コンシューマー側では再発行するのではなく、実行順序を制御して出力を読み取ります。
<ItemGroup>
<ProjectReference Include="..\tool\tool.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
作成時の問題点とその理由については、msbuild-antipatterns スキル(AP-22)を参照してください。
ProjectReference 上で SetTargetFramework が単一ターゲットのプロジェクト自身の TFM を再注入する
問題: ProjectReference が SetTargetFramework="TargetFramework=<tfm>" メタデータを設定し、それが単一ターゲットのプロジェクト(<TargetFrameworks> ではなく単数形の <TargetFramework> を使用するもの)を指しており、注入される <tfm> がそのプロジェクトがすでにターゲットとしている TFM と等しい場合です。SetTargetFramework は、参照されるプロジェクトのビルドに対して TargetFramework をグローバルプロパティとして注入します。
<ProjectReference Include="..\Tool\Tool.csproj" SetTargetFramework="TargetFramework=net8.0" />
プロジェクトがすでにターゲットとしている TFM を注入することはパスに影響しません — プロジェクトはすでに単独で bin\<config>\net8.0\ と obj\<config>\net8.0\ に解決されます。そのため出力パスは変わりません。変わるのは、別個のインスタンス (project, {TargetFramework=net8.0}) に分岐する点だけです。ソリューション/グラフはまったく同じプロジェクトを (project, {}) としてビルドします。両者は同じ OutputPath/IntermediateOutputPath を共有するため、プロジェクトは同じ場所へ二重にビルドされます — 並列ビルドでの bin/obj の衝突です。
検出方法: 上記の Primary ワークフローに従ってください。evaluations と evaluation_global_properties ツールは、参照されるプロジェクトの 2 つの評価を明らかにします。これらは同じ OutputPath/IntermediateOutputPath を共有し、TargetFramework グローバルプロパティだけが異なります。一方で、プロジェクト自体は単一ターゲットです(それ自身の TargetFramework がすでに注入された値と等しい)。double_writes ツールは、その結果生じる共有ファイルへの書き込みを直接フラグします。
注記: P2P プロトコル自体は、マルチターゲットでない参照に対して TargetFramework を注入しません — 衝突は、明示的な SetTargetFramework メタデータがこの安全なデフォルトを上書きすることに起因します。
修正: プロジェクト自身の単一 TFM をただ言い直しているだけの場合、冗長な SetTargetFramework を削除します。
<ProjectReference Include="..\Tool\Tool.csproj" />
SetTargetFramework が正当な場合(衝突ではない):
-
マルチターゲット参照 — 参照されるプロジェクトが
<TargetFrameworks>を使用しており、特定の 1 つの TFM が必要な場合。各 TFM は異なる出力パスを持つため、衝突はありません。 -
別の TFM への上書き — 単一ターゲットのプロジェクトに対して
SetTargetFrameworkを使用し、それが宣言している TFM とは別の TFM でビルドすることがあります。この場合、注入された TFM が出力パス(obj\<config>\<different-tfm>\)を変更するため、そのインスタンスは(project, {})と衝突しなくなります。パスに影響せず衝突するのは同一 TFM の場合だけです。 -
フレームワーク非互換の参照 — 参照するプロジェクトと参照されるプロジェクトが互換性のないフレームワークをターゲットにしている場合(例:
.NETFrameworkプロジェクトが.NETCoreAppプロジェクトを参照する、またはその逆) — どちら側が単一ターゲットかマルチターゲットかに関係なく —SkipGetTargetFrameworkProperties="true"(そうしないと P2P のGetTargetFrameworkPropertiesネゴシエーションが失敗します)とReferenceOutputAssembly="false"(互換性のないフレームワーク向けにビルドされたアセンブリは参照として使用できません — ビルドのトリガー/順序付けだけを行いたいのです)を設定します:<ProjectReference Include="..\Tool\Tool.csproj" SkipGetTargetFrameworkProperties="true" ReferenceOutputAssembly="false" />SkipGetTargetFrameworkProperties="true"を設定すると、ネゴシエーションは参照するプロジェクト自身のTargetFrameworkグローバルプロパティ(特定の TFM 向けにビルドするとき、たとえばマルチターゲットの場合に存在します)が参照されるプロジェクトへ流れ込むのを止めなくなります。単一ターゲットの参照されるプロジェクトでは、これにより誤った TFM / 出力パスでビルドすることが強制されます。これを防ぐには、SetTargetFramework="TargetFramework=<tfm>"(TFM を固定する)またはUndefineProperties="TargetFramework"(継承されたグローバルプロパティを取り除き、プロジェクトが宣言どおりにビルドされるようにする)のいずれかを設定します — 両方ではなく、どちらか一方を使用してください:<ProjectReference Include="..\Tool\Tool.csproj" SkipGetTargetFrameworkProperties="true" UndefineProperties="TargetFramework" ReferenceOutputAssembly="false" />
作成時のにおいと根拠については、msbuild-antipatterns スキル(AP-23)を参照してください。
ヒント
- SDK の既定パスには
$(TargetFramework)が含まれています。プロジェクトがこれらの既定値を上書きすると、衝突が発生することがよくあります。比較する前に、相対パスを絶対パスに正規化してください。 - プロジェクト間の
IntermediateOutputPathの衝突は、AppendTargetFrameworkToOutputPathでは解決できません。project.assets.jsonなどのファイルは、中間パスに直接書き込まれます。同じプロジェクト内で複数のターゲットフレームワークを指定した際に発生する衝突には、AppendTargetFrameworkToOutputPath=trueが適切な修正方法です。 - パスの衝突を示すエラーメッセージには、
Cannot create a file when that file already exists(NuGet の復元時)、The process cannot access the file because it is being used by another process、または再試行すると成功する断続的な失敗があります。
評価を比較する際に確認するグローバルプロパティ
複数の評価で同じ出力パスが共有されている場合は、理由を把握するために、次のグローバルプロパティを比較してください。
| プロパティ | OutputPath に影響しますか? | 備考 |
|---|---|---|
TargetFramework | はい | TFM が異なる場合は、パスも異なる必要があります。例外: 単一ターゲットのプロジェクト自身の TFM を再注入(例:同じ値を指定した SetTargetFramework 経由)してもパスは変わらず、冗長なインスタンスが分岐して同じ出力パスを共有します(「SetTargetFramework による再注入...」を参照) |
RuntimeIdentifier | はい | RID が異なる場合は、パスも異なる必要があります |
Configuration | はい | Debug と Release の違い |
Platform | はい | AnyCPU と x64 など |
SolutionFileName | いいえ | プロジェクトのビルド元のソリューションを示します。値が異なる場合は、複数ソリューション間の衝突を示します |
SolutionName | いいえ | 拡張子を除いたソリューション名 |
SolutionPath | いいえ | ソリューションファイルのフルパス |
SolutionDir | いいえ | ソリューションファイルを含むディレクトリ |
CurrentSolutionConfigurationContents | いいえ | プロジェクトエントリを含む XML。エントリ数からソリューションを判別できます |
BuildProjectReferences | いいえ | false = 実際のビルドではなく、P2P クエリです。無視してください |
MSBuildRestoreSessionId | いいえ | 存在する場合は、復元フェーズの評価です |
PublishReadyToRun | いいえ | 発行設定です。ビルド出力パスは変わりませんが、異なるプロジェクトインスタンスが作成されます |
_IsPublishing | いいえ | 発行フラグです。このプロパティを設定した <MSBuild> の Build/Publish 呼び出し(このプロジェクト内、またはこのプロジェクトを利用する別のプロジェクト内)は、発行用インスタンスを分岐させ、ビルド出力パスを共有します(「追加のグローバルプロパティを指定した明示的な <MSBuild> Build/Publish」を参照) |
修正のテスト
パスの衝突を修正する変更を加えたら、クリーンして再ビルドし、確認してください。binlog ファイルを保持したままリポジトリをクリーンする方法については、binlog-generation スキルの「リポジトリのクリーン」セクションを参照してください。