ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MSBuild 增量构建实战:从 binlog 定位“什么都没改却总是重新编译“的 8 大根因

MSBuild 增量构建实战:从 binlog 定位“什么都没改却总是重新编译“的 8 大根因 MSBuild 增量构建实战从 binlog 定位什么都没改却总是重新编译的 8 大根因【免费下载链接】skillsRepository for skills to assist AI coding agents with .NET and C#项目地址: https://gitcode.com/GitHub_Trending/skills17/skills增量构建Incremental Build是 MSBuild 最容易被忽视、也最常被误伤的性能机制它让目标Target在输出已是最新时被整体跳过从而大幅缩短后续构建时间。但在真实项目中明明什么都没改构建却照跑不误几乎每天都在发生。本篇以 incremental-build Skill 为骨架结合 dotnet-msbuild 插件 中配套的 binlog 生成、binlog 分析、Target 编写等 Skill 与评估用例系统讲解 MSBuild 增量构建的工作原理、导致增量失效的 8 大根因、基于 binlog 的为什么又重编了诊断方法论以及把自定义 Target 改造成真正增量化的完整实践。读完你将能独立回答第二次构建到底该不该重编、为什么重编、如何让它不再重编。增量构建的工作原理MSBuild 靠什么决定跳过MSBuild 的增量构建机制允许目标在输出已是最新时被跳过这是后续构建大幅提速的根本来源。它的决策模型非常朴素完全基于Inputs/Outputs属性与文件时间戳声明了Inputs和Outputs的目标MSBuild 会比较Inputs中所有文件的时间戳与Outputs中所有文件的时间戳。如果每个输出文件都比每个输入文件新该目标就会被整体跳过没有声明Inputs/Outputs的目标每次构建调用都会执行。这是默认行为也是增量构建变慢最常见的原因Incremental属性目标可以显式选择加入或退出增量行为。Incrementalfalse会强制目标每次运行即使指定了Inputs/Outputs也无效基于时间戳而非内容哈希MSBuild 使用文件系统的时间戳最后写入时间判断过期与否不做内容哈希。因此仅仅触碰一个文件更新时间戳而未改内容就会触发重新构建。一个典型的增量目标与一个永远全量执行的目标对比如下!-- 这个目标是增量的当 Output 比所有 Input 都新时被跳过 -- Target NameTransform Inputs(TransformFiles) Outputs(TransformFiles-$(OutputPath)%(Filename).out) !-- work here -- /Target !-- 这个目标因为没有 Inputs/Outputs 每次都会运行 -- Target NamePrintMessage Message TextThis runs every build / /Target理解这条规则是后续一切诊断的前提凡是没有Inputs/Outputs的 Target就不可能被跳过。仓库中 incremental-build 的评估用例 第一条 rubric 正是要求识别自定义目标缺少 Inputs 和 Outputs 属性——这是该 Skill 的训练与考核核心。增量构建失效的 8 大根因实践中增量构建悄悄失效的原因高度集中本 Skill 归纳为以下 8 类几乎覆盖了绝大多数真实场景自定义 Target 缺少Inputs/Outputs——两个属性缺一目标就永远执行。这是不必要重建的头号原因。Outputs路径中的易变volatile属性——如果输出路径包含每次构建都变化的内容时间戳、构建号、随机 GUIDMSBuild 永远找不到上一次的输出于是永远重建。写入位置超出了被跟踪的Outputs——目标写了文件但没写进它的OutputsMSBuild 对这些文件一无所知目标可能因为声明的输出已最新而被跳过但下游目标仍可能被触发。缺少FileWrites注册——构建期间创建但未注册到FileWrites项组的文件dotnet clean不会清理。久而久之陈旧文件堆积反而干扰增量检查。Glob 变化——增删源文件会使项集如(Compile)变化。由于这些项会流入Inputs输入集合改变就会触发重建。这是预期行为但常常令人意外。属性变化——流入Inputs或Outputs路径的属性如$(Configuration)、$(TargetFramework)改变会导致重建。Debug 与 Release 之间切换本来就是一次完整重建属设计使然。NuGet 包更新——改包版本会更新project.assets.json并可能改变大量已解析的程序集路径从而改变ResolveAssemblyReferences和CoreCompile的输入触发重建。构建服务器 VBCSCompiler 缓存失效——Roslyn 编译器服务器VBCSCompiler会缓存编译状态。如果服务器被回收超时、崩溃或手动杀掉即使 MSBuild 的增量检查通过下一次构建仍可能更慢因为编译器必须重新填充内存缓存。前 4 类属于自定义构建逻辑写错是本 Skill 的主战场后 4 类属于输入集合本质变化多数是预期行为重点是能识别出来、避免误诊。需要强调的是这类问题与求值evaluation阶段性能问题有明确边界若慢在编译开始之前应转向 eval-performance Skill其 description 中明确标注增量构建问题请使用 incremental-build。诊断为什么重编了binlog 三步走不要凭感觉猜。用二进制日志binlog精确还原哪些目标执行了、为什么执行。第一步连续构建两次各留一份 binlogdotnet build /bl:first.binlog dotnet build /bl:second.binlog第一次构建建立基线第二次构建才是你期望增量的那次——分析second.binlog。关于 binlog 的生成细节例如用{}占位符自动生成唯一文件名、PowerShell 下需转义为{{}}、以及为什么禁止裸用/bl详见配套的 binlog-generation Skill该 Skill 强调每一次 MSBuild 调用都要单独传/bl:{}保证多个配置、多次重试的日志互不覆盖为增量对比提供可靠素材。第二步首选 binlog MCP 工具本插件随 dotnet-msbuild 一同提供binlog MCP serverMicrosoft.AITools.BinlogMcp暴露在binlogMCP 命名空间下无需把二进制日志转成文本即可结构化查询用 overview 工具查看整体构建状态与耗时用 search 工具查找执行与跳过的目标——搜索Building target completely、Building target incrementally、Skipping target用 search 工具查找is newer than output消息定位究竟是哪个输入文件触发了重建用 target 相关工具target_reasons、project_targets检查特定目标为何运行用expensive_targets工具找出第二次构建中耗时最长的目标——它们就是你的优化对象。该流程与 binlog-failure-analysis Skill 的约束一致binlog 是二进制格式绝不能cat、head、strings直接读只能通过 MCP 工具查询。第三步MCP 不可用时的回退文本日志回放在旧 SDK 或离线环境无法启动 MCP 服务器时把第二个 binlog 回放为诊断文本日志dotnet msbuild second.binlog -noconlog -fl -flp:vdiag;logfilesecond-full.log;performancesummaryPowerShell 下需将分号参数整体加引号-flp:vdiag;logfilesecond-full.log;performancesummary。然后搜索实际执行过的目标grep Building target\|Target.*was not skipped second-full.log在理想的增量构建中绝大多数目标应当被跳过。再检查未跳过的目标对应的执行消息并重点查看三类关键消息Building target X completely—— MSBuild 没找到任何输出或输出全部缺失属于完整执行Building target X incrementally—— 部分输出已过期Skipping target X because all output files are up-to-date—— 目标被正确跳过。最后用下面这条命令找出具体是哪个输入文件过期grep is newer than output second-full.log它会精确暴露是哪个输入文件的时间戳导致 MSBuild 判定目标过期。其他辅助诊断手段把first.binlog与second.binlog在 MSBuild Structured Log Viewer 中并排对比观察两次构建的差异用grep Target Performance Summary -A 30 second-full.log查看第二次构建中耗时最长的目标这些就是优化对象留意零耗时但仍然执行的目标——它们可能带着不必要的依赖链导致整条链被连带执行。FileWrites 与 Clean让生成文件被看见、被清理FileWrites项组是 MSBuild 跟踪构建期生成文件的机制它支撑dotnet clean也帮助维持正确的增量行为。FileWrites项自定义目标创建的每个文件都应注册dotnet clean才知道要删除它。不注册的话生成文件会跨构建累积并可能干扰增量检查FileWritesShareable项用于跨多个项目共享的文件如共享生成代码。这些文件会被跟踪但如果其他项目仍引用它们则不会删除不注册的后果文件堆积在输出目录与中间目录dotnet clean不会移除它们可能造成陈旧数据或混淆 up-to-date 检查。注册生成文件的推荐模式是在创建它的目标内部就地添加Target NameMyGenerator Inputs... Outputs$(IntermediateOutputPath)generated.cs !-- Generate the file -- WriteLinesToFile File$(IntermediateOutputPath)generated.cs Lines(GeneratedLines) / !-- Register for clean -- ItemGroup FileWrites Include$(IntermediateOutputPath)generated.cs / /ItemGroup /Target配套的 including-generated-files Skill 进一步解释了深层原因文件在构建执行阶段才生成而 glob 在求值阶段就已展开所以执行期创建的文件天然不可见必须手动加入Compile/FileWrites并在正确的BeforeTargets时机生成源码用CoreCompile;BeforeCompile非代码文件用BeforeBuild兜底用AssignTargetPaths注入。该 Skill 还给出了 glob 行为对照表目标外部的 glob 只能捕获求值期可见的文件目标内部的 glob 才能捕获执行期已生成的文件——这正是把ItemGroup放进Target的原因。同时它强调始终使用$(IntermediateOutputPath)而不是硬编码obj\$(Configuration)\$(TargetFramework)\因为中间输出路径可能被重定向共享输出目录、CI 环境。区分 VS 的 Fast Up-to-Date CheckFUTDCVisual Studio 有一套独立于 MSBuildInputs/Outputs机制的自身 up-to-date 检查Fast Up-to-Date CheckFUTDC。不理解两者的区别就无法诊断VS 里重编、命令行却不重编这类诡异问题。FUTDC 更快它进程内运行不调用 MSBuild只把一组已知项类型Compile、Content、EmbeddedResource等的时间戳与项目主输出比较它可能判错如果项目使用自定义构建操作、生成文件的自定义目标、或 FUTDC 不认识的非标准项类型检查结果就会失真关闭 FUTDC强制 VS 走 MSBuild 的完整增量检查PropertyGroup DisableFastUpToDateChecktrue/DisableFastUpToDateCheck /PropertyGroup诊断 FUTDC 的决策在 VS 中打开工具 → 选项 → 项目和解决方案 → SDK 风格项目把Up-to-date 检查的日志级别设为Verbose或更高。FUTDC 会逐条记录它认为过期的具体文件VS FUTDC 常见问题自定义构建操作未注册到 FUTDC 系统比上次构建更新的CopyToOutputDirectory项由目标动态添加、FUTDC 未求值的项带CopyToOutputDirectoryPreserveNewest且被修改过的Content或None项。编写真正增量的自定义 Target完整示例与常见错误下面是一个结构良好的增量自定义目标完整示例Target NameGenerateConfig Inputs$(MSBuildProjectFile);(ConfigInput) Outputs$(IntermediateOutputPath)config.generated.cs BeforeTargetsCoreCompile !-- Generate file only if inputs changed -- 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 管理并自动清理BeforeTargetsCoreCompile保证编译前生成文件已就绪FileWrites注册保证dotnet clean能删除生成文件Compile加入把生成文件纳入编译且不要求在求值期文件已存在。这与 target-authoring Skill 的规范完全同源它给出了 Build→CoreBuild 三级目标链、$(XxxDependsOn)链式追加追加而非覆盖覆盖会悄悄丢掉 SDK 目标、以及DependsOnTargets/BeforeTargets/AfterTargets的选择表——当你不拥有某条流水线时用BeforeTargets/AfterTargets注入BeforeTargetsCoreCompile优先于改$(CompileDependsOn)。此外该 Skill 还明确了目标命名约定_Xxx内部目标、CoreXxx实现、BeforeXxx/AfterXxx空扩展钩子、GetXxx轻量查询并警示在.props中定义目标会导致BeforeTargets无从挂钩目标应放进.targets。常见错误对照!-- BAD: 没有 Inputs/Outputs —— 每次构建都运行 -- Target NameBadTarget BeforeTargetsCoreCompile Exec Commandgenerate-code.exe / /Target !-- BAD: 易变输出路径 —— 永远找不到上一次的输出 -- Target NameBadTarget2 Inputs(Compile) Outputs$(OutputPath)gen_$([System.DateTime]::Now.Ticks).cs Exec Commandgenerate-code.exe / /Target !-- GOOD: 稳定路径、注册输出 -- Target NameGoodTarget Inputs(Compile) Outputs$(IntermediateOutputPath)generated.cs BeforeTargetsCoreCompile Exec Commandgenerate-code.exe -o $(IntermediateOutputPath)generated.cs / ItemGroup FileWrites Include$(IntermediateOutputPath)generated.cs / Compile Include$(IntermediateOutputPath)generated.cs / /ItemGroup /TargetBadTarget2中的$([System.DateTime]::Now.Ticks)是典型反例输出文件名每次构建都不同增量检查永远找不到上一次的输出于是退化为全量执行。这也呼应了避免在构建中嵌入易变数据的总原则。PerformanceSummary 与 Preprocess两条内置透视工具MSBuild 自带两个低成本工具帮助你快速理解什么在跑、为什么跑/clp:PerformanceSummary—— 在构建结束时追加摘要展示每个目标和任务花费的时间。快速定位最昂贵的操作dotnet build /clp:PerformanceSummary它会输出一张按累计耗时排序的目标表方便你一眼找出增量构建里根本不该运行的目标。该开关同样适用于回放场景前述performancesummary参数即其日志版。/pp:preprocess.xml—— 生成一个内联了全部导入的单一 XML即完全求值后的项目。对理解哪些目标、属性、项被定义以及来自哪里价值极大dotnet msbuild /pp:preprocess.xml在预处理输出中搜索任意目标的Inputs/Outputs定义或理清整条导入链。该工具与 eval-performance Skill 的用法一致预处理输出超过约 1 万行通常意味着求值负担偏重可配合其求值五阶段模型进一步定位。两者结合使用用PerformanceSummary看什么在跑用/pp看什么被导入再与 binlog 分析交叉印证即可拼出完整图景。常见修复清单永远给自定义 Target 添加Inputs和Outputs——这是对增量构建性能影响最大的一步。缺任何一个属性目标都会每次执行生成文件使用$(IntermediateOutputPath)——obj/下的文件由 MSBuild 的清理基础设施跟踪不会跨配置泄漏在FileWrites中注册生成文件——保证dotnet clean删除它们防止陈旧文件堆积避免构建中的易变数据——不要把时间戳、随机值、构建计数器嵌进文件路径或生成内容除非你有刻意设计的过期管理策略。确需使用易变数据时把它隔离到单一文件最小化下游影响需要传递项但不想建立增量依赖时用Returns而非Outputs——Outputs身兼两职既定义增量检查又作为目标返回的项。如果只是想给调用方传项而不影响增量性用Returns!-- Outputs: 既影响增量检查也影响返回值 -- Target NameGetFiles Outputs(DiscoveredFiles).../Target !-- Returns: 只影响返回值不参与增量检查 -- Target NameGetFiles Returns(DiscoveredFiles).../Target这一点在 target-authoring Skill 中被进一步强调在查询目标GetTargetPath、GetTargetFrameworks上误用Outputs会导致目标看似最新而被跳过、返回陈旧数据查询目标应始终使用Returns。收尾用两次构建验证修复效果修复完成后回到诊断的第一步做闭环验证再次连续执行两次构建并各留一份 binlog在第二次构建的日志或 MCP 查询结果中确认自定义目标出现了Skipping target ... because all output files are up-to-date且grep is newer than output不再命中预期外的输入文件。这正是 incremental-build 评估用例 的最终 rubric——构建两次验证增量性第二次应跳过该目标——也是本 Skill 期望 Agent 交付的落地标准。当第二次构建仍然全量重编时按 8 大根因逐一对照排查配合 binlog-failure-analysis 与 target-authoring 等姊妹 Skill 协同分析绝大多数增量失效问题都能在几分钟内定位并修复。【免费下载链接】skillsRepository for skills to assist AI coding agents with .NET and C#项目地址: https://gitcode.com/GitHub_Trending/skills17/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表