关于 check-bin-obj-clash
用于诊断多个 MSBuild 项目共用输出目录或中间目录的技能,指导开发者检查构建日志、确认冲突并查找可能的修复方式。
主要功能
- 01通过 MCP 工具检查二进制日志
- 02比较输出路径和中间路径
- 03筛除非构建项目评估
- 04识别常见冲突原因
- 05提供路径与构建配置修复建议
适用场景
- 01排查 NuGet 还原时的文件已存在错误
- 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。这是导致构建失败的常见原因,包括:
- 并行构建期间发生文件访问冲突
- 输出文件缺失或被覆盖
- 间歇性构建失败
- “文件正在使用”错误
- NuGet 还原错误,例如
Cannot create a file when that file already exists——这强烈表明多个项目共用了写入project.assets.json的IntermediateOutputPath
冲突可能发生在以下情况中:
- 不同项目共用同一个输出目录
- 多目标框架构建(例如
TargetFrameworks=net8.0;net9.0)的路径未包含目标框架 - 多个解决方案构建中,同一个项目在一次构建中由不同解决方案构建
注意:分析冲突时,应忽略设置了 BuildProjectReferences=false 的项目实例——这些实例用于 P2P 引用解析构建,只查询元数据(通过 GetTargetPath),实际上不会写入输出目录。
何时使用此技能
遇到以下情况时,请立即调用此技能:
- 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 查询) - 报告冲突——任何包含多个评估的组都表示存在冲突
备用工作流 — 文本日志回放(MCP 不可用时)
仅在 MCP 服务无法启动时使用。
将 binlog 回放为诊断文本日志,然后 grep MCP 工具显示的相同信号:
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,可显示第二个实例是否向同一路径写入(或跳过被掩盖的写入);CoreCompile耗时较长还是约 0 ms,可用于区分实际构建和冗余实例。
然后识别冲突: 将路径规范化为绝对路径,按 OutputPath 和 IntermediateOutputPath 对评估分组,并排除 BuildProjectReferences=false(P2P 查询)——此外,对于 OutputPath,还需排除 MSBuildRestoreSessionId 还原评估。任何剩余评估数大于 1 的组都属于冲突。
常见原因与修复方法
多目标构建的路径中未包含 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 会将某些文件直接写入 IntermediateOutputPath,不添加 TargetFramework 后缀,包括:
project.assets.json(NuGet 还原输出)- 其他与 NuGet 相关的文件
这会导致并行还原期间出现类似 Cannot create a file when that file already exists 的错误。
修复方法: 每个项目都必须具有唯一的 BaseIntermediateOutputPath。不要让多个项目共用中间输出目录:
<BaseIntermediateOutputPath>..\obj\ProjectA\</BaseIntermediateOutputPath>
<BaseIntermediateOutputPath>..\obj\ProjectB\</BaseIntermediateOutputPath>
或者直接使用 SDK 默认设置,将 obj 放在每个项目自己的目录中。
RuntimeIdentifier 构建发生冲突
问题: 为多个 RID 构建,但路径中不包含 RID。
修复方法: 确保路径中包含 RuntimeIdentifier:
<AppendRuntimeIdentifierToOutputPath>true</AppendRuntimeIdentifierToOutputPath>
同一项目通过多个解决方案构建
问题: 单次构建会调用多个解决方案(例如,通过 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\。第一个构建会编译;第二个会跳过编译,但仍会运行 CopyFilesToOutputDirectory。
修复方法: 可选方案包括:
- 合并解决方案 - 确保单次构建中每个项目只从一个解决方案构建
- 使用不同的配置 - 使用不同的
Configuration值构建解决方案,使其对应不同的输出路径 - 排除重复项目 - 使用解决方案筛选器或条件式项目包含,避免重复构建同一项目
额外的全局属性导致冗余项目实例
问题: 由于额外的全局属性(例如 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 这类路径中性的发布标志上有所不同;double_writes 工具会直接标出由此产生的共享文件写入。若要区分情况(a)和(b),请查看构建树中额外的 {_IsPublishing=true} 求值实例隶属于哪个项目(使用 overview/projects 工具):情况(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)。
SetTargetFramework 在 ProjectReference 上重新注入单目标项目自身的 TFM
问题: 某个 ProjectReference 设置了 SetTargetFramework="TargetFramework=<tfm>" 元数据,指向一个单目标项目(使用单数形式的 <TargetFramework>,而不是 <TargetFrameworks>),且注入的 <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 冲突。
检测方法: 按照上面的主要工作流操作。evaluations 和 evaluation_global_properties 工具会显示被引用项目的两次评估:它们共用相同的 OutputPath/IntermediateOutputPath,且区别仅在于 TargetFramework 全局属性;同时,项目本身是单目标的(其自身的 TargetFramework 已等于注入值)。double_writes 工具会直接标记由此产生的共享文件写入。
注意: 对于非多目标引用,P2P 协议本身不会注入 TargetFramework——冲突特定源于显式的 SetTargetFramework 元数据覆盖了这个安全默认值。
修复方法: 如果 SetTargetFramework 只是重复声明项目自身的单一 TFM,请将其移除:
<ProjectReference Include="..\Tool\Tool.csproj" />
SetTargetFramework 合理使用的情形(不会导致冲突):
-
多目标引用——被引用项目使用
<TargetFrameworks>,且需要指定其中一个 TFM。每个 TFM 都有独立的输出路径,因此不会冲突。 -
覆盖为不同的 TFM——可以在单目标项目上使用
SetTargetFramework,使其在不同于自身声明的 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 技能中的“清理仓库”部分。