重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
npx skills add https://github.com/dotnet/skills --skill incremental-buildMSBuild 的增量构建机制允许在输出文件已是最新状态时跳过目标,从而显著减少后续运行的构建时间。
具有 Inputs 和 Outputs 属性的目标:MSBuild 会比较 Inputs 中列出的所有文件与 Outputs 中列出的所有文件的时间戳。如果每个输出文件都比每个输入文件新,则完全跳过该目标。
没有 Inputs/Outputs:每次调用构建时,该目标都会运行。这是默认行为,也是导致增量构建缓慢的最常见原因。
目标上的 Incremental 属性:目标可以显式选择启用或禁用增量行为。设置 Incremental="false" 会强制目标始终运行,即使指定了 和 。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
InputsOutputs基于时间戳的比较:MSBuild 使用文件系统时间戳(最后写入时间)来确定文件是否过时。它不使用内容哈希。这意味着修改文件(更新其时间戳但不更改内容)将触发重新构建。
<!-- 此目标是增量式的:如果 Output 比所有 Input 都新,则跳过 --><Target Name="Transform" Inputs="@(TransformFiles)" Outputs="@(TransformFiles->'$(OutputPath)%(Filename).out')">
<!-- 在此处工作 --> </Target> <!-- 此目标始终运行,因为它没有 Inputs/Outputs --> <Target Name="PrintMessage"> <Message Text="This runs every build" /> </Target>Outputs 中列出,MSBuild 将不知道这些文件。目标可能会被跳过(因为其声明的输出是最新的),但下游目标可能仍会被触发。FileWrites 项组中注册的文件不会被 dotnet clean 清理。随着时间的推移,过时的文件可能会干扰增量检查。@(Compile))会发生变化。由于这些项会作为 Inputs 的输入,输入集合的变化会触发重新构建。这是预期行为,但可能会令人意外。Inputs 或 Outputs 路径输入的属性(例如 $(Configuration)、$(TargetFramework))在更改时将导致重新构建。在 Debug 和 Release 之间切换按设计会进行完全重新构建。project.assets.json 以及许多已解析的程序集路径。这会改变 ResolveAssemblyReferences 和 CoreCompile 的输入,从而触发重新构建。VBCSCompiler)会缓存编译状态。如果服务器被回收(超时、崩溃或手动终止),即使 MSBuild 的增量检查通过,下一次构建也可能会变慢,因为编译器必须重新填充其内存缓存。使用二进制日志(binlogs)来准确理解目标为何运行而不是被跳过。
使用 binlog 构建两次 以捕获增量构建行为:
dotnet build /bl:first.binlog
dotnet build /bl:second.binlog
第一次构建建立基线。第二次构建是您希望是增量的那次。分析 second.binlog。
将第二个 binlog 重放 到诊断文本日志:
dotnet msbuild second.binlog -noconlog -fl -flp:v=diag;logfile=second-full.log;performancesummary
然后搜索实际执行的目标:
grep 'Building target\|Target.*was not skipped' second-full.log
在完美的增量构建中,大多数目标应该被跳过。
检查未跳过的目标,在诊断日志中查找它们的执行消息。检查指示目标为何运行的“out of date”消息。
在 binlog 中查找关键消息:
"Building target 'X' completely" — 表示 MSBuild 未找到任何输出或所有输出都缺失;这是完整的目标执行。"Building target 'X' incrementally" — 表示一些(但非全部)输出已过时。"Skipping target 'X' because all output files are up-to-date" — 目标被正确跳过。搜索 "is newer than output" 消息 以找到触发重新构建的特定输入文件:
grep "is newer than output" second-full.log
这将准确揭示是哪个输入文件的时间戳导致 MSBuild 认为目标已过时。
first.binlog 和 second.binlog,以查看发生了什么变化。grep 'Target Performance Summary' -A 30 second-full.log 查看第二次构建中哪些目标消耗了最多时间 — 这些就是您需要优化的目标。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 拥有其独立于 MSBuild Inputs/Outputs 机制的最新状态检查(快速最新状态检查,简称 FUTDC)。理解其差异对于诊断“在 VS 中会重新构建但在命令行中不会”的问题至关重要。
VS FUTDC 更快,因为它进程内运行,并且在不调用 MSBuild 的情况下检查一组已知的项。它比较已知项类型(Compile、Content、EmbeddedResource 等)的时间戳与项目的主要输出。
如果您的项目使用自定义生成操作、生成文件的自定义目标或 FUTDC 不知道的非标准项类型,它可能会出错。
禁用 FUTDC 以强制 Visual Studio 使用 MSBuild 的完整增量检查:
<PropertyGroup>
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
</PropertyGroup>
诊断 FUTDC 决策:通过查看 VS 中的输出窗口:转到 工具 → 选项 → 项目和解决方案 → SDK 风格项目,并将 最新状态检查 日志级别设置为 详细 或更高。FUTDC 将准确记录它认为哪个文件已过时。
常见的 VS FUTDC 问题:
CopyToOutputDirectory 项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 包含:将生成的文件添加到编译中,而无需在评估时要求其存在。<!-- 错误:没有 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>
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 会删除它们,并防止过时文件累积。
避免在构建中使用易变数据 — 除非您有管理过时状态的明确策略,否则不要在文件路径或生成内容中嵌入时间戳、随机值或构建计数器。如果必须使用易变数据,请将其隔离到单个文件中,并尽量减少对下游的影响。
当您需要传递项而不创建增量构建依赖时,使用 Returns 代替 Outputs — Outputs 有双重作用:它定义了增量检查 AND 从目标返回的项。如果您只需要将项传递给调用目标而不影响增量性,请改用 Returns:
<!-- Outputs: 影响增量检查 AND 返回值 -->
<Target Name="GetFiles" Outputs="@(DiscoveredFiles)">...</Target>
<!-- Returns: 仅影响返回值,不影响增量检查 -->
<Target Name="GetFiles" Returns="@(DiscoveredFiles)">...</Target>
每周安装次数
44
代码仓库
GitHub 星标数
703
首次出现
2026年3月10日
安全审计
安装于
kimi-cli42
gemini-cli42
amp42
cline42
github-copilot42
codex42
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 withInputs and Outputs attributes: MSBuild compares the timestamps of all files listed in Inputs against all files listed in Outputs. If every output file is newer than every input file, the target is skipped entirely.
WithoutInputs/Outputs: The target runs every time the build is invoked. This is the default behavior and the most common cause of slow incremental builds.
Incremental attribute on targets: Targets can explicitly opt in or out of incremental behavior. Setting Incremental="false" forces the target to always run, even if Inputs and Outputs are specified.
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.
<!-- 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>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 Outputs, 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.
Missing FileWrites registration — Files created during the build but not registered in the FileWrites item group won't be cleaned by dotnet clean. Over time, stale files can confuse incremental checks.
Glob changes — When you add or remove source files, the item set (e.g., @(Compile)) changes. Since these items feed into Inputs, the set of inputs changes and triggers a rebuild. This is expected behavior but can be surprising.
Use binary logs (binlogs) to understand exactly why targets ran instead of being skipped.
Build twice with binlogs to capture the incremental build behavior:
dotnet build /bl:first.binlog
dotnet build /bl:second.binlog
The 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:
dotnet msbuild second.binlog -noconlog -fl -flp:v=diag;logfile=second-full.log;performancesummary
Then search for targets that actually executed:
grep 'Building target\|Target.*was not skipped' second-full.log
In 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:
"Building target 'X' completely" — means MSBuild found no outputs or all outputs are missing; this is a full target execution."Building target 'X' incrementally" — means some (but not all) outputs are out of date."Skipping target 'X' because all output files are up-to-date" — target was correctly skipped.Search for "is newer than output" messages to find the specific input file that triggered the rebuild:
grep "is newer than output" second-full.log
This reveals exactly which input file's timestamp caused MSBuild to consider the target out of date.
first.binlog and second.binlog side by side in the MSBuild Structured Log Viewer to see what changed.grep 'Target Performance Summary' -A 30 second-full.log to see which targets consumed the most time in the second build — these are your optimization targets.The FileWrites item group is MSBuild's mechanism for tracking files generated during the build. It powers dotnet clean and helps maintain correct incremental behavior.
FileWrites item: Register any file your custom targets create so that dotnet clean knows to remove them. Without this, generated files accumulate across builds and may confuse incremental checks.FileWritesShareable 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.dotnet clean won't remove them, and they may cause stale data issues or confuse up-to-date checks.Add generated files to FileWrites inside the target that creates them:
<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 has its own up-to-date check (Fast Up-to-Date Check, or FUTDC) that is separate from MSBuild's Inputs/Outputs mechanism. Understanding the difference is critical for diagnosing "it rebuilds in VS but not on the command line" issues.
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:
<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 :
CopyToOutputDirectory items that are newer than the last buildContent or items with that have been modifiedThe following is a complete example of a well-structured incremental custom target:
<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:
Inputs includes $(MSBuildProjectFile): This ensures the target reruns if the project file itself changes (e.g., a property that affects generation is modified).Inputs includes @(ConfigInput): The actual source files that drive generation.Outputs uses $(IntermediateOutputPath): Generated files go in the obj/ directory, which is managed by MSBuild and cleaned automatically.BeforeTargets="CoreCompile" : The generated file is available before the compiler runs.FileWrites registration: Ensures removes the generated file.<!-- 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>
MSBuild provides built-in tools to understand what's running and why.
/clp:PerformanceSummary — 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:
dotnet build /clp:PerformanceSummary
This shows a table of targets sorted by cumulative time, making it easy to spot targets that shouldn't be running in an incremental build.
/pp:preprocess.xml — 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:
dotnet msbuild /pp:preprocess.xml
Search the preprocessed output to find where Inputs and Outputs are defined for any target, or to understand the full chain of imports.
PerformanceSummary) and what's imported (/pp), then cross-reference with binlog analysis for a complete picture.Always addInputs and Outputs to custom targets — This is the single most impactful change for incremental build performance. Without both attributes, the target runs every time.
Use$(IntermediateOutputPath) for generated files — Files in obj/ are tracked by MSBuild's clean infrastructure and won't leak between configurations.
Register generated files inFileWrites — Ensures dotnet clean removes them and prevents stale file accumulation.
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.
UseReturns instead of when you need to pass items without creating incremental build dependency — serves 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, use instead:
Weekly Installs
44
Repository
GitHub Stars
703
First Seen
Mar 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
kimi-cli42
gemini-cli42
amp42
cline42
github-copilot42
codex42
TanStack Query v5 完全指南:React 数据管理、乐观更新、离线支持
2,500 周安装
Vitest 测试框架最佳实践指南:断言、模拟与异步测试完整教程
291 周安装
Seedance 2.0 视频提示词生成器 | AI视频生成 | 字节跳动即梦平台多模态创作工具
291 周安装
Segment CDP 客户数据平台使用指南:Analytics.js与Node.js追踪最佳实践
292 周安装
Magic UI与React Bits动画组件库:React动画组件开发与性能优化指南
296 周安装
计算机使用智能体开发指南:感知-推理-行动循环与沙盒环境部署
293 周安装
Docker容器化最佳实践指南:生产就绪容器构建、安全优化与CI/CD部署
291 周安装
Property changes — Properties that feed into Inputs or Outputs paths (e.g., $(Configuration), $(TargetFramework)) will cause rebuilds when changed. Switching between Debug and Release is a full rebuild by design.
NuGet package updates — Changing a package version updates project.assets.json and potentially many resolved assembly paths. This changes the inputs to ResolveAssemblyReferences and CoreCompile, triggering a rebuild.
Build server VBCSCompiler cache invalidation — The Roslyn compiler server (VBCSCompiler) 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.
NoneCopyToOutputDirectory="PreserveNewest"dotnet cleanCompile inclusion: Adds the generated file to the compilation without requiring it to exist at evaluation time.OutputsOutputsReturns<!-- 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>