mcpField
C
check-bin-obj-clash

check-bin-obj-clash

github.com/dotnet开发5.8k3.6k 次安装1 天前更新
代码仓库

关于 check-bin-obj-clash

用于诊断多个 MSBuild 项目共用输出目录或中间目录的技能,指导开发者检查构建日志、确认冲突并查找可能的修复方式。

主要功能

  1. 01通过 MCP 工具检查二进制日志
  2. 02比较输出路径和中间路径
  3. 03筛除非构建项目评估
  4. 04识别常见冲突原因
  5. 05提供路径与构建配置修复建议

适用场景

  1. 01排查 NuGet 还原时的文件已存在错误
  2. 02追踪间歇性并行构建失败
  3. 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 值:

  1. 规范化路径——转换为绝对路径并统一分隔符
  2. 按路径分组——找出共用相同 OutputPath 或 IntermediateOutputPath 的评估
  3. 排除非构建评估——排除设置了 BuildProjectReferences=false 的实例(P2P 查询)
  4. 报告冲突——任何包含多个评估的组都表示存在冲突

备用工作流 — 文本日志回放(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 的评估
SolutionFileNameBuildAnalyzers.slnMain.slnx
CurrentSolutionConfigurationContents1 个项目条目约 49 个项目条目
OutputPathbin\Release\netstandard2.0\bin\Release\netstandard2.0\ ← 冲突

示例: 仓库构建脚本先构建 BuildAnalyzers.sln,再构建 Main.slnx,且两个解决方案都包含 SharedAnalyzers.csproj。两个构建都会写入 bin\Release\netstandard2.0\。第一个构建会编译;第二个会跳过编译,但仍会运行 CopyFilesToOutputDirectory。

修复方法: 可选方案包括:

  1. 合并解决方案 - 确保单次构建中每个项目只从一个解决方案构建
  2. 使用不同的配置 - 使用不同的 Configuration 值构建解决方案,使其对应不同的输出路径
  3. 排除重复项目 - 使用解决方案筛选器或条件式项目包含,避免重复构建同一项目
额外的全局属性导致冗余项目实例

问题: 由于额外的全局属性(例如 PublishReadyToRun=false)会创建不同的 MSBuild 项目实例,因此同一解决方案中的某个项目会被多次构建。这些属性不会影响输出路径,但会阻止 MSBuild 在不同实例间缓存结果,从而导致目标重复执行。

检测方法: 比较同一解决方案中同一项目不同评估的全局属性(SolutionFileName 相同)。查找值不同但不会影响路径区分的属性:

属性评估 A(来自 Razor.slnx)评估 B(来自 Razor.slnx)
PublishReadyToRun(未设置)false
OutputPathbin\Release\netstandard2.0\bin\Release\netstandard2.0\ ← 冲突

对于额外属性不起作用的项目,这种情况尤其浪费(例如,对不使用 ReadyToRun 编译的 netstandard2.0 类库设置 PublishReadyToRun)。

修复方法: 可选方案包括:

  1. 移除额外的全局属性 - 查明哪个父目标/任务注入了该属性,并阻止将其传递给不需要该属性的项目
  2. 使用 RemoveGlobalProperties 元数据 - 在 ProjectReference 项上使用 RemoveGlobalProperties="PublishReadyToRun",在构建被引用项目之前移除该属性
  3. 为属性添加条件 - 仅在确实会使用该属性的项目上设置它(例如,仅用于可执行项目,而非类库)
使用额外全局属性显式调用 <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 技能中的“清理仓库”部分。

评论

支持 Markdown