)
MSBuild 增量构建入门为自定义 Target 补齐Inputs与Outputsmsbuild-antipatterns 技能实战【免费下载链接】skillsRepository for skills to assist AI coding agents with .NET and C#项目地址: https://gitcode.com/GitHub_Trending/skills17/skills导读本文围绕 dotnet-msbuild 插件msbuild-antipatterns技能中的核心反模式AP-11自定义 Target 缺少Inputs/Outputs系统讲解 MSBuild 增量构建的基本原理、正确的目标编写姿势以及生成文件如何被dotnet clean正确追踪。读完本文你将掌握如何判断一个自定义 Target 是否破坏了增量构建、如何用Inputs/Outputs/FileWrites写出可被 MSBuild 正确跳过与清理的生成步骤以及如何借助 binlog 定位什么都没改却重新构建的根因。本文的理论骨架取自仓库中的参考资料 incremental-build-inputs-outputs.md并融合了同仓库中 incremental-build/SKILL.md 与 target-authoring/SKILL.md 两套技能文档的纵深内容所有结论均可在仓库文件中复核。为什么自定义 Target 必须声明Inputs和OutputsMSBuild 的增量构建机制允许一个 Target 在输出已经是最新时被整体跳过从而显著缩短后续构建的时间。其判定方式非常朴素比较文件时间戳。当 Target 同时声明了Inputs与Outputs后MSBuild 会比较所有输入文件与所有输出文件的最后写入时间——如果每个输出文件都比每个输入文件新该 Target 就被跳过否则就执行。关键结论来自 incremental-build/SKILL.md同时声明Inputs和OutputsMSBuild 依据时间戳比较决定是否跳过缺失二者之一或全部缺失Target 在每次构建被调用时都会执行这是默认行为也是增量构建变慢的最常见原因Incremental属性可以显式控制。Incrementalfalse会强制 Target 即使声明了Inputs/Outputs也总是执行时间戳而非内容哈希MSBuild 比较的是文件系统时间戳最后写入时间不比较内容。因此仅仅touch一个文件更新时间戳但内容未变也会触发重新构建。在 msbuild-antipatterns/SKILL.md 中这一条被编号为AP-11SmellTarget NameMyTarget BeforeTargetsBuild且没有Inputs/Outputs属性。Why its badTarget 在每次构建时都会运行即使没有任何变化这破坏了增量构建并拖慢 no-op 构建。在 additional-antipatterns.md 的快速检查清单中AP-11 的严重级别被标注为 性能回归。AP-11 的典型修复从每次都跑到最新即跳过以下示例完整摘自 incremental-build-inputs-outputs.md是 AP-11 的标准 BAD→GOOD 对照!-- BAD: Runs every time -- Target NameGenerateBuildInfo BeforeTargetsCoreCompile WriteLinesToFile File$(IntermediateOutputPath)BuildInfo.g.cs Lines// Generated at $(Version) Overwritetrue / /Target !-- GOOD: Skipped when up-to-date -- Target NameGenerateBuildInfo BeforeTargetsCoreCompile Inputs$(MSBuildProjectFile) Outputs$(IntermediateOutputPath)BuildInfo.g.cs WriteLinesToFile File$(IntermediateOutputPath)BuildInfo.g.cs Lines// Generated at $(Version) Overwritetrue / ItemGroup FileWrites Include$(IntermediateOutputPath)BuildInfo.g.cs / Compile Include$(IntermediateOutputPath)BuildInfo.g.cs / /ItemGroup /Target这里值得注意的细节是BAD 版本中WriteLinesToFile每次写入的文件内容包含$(Version)但$(Version)通常并不变化——真正的问题是 Target 没有声明任何增量信息MSBuild 每次构建都必须重新生成并重写文件。GOOD 版本通过Inputs$(MSBuildProjectFile)让项目文件本身成为唯一输入只要项目文件没变目标就被跳过BuildInfo.g.cs根本不会被重写。四个关键点逐项拆解GOOD 版本中隐藏着四条编写增量 Target 的黄金规则逐一展开1.Inputs应包含$(MSBuildProjectFile)及驱动生成的源文件Inputs定义了什么变化才需要重新生成。$(MSBuildProjectFile)代表当前项目文件本身——如果项目文件里改了某个影响生成内容的属性比如$(Version)Target 应当重新运行。更完整的写法还会追加真正的生成输入源Target NameGenerateConfig Inputs$(MSBuildProjectFile);(ConfigInput) Outputs$(IntermediateOutputPath)config.generated.cs BeforeTargetsCoreCompile(ConfigInput)是驱动生成的实际源文件集合例如 JSON 配置、模板等。将项目文件与源文件一并列入Inputs是 incremental-build/SKILL.md 中Making Custom Targets Incremental一节的推荐形态。此外target-authoring/SKILL.md 的完整模板还使用了更宽泛的$(MSBuildAllProjects)涵盖所有参与评估的导入文件适合需要响应任何导入文件变化的场景。2.Outputs应使用$(IntermediateOutputPath)让生成文件进入obj/Outputs是增量检查的对照物同时定义了 Target 产出的文件。规范要求把生成文件放在$(IntermediateOutputPath)即obj/config/tfm/目录下原因有二中间目录由 MSBuild 的清理基础设施统一管理不会在多个配置间互相泄漏obj/天然属于可再生成的构建产物与源码目录隔离避免污染版本控制。一个需要避免的陷阱是Outputs 路径中包含易变值时间戳、随机 GUID、构建号等。例如!-- BAD: Volatile output path — never finds previous output -- Target NameBadTarget2 Inputs(Compile) Outputs$(OutputPath)gen_$([System.DateTime]::Now.Ticks).cs Exec Commandgenerate-code.exe / /Target如果输出路径每次构建都不同MSBuild 永远找不到上一次的输出于是每次都判定过期、每次都重建——增量机制形同虚设。这是 incremental-build/SKILL.md 列出的破坏增量构建的 8 大原因之一。3.FileWrites注册确保dotnet clean能删除生成文件FileWrites是 MSBuild 追踪构建期间创建的文件的 Item 组它驱动dotnet clean的行为并维护增量检查的正确性FileWrites注册自定义 Target 创建的任何文件dotnet clean才知道要删除它们FileWritesShareable用于跨项目共享的文件如共享生成代码被追踪但不会被随意删除如果不注册生成文件会在输出与中间目录中不断累积dotnet clean不会清理它们残留的过期文件还可能干扰后续的 up-to-date 检查。注册模式非常简单——在创建该文件的 Target 内部把文件加入FileWritesTarget 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 /Target4.Compile包含让生成文件参与编译且无需在评估期存在在同一个ItemGroup中追加Compile Include$(IntermediateOutputPath)BuildInfo.g.cs /的作用是把生成文件纳入 C# 编译集合而不需要它在评估阶段就已经存在。BeforeTargetsCoreCompile保证了文件在编译器运行前生成完毕。若省略这一步即使文件生成了编译器也不会把它编译进程序集——这正是代码生成器场景中最常见的遗漏。增量构建被破坏的常见原因清单除了 AP-11 本身incremental-build/SKILL.md 归纳了 8 类最常见的破坏因素方便排查时对照自定义 Target 缺少 Inputs/Outputs—— 最普遍的原因即本文主题 AP-11Outputs 路径含易变属性—— 时间戳、构建号、随机 GUID 导致永远找不到上一次输出文件写在了 Outputs 之外—— Target 写了未被声明的文件MSBuild 不知道它们的存在缺少 FileWrites 注册——dotnet clean无法清理过期文件累积Glob 集合变化—— 增删源文件使(Compile)输入集变化触发重建属预期行为属性变化——$(Configuration)、$(TargetFramework)等参与 Inputs/Outputs 路径的属性变化会触发重建Debug/Release 切换本身就是全量重建NuGet 包更新——project.assets.json与程序集解析路径变化触发ResolveAssemblyReferences与CoreCompile重建VBCSCompiler 缓存失效—— Roslyn 编译服务器被回收后即使 MSBuild 增量检查通过编译本身仍需重新预热。诊断用 binlog 回答为什么又重建了排查增量构建问题最有效的工具是二进制日志binlog。标准流程是连续构建两次分析第二次dotnet build /bl:first.binlog dotnet build /bl:second.binlog第二次构建应当是增量的分析second.binlog时重点寻找三类关键消息incremental-build/SKILL.md 原文Building target X completely—— MSBuild 找不到任何输出或输出全部缺失Target 全量执行Building target X incrementally—— 部分输出过期Skipping target X because all output files are up-to-date—— Target 被正确跳过。在无 MCP 工具的兜底场景下可将 binlog 回放为诊断文本日志dotnet msbuild second.binlog -noconlog -fl -flp:vdiag;logfilesecond-full.log;performancesummary然后搜索实际执行的 Target 与触发原因grep Building target\|Target.*was not skipped second-full.log grep is newer than output second-full.logis newer than output消息会精确指出哪一份输入文件的哪个时间戳导致 Target 被判为过期。此外dotnet build /clp:PerformanceSummary可输出各 Target 的耗时汇总dotnet msbuild /pp:preprocess.xml可内联所有导入、看到任意 Target 的Inputs/Outputs定义来源两者常与 binlog 配合使用。Outputs与Returns不要把两个职责混在一起Outputs承担了双重职责既定义增量检查又定义 Target 返回给调用方的项。当只需要向调用方传递项、而不想引入增量构建依赖时应使用Returnsincremental-build/SKILL.md 与 target-authoring/SKILL.md 均强调此点!-- Outputs: affects incremental check AND return value -- Target NameGetFiles Outputs(DiscoveredFiles).../Target !-- Returns: only affects return value, no incremental check -- Target NameGetFiles Returns(DiscoveredFiles).../Target特别地查询类 Target如GetTargetPath、GetTargetFrameworks必须使用Returns而不是Outputs若用Outputs声明MSBuild 会因up-to-date而跳过它们向调用方返回陈旧数据。Returns仅影响返回值不参与增量判定target-authoring/SKILL.md 将其列为查询 Target 的标准写法。Visual Studio 的 Fast Up-to-Date Check 与命令行差异Visual Studio 拥有独立于 MSBuild 的快速最新检查FUTDC它运行在进程内不调用 MSBuild仅对一组已知 Item 类型Compile、Content、EmbeddedResource等与项目主输出做时间戳比较。因此可能出现命令行不重建、VS 里却重建的割裂现象。FUTDC 的常见失效场景包括自定义构建动作未注册到 FUTDC、CopyToOutputDirectory项比上次构建新、Target 动态添加的项 FUTDC 无法评估等。如需强制 VS 退回 MSBuild 的完整增量检查可设置PropertyGroup DisableFastUpToDateChecktrue/DisableFastUpToDateCheck /PropertyGroup诊断 FUTDC 决策时可在 VS 中打开工具 → 选项 → 项目和解决方案 → SDK 风格项目将Up-to-date Checks的日志级别调至Verbose或更高FUTDC 会输出它判定为过期的具体文件。一个可直接套用的完整增量 Target 模板综合上述要点incremental-build/SKILL.md 给出了生产可用的完整形态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而 target-authoring/SKILL.md 的Complete Custom Target Template进一步展示了将增量核心实现嵌入DependsOn链的规范分层外层的MyFeature用Returns做跨项目通信内部的CoreMyFeature声明Inputs/Outputs负责增量与文件注册前后通过BeforeMyFeature/AfterMyFeature空钩子提供扩展点并用_ValidateMyFeatureInputs在链首做输入校验。在仓库中的定位与延伸阅读本文主题对应的反模式条目msbuild-antipatterns/SKILL.md 中的AP-11其 Smell/Why its bad 定义与本文一致本文直接取材的参考资料incremental-build-inputs-outputs.md增量构建深度指南incremental-build/SKILL.md覆盖 8 大破坏原因、binlog 诊断流程、FUTDC、ReturnsvsOutputs自定义 Target 编写的规范分层target-authoring/SKILL.md该技能的能力评测与验收标准见 tests/dotnet-msbuild/msbuild-antipatterns/eval.yaml其中包含对LibA 的 publish-on-build 目标用MSBuild任务以路径无关的全局属性再次调用自身、分叉出共享输出路径的重复实例等更深层构建缺陷的检测条目可作为排查同类问题的延伸参考。一句话总结为每个自定义 Target 同时声明Inputs与Outputs把生成文件放到$(IntermediateOutputPath)并注册进FileWrites与Compile——这是让 MSBuild 增量构建重新生效、让dotnet clean尽职尽责的最低成本修复也是 AP-11 反模式给出的最终答案。【免费下载链接】skillsRepository for skills to assist AI coding agents with .NET and C#项目地址: https://gitcode.com/GitHub_Trending/skills17/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考