mcpField
I
incremental-build

incremental-build

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

关于 incremental-build

incremental-build 是一项用于诊断 MSBuild 增量构建行为的技能,涵盖目标意外重跑和 Visual Studio 最新状态检查。它要求通过二进制日志、时间戳、输出跟踪和无改动的重复构建收集证据;除非用户要求修复,否则不修改项目。

主要功能

  1. 01诊断 MSBuild 的 Inputs 和 Outputs
  2. 02调查二进制日志和诊断日志
  3. 03检查时间戳及输出文件是否存在
  4. 04核对批处理输入与输出的映射
  5. 05指导检查 Visual Studio 最新状态
  6. 06验证无改动构建的跳过行为

适用场景

  1. 01查明 MSBuild 目标重跑的原因
  2. 02检查生成输出是否已跟踪并可清理
  3. 03调查 Visual Studio 中的重新构建差异

MCP 相关

该技能将 binlog MCP 服务 Microsoft.AITools.BinlogMcp 指定为检查第二次构建二进制日志的首选方式,并指导使用 load_binlog、search_binlog 等工具。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dotnet/skills --skill incremental-build

安装后在 Claude Code 中输入/incremental-build确认已加载。

详细介绍

必须涵盖的诊断内容

对于每个诊断或解释请求,最终回答必须明确涵盖以下所有要点,即使直接原因只是缺少 Inputs/Outputs:

  • 目标及确切的根因证据;
  • 实际输入/输出时间戳比较;
  • 缺失、已删除、已清理或被重写的输出;
  • 完整的输入,包括动态或易变的输入;
  • 已评估的输入/输出路径,以及批处理列表项时的一对一映射;
  • 两次完全相同且没有更改的构建,以及预期的第二次构建跳过/过期证据。

除非用户要求修复,否则不要进行编辑。如果无法运行某项检查,请将其标记为未经验证的检查,不要省略,也不要声称检查成功。当缺少 Inputs 或 Outputs 时,应将时间戳比较或一对一映射报告为不适用——未启用增量机制,而不是报告为未经验证的比较。

MSBuild 增量构建的工作原理

MSBuild 的增量构建机制可在输出已是最新状态时跳过目标,从而大幅缩短后续构建的时间。

  • 具有 Inputs 和 Outputs 属性的目标:MSBuild 会比较 Inputs 中所有文件与 Outputs 中所有文件的时间戳。如果每个输出文件都比每个输入文件新,则会完全跳过该目标。
  • 不带 Inputs/Outputs:每次调用构建时都会运行该目标。这是默认行为,也是增量构建缓慢最常见的原因。
  • 基于时间戳的比较:MSBuild 使用文件系统时间戳(最后写入时间)来判断文件是否过期。它不使用内容哈希。这意味着触碰文件(仅更新时间戳而不更改内容)会触发重新构建。

<Target Name="Transform"
        Inputs="@(TransformFiles)"
        Outputs="@(TransformFiles->'$(OutputPath)%(Filename).out')">
  
</Target>


<Target Name="PrintMessage">
  <Message Text="This runs every build" />
</Target>

增量构建失效的原因(主要原因)

  1. 自定义目标缺少 Inputs/Outputs — 缺少这两个属性时,目标总会运行。这是导致不必要的重新构建的最常见原因。

  2. Outputs 路径中包含易变属性 — 如果输出路径中包含会在构建之间发生变化的内容(例如时间戳、构建编号或随机 GUID),MSBuild 将永远找不到上一次的输出,并且总会重新构建。

  3. 在受跟踪的 Outputs 之外写入文件 — 如果目标写入的文件未列在其 Outputs 中,MSBuild 就不会识别这些文件。目标可能会被跳过(因为其声明的输出已是最新状态),但下游目标仍可能被触发。

  4. 缺少 FileWrites 注册 — 构建期间创建但未注册到 FileWrites 项组中的文件不会被 dotnet clean 清理。久而久之,过期文件可能会干扰增量检查。

  5. Glob 发生变化 — 添加或删除源文件时,项集(例如 @(Compile))会发生变化。由于这些项会作为 Inputs,输入集也会随之变化并触发重新构建。这是预期行为,但可能令人意外。

  6. 属性发生变化 — 用于 Inputs 或 Outputs 路径的属性(例如 $(Configuration)、$(TargetFramework))在更改时会导致重新构建。Debug 和 Release 之间的切换按设计会触发完整重新构建。

  7. NuGet 包更新 — 更改包版本会更新 project.assets.json,并可能更改许多已解析程序集路径。这会改变 ResolveAssemblyReferences 和 CoreCompile 的输入,从而触发重新构建。

  8. 构建服务器 VBCSCompiler 缓存失效 — Roslyn 编译器服务器(VBCSCompiler)会缓存编译状态。如果服务器被回收(超时、崩溃或手动终止),即使 MSBuild 的增量检查已通过,下次构建也可能会更慢,因为编译器必须重新填充其内存缓存。

