incremental-build
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHow MSBuild Incremental Build Works
MSBuild增量构建的工作原理
MSBuild's incremental build mechanism allows targets to be skipped when their outputs are already up to date, dramatically reducing build times on subsequent runs.
- Targets with and
Inputsattributes: MSBuild compares the timestamps of all files listed inOutputsagainst all files listed inInputs. If every output file is newer than every input file, the target is skipped entirely.Outputs - Without /
Inputs: The target runs every time the build is invoked. This is the default behavior and the most common cause of slow incremental builds.Outputs - attribute on targets: Targets can explicitly opt in or out of incremental behavior. Setting
Incrementalforces the target to always run, even ifIncremental="false"andInputsare specified.Outputs - Timestamp-based comparison: MSBuild uses file system timestamps (last write time) to determine staleness. It does not use content hashes. This means touching a file (updating its timestamp without changing content) will trigger a rebuild.
xml
<!-- This target is incremental: skipped if Output is newer than all Inputs -->
<Target Name="Transform"
Inputs="@(TransformFiles)"
Outputs="@(TransformFiles->'$(OutputPath)%(Filename).out')">
<!-- work here -->
</Target>
<!-- This target always runs because it has no Inputs/Outputs -->
<Target Name="PrintMessage">
<Message Text="This runs every build" />
</Target>MSBuild的增量构建机制允许在输出文件已处于最新状态时跳过目标执行,大幅缩短后续构建的耗时。
- 带有和
Inputs属性的目标:MSBuild会将Outputs中列出的所有文件的时间戳与Inputs中的所有文件进行比较。如果每个输出文件的时间戳都晚于所有输入文件,该目标将被完全跳过。Outputs - 无/
Inputs的目标:每次触发构建时该目标都会运行。这是默认行为,也是增量构建速度慢的最常见原因。Outputs - 目标的属性:目标可以显式启用或禁用增量行为。设置
Incremental会强制目标始终运行,即使已指定Incremental="false"和Inputs。Outputs - 基于时间戳的比较:MSBuild使用文件系统时间戳(最后写入时间)来判断文件是否过期,不使用内容哈希。这意味着仅修改文件时间戳(不更改内容)也会触发重建。
xml
<!-- This target is incremental: skipped if Output is newer than all Inputs -->
<Target Name="Transform"
Inputs="@(TransformFiles)"
Outputs="@(TransformFiles->'$(OutputPath)%(Filename).out')">
<!-- work here -->
</Target>
<!-- This target always runs because it has no Inputs/Outputs -->
<Target Name="PrintMessage">
<Message Text="This runs every build" />
</Target>Why Incremental Builds Break (Top Causes)
增量构建失效的常见原因
-
Missing Inputs/Outputs on custom targets — Without both attributes, the target always runs. This is the single most common cause of unnecessary rebuilds.
-
Volatile properties in Outputs path — If the output path includes something that changes between builds (e.g., a timestamp, build number, or random GUID), MSBuild will never find the previous output and will always rebuild.
-
File writes outside of tracked Outputs — If a target writes files that aren't listed in its, MSBuild doesn't know about them. The target may be skipped (because its declared outputs are up to date), but downstream targets may still be triggered.
Outputs -
Missing FileWrites registration — Files created during the build but not registered in theitem group won't be cleaned by
FileWrites. Over time, stale files can confuse incremental checks.dotnet clean -
Glob changes — When you add or remove source files, the item set (e.g.,) changes. Since these items feed into
@(Compile), the set of inputs changes and triggers a rebuild. This is expected behavior but can be surprising.Inputs -
Property changes — Properties that feed intoor
Inputspaths (e.g.,Outputs,$(Configuration)) will cause rebuilds when changed. Switching between Debug and Release is a full rebuild by design.$(TargetFramework) -
NuGet package updates — Changing a package version updatesand potentially many resolved assembly paths. This changes the inputs to
project.assets.jsonandResolveAssemblyReferences, triggering a rebuild.CoreCompile -
Build server VBCSCompiler cache invalidation — The Roslyn compiler server () caches compilation state. If the server is recycled (timeout, crash, or manual kill), the next build may be slower even though MSBuild's incremental checks pass, because the compiler must repopulate its in-memory caches.
VBCSCompiler
-
自定义目标缺少Inputs/Outputs — 若未同时配置这两个属性,目标会每次都运行。这是导致不必要重建的最常见原因。
-
输出路径包含易变属性 — 如果输出路径包含构建间会变化的内容(如时间戳、构建编号或随机GUID),MSBuild将永远无法找到之前的输出文件,从而每次都会触发重建。
-
在追踪的Outputs之外写入文件 — 如果目标写入的文件未列入其,MSBuild将无法识别这些文件。即使该目标因声明的输出已更新而被跳过,下游目标仍可能被触发。
Outputs -
未注册FileWrites — 构建过程中创建但未注册到项组的文件,无法通过
FileWrites清理。随着时间推移,过期文件会干扰增量检查。dotnet clean -
通配符匹配变化 — 当添加或移除源文件时,项集(如)会发生变化。由于这些项会作为
@(Compile)的输入,输入集的变化会触发重建。这是预期行为,但可能会让用户感到意外。Inputs -
属性变化 — 影响或
Inputs路径的属性(如Outputs、$(Configuration))发生变化时,会触发重建。在Debug和Release模式间切换会触发完整重建,这是设计使然。$(TargetFramework) -
NuGet包更新 — 更改包版本会更新,并可能改变许多已解析程序集的路径。这会导致
project.assets.json和ResolveAssemblyReferences的输入发生变化,从而触发重建。CoreCompile -
构建服务器VBCSCompiler缓存失效 — Roslyn编译器服务器()会缓存编译状态。如果服务器被回收(超时、崩溃或手动终止),即使MSBuild的增量检查通过,下一次构建也可能变慢,因为编译器需要重新填充内存中的缓存。
VBCSCompiler
Diagnosing "Why Did This Rebuild?"
诊断「为何触发了重建?」
Use binary logs (binlogs) to understand exactly why targets ran instead of being skipped.
使用二进制日志(binlog)可以准确了解目标未被跳过而运行的原因。
Step-by-step using binlog
使用binlog的分步诊断流程
-
Build twice with binlogs to capture the incremental build behavior:shell
dotnet build /bl:first.binlog dotnet build /bl:second.binlogThe first build establishes the baseline. The second build is the one you want to be incremental. Analyze.second.binlog -
Replay the second binlog to a diagnostic text log:shell
dotnet msbuild second.binlog -noconlog -fl -flp:v=diag;logfile=second-full.log;performancesummaryThen search for targets that actually executed:bashgrep 'Building target\|Target.*was not skipped' second-full.logIn a perfectly incremental build, most targets should be skipped. -
Inspect non-skipped targets by looking for their execution messages in the diagnostic log. Check for "out of date" messages that indicate why a target ran.
-
Look for key messages in the binlog:
- — means MSBuild found no outputs or all outputs are missing; this is a full target execution.
"Building target 'X' completely" - — means some (but not all) outputs are out of date.
"Building target 'X' incrementally" - — target was correctly skipped.
"Skipping target 'X' because all output files are up-to-date"
-
Search for "is newer than output" messages to find the specific input file that triggered the rebuild:bash
grep "is newer than output" second-full.logThis reveals exactly which input file's timestamp caused MSBuild to consider the target out of date.
-
两次构建并生成binlog,以捕获增量构建行为:shell
dotnet build /bl:first.binlog dotnet build /bl:second.binlog第一次构建建立基准,第二次构建是需要分析的增量构建。重点分析。second.binlog -
将第二个binlog重放为诊断文本日志:shell
dotnet msbuild second.binlog -noconlog -fl -flp:v=diag;logfile=second-full.log;performancesummary然后搜索实际执行的目标:bashgrep 'Building target\|Target.*was not skipped' second-full.log在理想的增量构建中,大多数目标都应被跳过。 -
检查未被跳过的目标,查看诊断日志中的执行信息,寻找表明目标运行原因的「过期」相关消息。
-
在binlog中查找关键消息:
- — 表示MSBuild未找到输出文件或所有输出文件缺失,该目标会完整执行。
"Building target 'X' completely" - — 表示部分(非全部)输出文件已过期。
"Building target 'X' incrementally" - — 目标已被正确跳过。
"Skipping target 'X' because all output files are up-to-date"
-
搜索「is newer than output」消息,找到触发重建的具体输入文件:bash
grep "is newer than output" second-full.log这会明确显示是哪个输入文件的时间戳导致MSBuild判定目标过期。
Additional diagnostic techniques
其他诊断技巧
- Compare and
first.binlogside by side in the MSBuild Structured Log Viewer to see what changed.second.binlog - Use to see which targets consumed the most time in the second build — these are your optimization targets.
grep 'Target Performance Summary' -A 30 second-full.log - Check for targets with zero-duration that still ran — they may have unnecessary dependencies causing them to execute.
- 在MSBuild结构化日志查看器中并排比较和
first.binlog,查看两次构建的差异。second.binlog - 使用查看第二次构建中耗时最长的目标——这些是需要优化的重点。
grep 'Target Performance Summary' -A 30 second-full.log - 检查耗时为零但仍运行的目标——它们可能存在不必要的依赖关系导致执行。
FileWrites and Clean Build
FileWrites与清理构建
The item group is MSBuild's mechanism for tracking files generated during the build. It powers and helps maintain correct incremental behavior.
FileWritesdotnet clean- item: Register any file your custom targets create so that
FileWritesknows to remove them. Without this, generated files accumulate across builds and may confuse incremental checks.dotnet clean - item: Use this for files that are shared across multiple projects (e.g., shared generated code). These files are tracked but not deleted if other projects still reference them.
FileWritesShareable - If not registered: Files accumulate in the output and intermediate directories. won't remove them, and they may cause stale data issues or confuse up-to-date checks.
dotnet clean
FileWritesdotnet clean- 项:注册自定义目标创建的所有文件,确保
FileWrites知道要删除这些文件。如果不注册,生成的文件会在多次构建中累积,可能干扰增量检查。dotnet clean - 项:用于跨多个项目共享的文件(如共享生成代码)。这些文件会被追踪,但如果其他项目仍引用它们,则不会被删除。
FileWritesShareable - 未注册的影响:文件会在输出目录和中间目录中累积。无法删除它们,可能导致过期数据问题或干扰最新状态检查。
dotnet clean
Pattern for registering generated files
注册生成文件的模式
Add generated files to inside the target that creates them:
FileWritesxml
<Target Name="MyGenerator" 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>在创建文件的目标内部,将生成的文件添加到:
FileWritesxml
<Target Name="MyGenerator" 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>Visual Studio Fast Up-to-Date Check
Visual Studio快速最新状态检查
Visual Studio has its own up-to-date check (Fast Up-to-Date Check, or FUTDC) that is separate from MSBuild's / mechanism. Understanding the difference is critical for diagnosing "it rebuilds in VS but not on the command line" issues.
InputsOutputs- VS FUTDC is faster because it runs in-process and checks a known set of items without invoking MSBuild at all. It compares timestamps of well-known item types (Compile, Content, EmbeddedResource, etc.) against the project's primary output.
- It can be wrong if your project uses custom build actions, custom targets that generate files, or non-standard item types that FUTDC doesn't know about.
- Disable FUTDC to force Visual Studio to use MSBuild's full incremental check:
xml
<PropertyGroup> <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck> </PropertyGroup> - Diagnose FUTDC decisions by viewing the Output window in VS: go to Tools → Options → Projects and Solutions → SDK-Style Projects and set Up-to-date Checks logging level to Verbose or above. FUTDC will log exactly which file it considers out of date.
- Common VS FUTDC issues:
- Custom build actions not registered with the FUTDC system
- items that are newer than the last build
CopyToOutputDirectory - Items added dynamically by targets that FUTDC doesn't evaluate
- or
Contentitems withNonethat have been modifiedCopyToOutputDirectory="PreserveNewest"
Visual Studio拥有独立于MSBuild /机制的最新状态检查(Fast Up-to-Date Check,简称FUTDC)。理解两者的差异对于诊断「VS中触发重建但命令行中不触发」的问题至关重要。
InputsOutputs- VS FUTDC速度更快:它在进程内运行,无需调用MSBuild即可检查一组已知项,将知名项类型(Compile、Content、EmbeddedResource等)的时间戳与项目主输出进行比较。
- 可能出现误判:如果项目使用自定义构建操作、生成文件的自定义目标,或FUTDC不识别的非标准项类型,就可能出现误判。
- 禁用FUTDC:强制Visual Studio使用MSBuild的完整增量检查:
xml
<PropertyGroup> <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck> </PropertyGroup> - 诊断FUTDC决策:在VS的输出窗口中查看相关日志:依次进入工具 → 选项 → 项目和解决方案 → SDK风格项目,将最新状态检查的日志级别设置为详细或更高。FUTDC会记录它判定为过期的具体文件。
- VS FUTDC常见问题:
- 自定义构建操作未注册到FUTDC系统
- 项的时间戳晚于上次构建
CopyToOutputDirectory - 目标动态添加的项未被FUTDC评估
- 已修改的、带有属性的
CopyToOutputDirectory="PreserveNewest"或Content项None
Making Custom Targets Incremental
实现自定义增量目标
The following is a complete example of a well-structured incremental custom target:
xml
<Target Name="GenerateConfig"
Inputs="$(MSBuildProjectFile);@(ConfigInput)"
Outputs="$(IntermediateOutputPath)config.generated.cs"
BeforeTargets="CoreCompile">
<!-- 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>Key points in this example:
- includes
Inputs: This ensures the target reruns if the project file itself changes (e.g., a property that affects generation is modified).$(MSBuildProjectFile) - includes
Inputs: The actual source files that drive generation.@(ConfigInput) - uses
Outputs: Generated files go in the$(IntermediateOutputPath)directory, which is managed by MSBuild and cleaned automatically.obj/ - : The generated file is available before the compiler runs.
BeforeTargets="CoreCompile" - registration: Ensures
FileWritesremoves the generated file.dotnet clean - inclusion: Adds the generated file to the compilation without requiring it to exist at evaluation time.
Compile
以下是一个结构规范的自定义增量目标完整示例:
xml
<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)目录,该目录由MSBuild管理,会被自动清理。obj/ - :生成的文件在编译器运行前已准备就绪。
BeforeTargets="CoreCompile" - 注册到:确保
FileWrites会删除生成的文件。dotnet clean - 添加到:无需在评估阶段确保文件存在,即可将生成的文件加入编译流程。
Compile
Common mistakes to avoid
需避免的常见错误
xml
<!-- BAD: No Inputs/Outputs — runs every build -->
<Target Name="BadTarget" BeforeTargets="CoreCompile">
<Exec Command="generate-code.exe" />
</Target>
<!-- BAD: Volatile output path — never finds previous output -->
<Target Name="BadTarget2"
Inputs="@(Compile)"
Outputs="$(OutputPath)gen_$([System.DateTime]::Now.Ticks).cs">
<Exec Command="generate-code.exe" />
</Target>
<!-- GOOD: Stable paths, registered outputs -->
<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>xml
<!-- 错误:无Inputs/Outputs — 每次构建都运行 -->
<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>Performance Summary and Preprocess
性能汇总与预编译
MSBuild provides built-in tools to understand what's running and why.
-
— Appends a summary at the end of the build showing time spent in each target and task. Use this to quickly identify the most expensive operations:
/clp:PerformanceSummaryshelldotnet build /clp:PerformanceSummaryThis shows a table of targets sorted by cumulative time, making it easy to spot targets that shouldn't be running in an incremental build. -
— Generates a single XML file with all imports inlined, showing the fully evaluated project. This is invaluable for understanding what targets, properties, and items are defined and where they come from:
/pp:preprocess.xmlshelldotnet msbuild /pp:preprocess.xmlSearch the preprocessed output to find whereandInputsare defined for any target, or to understand the full chain of imports.Outputs -
Use both together to understand what's running () and what's imported (
PerformanceSummary), then cross-reference with binlog analysis for a complete picture./pp
MSBuild提供内置工具帮助用户了解构建的执行内容和原因。
-
— 在构建结束时追加一个汇总信息,显示每个目标和任务的耗时。使用该参数可快速识别最耗时的操作:
/clp:PerformanceSummaryshelldotnet build /clp:PerformanceSummary该参数会生成按累计耗时排序的目标表格,便于发现增量构建中不应运行的目标。 -
— 生成一个内联所有导入内容的单XML文件,展示完全评估后的项目。该文件对于理解目标、属性和项的定义及来源非常重要:
/pp:preprocess.xmlshelldotnet msbuild /pp:preprocess.xml在预编译输出中搜索,可找到任意目标的和Inputs定义位置,或了解完整的导入链。Outputs -
结合使用这两个工具:通过了解执行内容,通过
PerformanceSummary了解导入内容,再结合binlog分析,可获得完整的构建全景。/pp
Common Fixes
常见修复方案
- Always add and
Inputsto custom targets — This is the single most impactful change for incremental build performance. Without both attributes, the target runs every time.Outputs - Use for generated files — Files in
$(IntermediateOutputPath)are tracked by MSBuild's clean infrastructure and won't leak between configurations.obj/ - Register generated files in — Ensures
FileWritesremoves them and prevents stale file accumulation.dotnet clean - Avoid volatile data in build — Don't embed timestamps, random values, or build counters in file paths or generated content unless you have a deliberate strategy for managing staleness. If you must use volatile data, isolate it to a single file with minimal downstream impact.
- Use instead of
Returnswhen you need to pass items without creating incremental build dependency —Outputsserves double duty: it defines the incremental check AND the items returned from the target. If you only need to pass items to calling targets without affecting incrementality, useOutputsinstead:Returnsxml<!-- Outputs: affects incremental check AND return value --> <Target Name="GetFiles" Outputs="@(DiscoveredFiles)">...</Target> <!-- Returns: only affects return value, no incremental check --> <Target Name="GetFiles" Returns="@(DiscoveredFiles)">...</Target>
- 始终为自定义目标添加和
Inputs— 这是提升增量构建性能最有效的措施。若未同时配置这两个属性,目标会每次都运行。Outputs - 使用存放生成文件 —
$(IntermediateOutputPath)目录下的文件由MSBuild的清理基础设施追踪,不会在不同配置间泄漏。obj/ - 将生成文件注册到— 确保
FileWrites会删除这些文件,防止过期文件累积。dotnet clean - 避免在构建中使用易变数据 — 除非有明确的过期管理策略,否则不要在文件路径或生成内容中嵌入时间戳、随机值或构建计数器。如果必须使用易变数据,应将其隔离到单个文件,减少对下游的影响。
- 当仅需传递项而不创建增量构建依赖时,使用替代
Returns—Outputs有双重作用:定义增量检查规则,同时定义目标返回的项。如果仅需将项传递给调用目标而不影响增量性,应使用Outputs:Returnsxml<!-- Outputs:影响增量检查和返回值 --> <Target Name="GetFiles" Outputs="@(DiscoveredFiles)">...</Target> <!-- Returns:仅影响返回值,不触发增量检查 --> <Target Name="GetFiles" Returns="@(DiscoveredFiles)">...</Target>