MSBuild has mature incremental execution from explicit target inputs and outputs, but almost no automatic discovery of those inputs and outputs. It is best described as shallow, declarative inference, not dependency inference comparable to Ninja or Bazel.
| Capability | Current support |
|---|---|
| Infer a target's inputs/outputs by inspecting its tasks | No |
Skip a target using declared Inputs/Outputs |
Yes, using file timestamps |
| Partially execute a target for changed items | Yes, when a one-to-one item transform can be recognized |
| Reconstruct items/properties when a target is skipped | Yes, when derivable from project XML |
| Infer arbitrary custom-task results | No |
| Persist inferred target state between builds | No |
| Detect changes to the input list since the previous build | No |
| Content/hash-based dependency checking | No |
Given:
<Target Name="Compile"
Inputs="@(Source)"
Outputs="@(Source->'obj\%(Filename).obj')">
...
</Target>MSBuild expands the explicitly declared expressions and compares timestamps:
- All outputs current: skip and perform output inference.
- Some mapped outputs stale: execute against only those input items.
- All mapped outputs stale: full execution.
- No recognizable mapping: compare every input against the oldest output and either fully execute or fully skip.
Partial builds require a structural correlation through the same item vector. Input transforms, unrelated output item lists, literal outputs, and mismatched list lengths generally degrade to all-or-nothing checking.
When a target is skipped, MSBuild walks its body without executing ordinary tasks:
<ItemGroup>and<PropertyGroup>operations are replayed.- Conditions and expressions are evaluated.
- Task
<Output>elements can be inferred only when theirTaskParameternames a parameter explicitly supplied to that same task invocation.
For example, this is inferable:
<MyTask DestinationFiles="@(Source->'obj\%(Filename).obj')">
<Output TaskParameter="DestinationFiles"
ItemName="ProducedFiles" />
</MyTask>DestinationFiles is already known from the XML. But this is not inferable if GeneratedFiles is computed inside the task:
<MyTask Sources="@(Source)">
<Output TaskParameter="GeneratedFiles"
ItemName="ProducedFiles" />
</MyTask>MSBuild does not instantiate or inspect the task during inference, so it cannot know GeneratedFiles.
For a partial build, the engine effectively processes the target twice:
- Infer state for the up-to-date input subset.
- Execute tasks for the stale input subset.
- Merge both scopes, with actual execution results taking precedence.
InputsandOutputsare fundamentally file timestamp declarations, not a semantic dependency graph.Returnscontrols what a target returns to callers; it does not participate in up-to-date analysis. WithoutReturns,Outputsalso acts as the returned item expression.- Inferred items and properties are reconstructed in memory on every build; they are not cached on disk.
- MSBuild only considers the currently evaluated input list. Adding or removing an item since the previous build is not inherently detected unless that affects a declared timestamp input, such as the project file or a manifest.
- Task-level incrementality--
Copy,Csc,ResolveAssemblyReference, custom task caches,IIncrementalTask, and so on--is separate and opaque to target-level inference. - If
Inputsevaluates empty, or declaredOutputsevaluates empty, the skip behavior can be surprising; authors must ensure those expressions form a reliable contract.
The practical assessment is: strong support for explicitly modeled file transforms, limited syntactic reconstruction of skipped-target state, and essentially zero automatic inference of the true dependency graph.