诊断“为什么又重新生成了?”

使用二进制日志(binlog)准确了解目标为何执行,而不是被跳过。

在建议修复方案之前,请为每个重新运行的目标报告以下简明清单:

  1. 实际时间戳比较 — 指出最新的已解析输入和最早的已解析输出及其最后写入时间,并说明哪个输入更新。
  2. 输出是否存在 — 确认每个声明的输出在第二次构建之前都存在,且未被删除、清理或其他目标改写。
  3. 输入是否完整 — 验证 Inputs 是否包含所有可能导致输出失效的文件,包括易变依赖项或生成的依赖项。如果某个生产者每次构建都会触碰输入,请检查其内容是否发生变化。修复生产者中不必要的重写(例如,仅在内容发生变化时写入);如果相关内容确实发生了变化,则依赖目标必须重新运行。只有在有证据表明某个文件不会影响输出时,才将其排除,绝不能仅仅因为它经常变化就排除。
  4. 已解析路径是否稳定 — 展开两次构建中的属性和相对路径;输出必须解析到相同位置,且不能包含时间戳、构建编号或 GUID。
  5. 批处理时是否一一对应 — 对于项转换,将每个输入标识映射到其预期输出,并报告缺失、重复或未匹配的对应项。
  6. 无更改验证 — 不做任何编辑,连续运行两次相同的构建,并引用第二次构建的跳过或过期原因。

如果用户要求诊断或解释,除非用户要求修复,否则不要编辑项目。如果无法执行构建,请区分已检查的事实和预期行为,也不要声称已应用或验证修复。

WriteLinesToFile 上的 Overwrite="true" 本身并不是增量目标重新运行的原因。只有在 MSBuild 已安排目标执行后,任务才会重写文件;正确设置 Inputs/Outputs 后,可以在任务执行前跳过目标。

使用 binlog 的分步指南
  1. 不做任何编辑,连续构建两次,并收集第二次构建的结构化证据和文本证据:
    dotnet build /bl:first.binlog
    dotnet build /bl:second.binlog -fl "-flp:v=diag;logfile=second-full.log;performancesummary"
    
    第一次构建用于建立基线。第二次构建才是需要实现增量构建的那次。分析 second.binlog。
首选:binlog MCP

使用 binlog MCP 服务(Microsoft.AITools.BinlogMcp,通过 binlog MCP 命名空间提供)分析第二份 binlog:

  1. 使用 load_binlog 加载 second.binlog。
  2. 使用 search_binlog 搜索 "Building target completely"、"Building target incrementally"、"Skipping target" 和 "is newer than output"。
  3. 使用 get_target_info_by_name 查询重新运行的目标。
  4. 使用 get_project_target_list 确认目标及其项目上下文。
  5. 只有在了解跳过原因后,才使用 get_expensive_targets 对未跳过的目标排序。
后备方案:诊断文本日志(MCP 不可用时)
  1. 读取第二次构建期间捕获的诊断日志。默认后备方案不要再次运行构建,也不要依赖重放支持:

    grep 'Building target\|Target.*was not skipped' second-full.log
    

    在完全实现增量构建的情况下,大多数目标都应被跳过。

  2. 检查未跳过的目标,查找诊断日志中的执行消息。检查表明目标为何运行的“过期”消息。

  3. 查找关键消息,查看 binlog 中的以下内容:

    • "Building target 'X' completely" — 表示 MSBuild 未找到输出,或所有输出都缺失;目标将完整执行。
    • "Building target 'X' incrementally" — 表示部分(而非全部)输出已过期。
    • "Skipping target 'X' because all output files are up-to-date" — 表示目标已正确跳过。
  4. 搜索 "is newer than output" 消息,找出触发重新构建的具体输入文件:

    grep "is newer than output" second-full.log
    

    这会准确显示哪个输入文件的时间戳导致 MSBuild 认为目标已过期。

其他诊断技巧
  • 在 MSBuild Structured Log Viewer 中并排比较 first.binlog 和 second.binlog,查看有哪些变化。
  • 使用 grep 'Target Performance Summary' -A 30 second-full.log,查看第二次构建中耗时最长的目标——这些就是你的优化目标。
  • 检查运行时间为零但仍然执行的目标——它们可能存在不必要的依赖关系,导致自身被执行。

FileWrites 与干净构建

FileWrites 项组是 MSBuild 用于跟踪构建过程中生成文件的机制。它为 dotnet clean 提供支持,并有助于保持增量行为正确。

  • FileWrites 项:将自定义目标创建的任何文件注册到此项中,以便 dotnet clean 知道要删除它们。如果不这样做,生成的文件会在多次构建后不断累积,并可能干扰增量检查。
  • FileWritesShareable 项:对于多个项目共享的文件(例如,共享的生成代码),请使用此项。这些文件会被跟踪,但如果仍有其他项目引用它们,则不会被删除。
  • 如果未注册:文件会在输出目录和中间目录中不断累积。dotnet clean 不会删除这些文件,它们可能导致过期数据问题或干扰最新状态检查。
注册生成文件的模式

在创建生成文件的目标中,将其添加到 FileWrites:

<Target Name="MyGenerator" Inputs="..." Outputs="$(IntermediateOutputPath)generated.cs">
  
  <WriteLinesToFile File="$(IntermediateOutputPath)generated.cs" Lines="@(GeneratedLines)" />

  
  <ItemGroup>
    <FileWrites Include="$(IntermediateOutputPath)generated.cs" />
  </ItemGroup>
</Target>

Visual Studio 快速最新状态检查

Visual Studio 有自己的最新状态检查(Fast Up-to-Date Check,简称 FUTDC),它与 MSBuild 的 Inputs/Outputs 机制相互独立。了解两者的区别,对于诊断“在 VS 中会重新构建,但在命令行中不会”的问题至关重要。

  • VS FUTDC 速度更快,因为它在进程内运行,无需调用 MSBuild,就能检查一组已知项。它会将已知项类型(Compile、Content、EmbeddedResource 等)的时间戳与项目的主要输出进行比较。
  • 它可能出错,如果项目使用了自定义生成操作、用于生成文件的自定义目标,或 FUTDC 不认识的非标准项类型。
  • 禁用 FUTDC,强制 Visual Studio 使用 MSBuild 的完整增量检查:
    <PropertyGroup>
      <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
    </PropertyGroup>
    
  • 诊断 FUTDC 的判断:查看 VS 中的“输出”窗口:转到 Tools → Options → Projects and Solutions → SDK-Style Projects,并将 Up-to-date Checks 日志级别设置为 Verbose 或更高。FUTDC 会准确记录它认为哪些文件已过期。
  • 常见的 VS FUTDC 问题:
    • 自定义生成操作未向 FUTDC 系统注册
    • CopyToOutputDirectory 项比上次构建的时间更新
    • 由 FUTDC 未评估的目标动态添加的项
    • 已修改且 CopyToOutputDirectory="PreserveNewest" 的 Content 或 None 项

让自定义目标支持增量构建

以下是结构良好的增量自定义目标的完整示例:

<Target Name="GenerateConfig"
        Inputs="$(MSBuildProjectFile);@(ConfigInput)"
        Outputs="$(IntermediateOutputPath)config.generated.cs"
        BeforeTargets="CoreCompile">
  
  <WriteLinesToFile File="$(IntermediateOutputPath)config.generated.cs" Lines="..." />
  <ItemGroup>
    <FileWrites Include="$(IntermediateOutputPath)config.generated.cs" />
    <Compile Include="$(IntermediateOutputPath)config.generated.cs" />
  </ItemGroup>
</Target>

此示例的要点:

  • Inputs 包含 $(MSBuildProjectFile):确保项目文件本身发生变化时(例如,影响生成的属性被修改时),目标会重新运行。
  • Inputs 包含 @(ConfigInput):驱动生成的实际源文件。
  • Outputs 使用 $(IntermediateOutputPath):生成的文件存放在 obj/ 目录中,该目录由 MSBuild 管理并会自动清理。
  • BeforeTargets="CoreCompile":确保生成的文件在编译器运行前可用。
  • 注册到 FileWrites:确保 dotnet clean 删除生成的文件。
  • 包含在 Compile 中:将生成的文件添加到编译中,无需在评估时就要求该文件存在。
应避免的常见错误

<Target Name="BadTarget" BeforeTargets="CoreCompile">
  <Exec Command="generate-code.exe" />
</Target>


<Target Name="BadTarget2"
        Inputs="@(Compile)"
        Outputs="$(OutputPath)gen_$([System.DateTime]::Now.Ticks).cs">
  <Exec Command="generate-code.exe" />
</Target>


<Target Name="GoodTarget"
        Inputs="@(Compile)"
        Outputs="$(IntermediateOutputPath)generated.cs"
        BeforeTargets="CoreCompile">
  <Exec Command="generate-code.exe -o $(IntermediateOutputPath)generated.cs" />
  <ItemGroup>
    <FileWrites Include="$(IntermediateOutputPath)generated.cs" />
    <Compile Include="$(IntermediateOutputPath)generated.cs" />
  </ItemGroup>
</Target>

性能摘要和预处理

MSBuild 提供了内置工具,可帮助了解当前运行的内容以及运行原因。

  • /clp:PerformanceSummary — 在生成结束时附加摘要,显示每个目标和任务耗费的时间。可用它快速找出开销最大的操作:

    dotnet build /clp:PerformanceSummary
    

    这会显示按累计时间排序的目标表,方便找出不应该在增量生成中运行的目标。

  • /pp:preprocess.xml — 生成一个内联所有导入内容的 XML 文件,展示经过完整求值的项目。要了解定义了哪些目标、属性和项,以及它们的来源,这个工具非常有用:

    dotnet msbuild /pp:preprocess.xml
    

    搜索预处理后的输出,查找任意目标中 Inputs 和 Outputs 的定义,或了解完整的导入链。

  • 将两者结合使用,了解正在运行的内容(PerformanceSummary)和已导入的内容(/pp),再与 binlog 分析交叉对照,便可全面了解情况。

常见修复方法

  • 始终为自定义目标添加 Inputs 和 Outputs — 这是提升增量生成性能最有效的单项改动。缺少这两个属性时,目标每次都会运行。
  • 使用 $(IntermediateOutputPath) 存放生成的文件 — obj/ 中的文件会由 MSBuild 的清理基础架构跟踪,不会在不同配置之间残留。
  • 在 FileWrites 中注册生成的文件 — 确保 dotnet clean 会将其删除,避免累积过时文件。
  • 避免生成器中不必要的易变内容 — 除非有需要,否则不要在文件路径或生成内容中嵌入时间戳、随机值或生成计数器。当内容未变化时,保留生成文件的时间戳(例如,使用 WriteLinesToFile 配合 Overwrite="true" WriteOnlyWhenDifferent="true")。在 Inputs 中保留实际依赖项;如果必需的易变内容发生变化,则接受依赖目标重新生成。只有在证明某个文件不会影响输出后,才将其排除。
  • 需要传递项但不创建增量生成依赖时,使用 Returns 而不是 Outputs — Outputs 有双重作用:定义增量检查,并返回目标产生的项。如果只需将项传递给调用目标而不影响增量性,请改用 Returns:
    
    <Target Name="GetFiles" Outputs="@(DiscoveredFiles)">...</Target>
    
    
    <Target Name="GetFiles" Returns="@(DiscoveredFiles)">...</Target>
    

评论

支持 Markdown