[browser] WebAssembly SDK targets more incremental#125367
[browser] WebAssembly SDK targets more incremental#125367
Conversation
Split _GenerateBuildWasmBootJson and GeneratePublishWasmBootJson into smaller targets to enable Inputs/Outputs-based incrementalism for the expensive GenerateWasmBootJson task invocations. Build path split: - _ResolveBuildWasmBootJsonEndpoints: always runs, resolves endpoints - _WriteBuildWasmBootJsonFile: incremental (Inputs/Outputs), writes boot JSON - _GenerateBuildWasmBootJson: always runs, defines boot config as static web asset Publish path split: - _ResolvePublishWasmBootJsonInputs: always runs, resolves publish inputs - GeneratePublishWasmBootJson: incremental (Inputs/Outputs), writes boot JSON When no inputs have changed, the GenerateWasmBootJson task (~100ms) is skipped entirely. The always-running targets handle endpoint resolution and asset definition to ensure downstream item collections remain populated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Split the monolithic _ResolveWasmOutputs target into three targets to enable MSBuild Inputs/Outputs-based skip optimization for DLL-to-webcil conversion: - _ComputeWasmBuildCandidates (always runs): resolves build asset candidates via ComputeWasmBuildAssets, separates DLL candidates into culture/non-culture groups, and computes expected webcil output paths. - _ConvertBuildDllsToWebcil (incremental): runs ConvertDllsToWebcil only when input DLLs are newer than their webcil outputs. The task also retains its internal per-file timestamp checks as a secondary optimization. - _ResolveWasmOutputs (always runs): reconstructs webcil candidate items from build asset candidates using MSBuild item transforms (matching the ConvertDllsToWebcil task's path/metadata logic), then calls DefineStaticWebAssets to produce the final asset definitions. Culture and non-culture DLLs are separated into distinct intermediate items (_WasmWebcilConvertedNonCulture / _WasmWebcilConvertedCulture) to avoid MSBuild batching errors on metadata like RelatedAsset that only culture items define. The _ResolveWasmOutputs target lists _ComputeWasmBuildCandidates explicitly in its DependsOnTargets (before _ConvertBuildDllsToWebcil) to ensure _WasmEnableWebcil is set by _ResolveWasmConfiguration before the conversion target's Condition is evaluated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR improves MSBuild incrementalism for WebAssembly SDK build targets by splitting monolithic targets into smaller, more focused targets with proper Inputs/Outputs declarations. This allows MSBuild to skip expensive operations (webcil DLL conversion, boot JSON generation) on no-op rebuilds when inputs haven't changed.
Changes:
- The
_ResolveWasmOutputstarget is split into_ComputeWasmBuildCandidates(resolve/classify candidates),_ConvertBuildDllsToWebcil(incremental webcil conversion), and_ResolveWasmOutputs(reconstruct webcil metadata and define static web assets). - The
_GenerateBuildWasmBootJsontarget is split into_ResolveBuildWasmBootJsonEndpoints(resolve endpoints),_WriteBuildWasmBootJsonFile(incremental boot JSON generation), and_GenerateBuildWasmBootJson(define static web asset for boot config). - The
GeneratePublishWasmBootJsontarget is split into_ResolvePublishWasmBootJsonInputs(resolve inputs) andGeneratePublishWasmBootJson(incremental publish boot JSON generation).
...nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets
Outdated
Show resolved
Hide resolved
Both ConvertDllsToWebcil (Utils.MoveIfDifferent) and GenerateWasmBootJson (ArtifactWriter.PersistFileIfChanged) use content-comparison write patterns that preserve old file timestamps when content is unchanged. This causes MSBuild's Inputs/Outputs check to always see old outputs < new inputs, permanently defeating incrementalism. Add <Touch> after each task invocation so output timestamps reflect the current build session, allowing MSBuild to correctly skip targets on subsequent no-op builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.
You can also share your feedback on Copilot code review. Take the survey.
| <Target Name="_ConvertBuildDllsToWebcil" | ||
| DependsOnTargets="_ComputeWasmBuildCandidates" | ||
| Condition="'$(_WasmEnableWebcil)' == 'true'" | ||
| Inputs="@(_WasmDllBuildCandidates);$(MSBuildProjectFullPath)" |
There was a problem hiding this comment.
The incremental _ConvertBuildDllsToWebcil target’s Inputs only includes the candidate DLLs and $(MSBuildProjectFullPath). If the SDK targets file or the tasks assembly changes (e.g., workload/SDK update), this target can be incorrectly skipped, leaving stale webcil outputs. Consider adding $(MSBuildThisFileFullPath) and/or $(_WebAssemblySdkTasksAssembly) to Inputs so outputs are regenerated when the generating logic changes.
| Inputs="@(_WasmDllBuildCandidates);$(MSBuildProjectFullPath)" | |
| Inputs="@(_WasmDllBuildCandidates);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)" |
| on no-op rebuilds. --> | ||
| <Target Name="_WriteBuildWasmBootJsonFile" | ||
| DependsOnTargets="_ResolveBuildWasmBootJsonEndpoints" | ||
| Inputs="@(IntermediateAssembly);@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset);@(_WasmJsConfigStaticWebAsset);@(_WasmDotnetJsForBuild);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath)" |
There was a problem hiding this comment.
_WriteBuildWasmBootJsonFile is incremental but its Inputs do not include the SDK targets file or the tasks assembly that implements GenerateWasmBootJson. After upgrading the workload/SDK, MSBuild could skip this target and keep an older boot JSON even though the generation logic changed. Consider adding $(MSBuildThisFileFullPath) and/or $(_WebAssemblySdkTasksAssembly) to Inputs.
| Inputs="@(IntermediateAssembly);@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset);@(_WasmJsConfigStaticWebAsset);@(_WasmDotnetJsForBuild);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath)" | |
| Inputs="@(IntermediateAssembly);@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset);@(_WasmJsConfigStaticWebAsset);@(_WasmDotnetJsForBuild);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)" |
| inputs are older than the output. --> | ||
| <Target Name="GeneratePublishWasmBootJson" | ||
| DependsOnTargets="_ResolvePublishWasmBootJsonInputs" | ||
| Inputs="@(IntermediateAssembly);@(_WasmPublishAsset);@(_WasmJsModuleCandidatesForPublish);@(_WasmPublishConfigFile);@(_WasmDotnetJsForPublish);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath)" |
There was a problem hiding this comment.
GeneratePublishWasmBootJson is now incremental, but its Inputs don’t include the SDK targets file or the tasks assembly. That means a workload/SDK update (changing Microsoft.NET.Sdk.WebAssembly.Browser.targets or Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.dll) can leave a stale publish boot JSON because MSBuild may consider the output up-to-date. Consider adding $(MSBuildThisFileFullPath) and/or $(_WebAssemblySdkTasksAssembly) to the Inputs list.
| Inputs="@(IntermediateAssembly);@(_WasmPublishAsset);@(_WasmJsModuleCandidatesForPublish);@(_WasmPublishConfigFile);@(_WasmDotnetJsForPublish);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath)" | |
| Inputs="@(IntermediateAssembly);@(_WasmPublishAsset);@(_WasmJsModuleCandidatesForPublish);@(_WasmPublishConfigFile);@(_WasmDotnetJsForPublish);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)" |
| </ConvertDllsToWebcil> | ||
|
|
||
| <!-- The ConvertDllsToWebcil task uses content comparison and preserves old timestamps when | ||
| the output content is unchanged. Touch the outputs so MSBuild's Inputs/Outputs check | ||
| sees current timestamps and can correctly skip this target on subsequent builds. --> | ||
| <Touch Files="@(_WasmExpectedWebcilOutputs)" /> |
There was a problem hiding this comment.
_ConvertBuildDllsToWebcil touches all expected webcil outputs after running conversion. Since ConvertDllsToWebcil already does per-file work (and may intentionally preserve timestamps for unchanged outputs), touching everything can force downstream copy/fingerprinting work for every webcil file even when only a subset actually changed. Consider using a single stamp file as the target Outputs, or only touching the files that were actually rewritten (e.g., capture the task’s file-writes into a private item and touch just that set).
| </ConvertDllsToWebcil> | |
| <!-- The ConvertDllsToWebcil task uses content comparison and preserves old timestamps when | |
| the output content is unchanged. Touch the outputs so MSBuild's Inputs/Outputs check | |
| sees current timestamps and can correctly skip this target on subsequent builds. --> | |
| <Touch Files="@(_WasmExpectedWebcilOutputs)" /> | |
| <Output TaskParameter="FileWrites" ItemName="_WasmConvertedWebcilOutputs" /> | |
| </ConvertDllsToWebcil> | |
| <!-- The ConvertDllsToWebcil task uses content comparison and preserves old timestamps when | |
| the output content is unchanged. Touch the files that were actually written so | |
| MSBuild's Inputs/Outputs check sees current timestamps and can correctly skip this | |
| target on subsequent builds, without forcing downstream work for unchanged outputs. --> | |
| <Touch Files="@(_WasmConvertedWebcilOutputs)" Condition="'@(_WasmConvertedWebcilOutputs)' != ''" /> |
When the incremental _WriteBuildWasmBootJsonFile or GeneratePublishWasmBootJson targets are skipped, the FileWrites items inside them are not populated. Move FileWrites tracking to the always-run wrapper targets (_GenerateBuildWasmBootJson and _AddPublishWasmBootJsonToStaticWebAssets) so boot JSON files are tracked for clean operations regardless of whether the file-writing target ran or was skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add $(MSBuildThisFileFullPath) and $(_WebAssemblySdkTasksAssembly) to Inputs of all three incremental targets so SDK/workload updates invalidate outputs - Use task's FileWrites output for selective Touch in _ConvertBuildDllsToWebcil instead of touching all expected outputs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.
You can also share your feedback on Copilot code review. Take the survey.
| the output content is unchanged. Touch the files that were actually written so | ||
| MSBuild's Inputs/Outputs check sees current timestamps and can correctly skip this | ||
| target on subsequent builds. --> |
There was a problem hiding this comment.
_WasmConvertedWebcilOutputs comes from ConvertDllsToWebcil's FileWrites output, but that task appends every finalWebcil path to FileWrites even when it logs "Skipping ... as it is older than the output" (i.e., no file write happened). Touching this list will therefore update timestamps for all webcil outputs whenever the target runs, which can force downstream PreserveNewest/SkipUnchangedFiles copies (and other timestamp-based incremental steps) to treat all webcil files as changed. Consider either (1) changing the task to output a separate item list for files that need timestamp refresh (e.g., only those where Utils.IsNewerThan was true and MoveIfDifferent returned false), and touch that list, or (2) adjusting the comment and accepting the broader timestamp churn intentionally.
| the output content is unchanged. Touch the files that were actually written so | |
| MSBuild's Inputs/Outputs check sees current timestamps and can correctly skip this | |
| target on subsequent builds. --> | |
| the output content is unchanged. Its FileWrites output currently includes all expected | |
| webcil outputs, so we intentionally touch all of them to ensure MSBuild's Inputs/Outputs | |
| check sees current timestamps and can correctly skip this target on subsequent builds. --> |
Summary
Improves MSBuild incrementalism for WebAssembly browser build targets in
Microsoft.NET.Sdk.WebAssembly.Browser.targets. On no-op rebuilds where inputs haven't changed, the expensiveConvertDllsToWebcilandGenerateWasmBootJsontasks are now skipped via MSBuild'sInputs/Outputsmechanism.Changes
Boot JSON Generation (commit 1)
Split
_GenerateBuildWasmBootJsoninto 3 targets:_ResolveBuildWasmBootJsonEndpoints(always runs) -- resolves endpoints and fingerprinted assets_WriteBuildWasmBootJsonFile(incremental) -- writes boot JSON file only when inputs change_GenerateBuildWasmBootJson(always runs) -- defines static web assets from the boot JSON outputSplit
GeneratePublishWasmBootJsoninto 2 targets:_ResolvePublishWasmBootJsonInputs(always runs) -- resolves publish endpointsGeneratePublishWasmBootJson(incremental) -- writes boot JSON only when inputs changeWebcil Conversion (commit 2)
Split
_ResolveWasmOutputsinto 3 targets:_ComputeWasmBuildCandidates(always runs) -- resolves build asset candidates viaComputeWasmBuildAssets_ConvertBuildDllsToWebcil(incremental) -- runsConvertDllsToWebcilonly when DLL inputs are newer than webcil outputs_ResolveWasmOutputs(always runs) -- reconstructs webcil items via MSBuild item transforms and callsDefineStaticWebAssetsTouch outputs for content-comparison tasks (commit 4)
Both
ConvertDllsToWebcil(Utils.MoveIfDifferent) andGenerateWasmBootJson(ArtifactWriter.PersistFileIfChanged) use content-comparison write patterns that preserve old file timestamps when output content is unchanged. This defeats MSBuild's timestamp-based Inputs/Outputs incrementalism — on no-op rebuilds, output files retained timestamps from a previous build session while input files had timestamps from the current session, causing MSBuild to always see inputs as newer than outputs.Added
<Touch>after each task invocation to ensure output timestamps reflect the current build session:_ConvertBuildDllsToWebcil: Touch@(_WasmExpectedWebcilOutputs)_WriteBuildWasmBootJsonFile: Touch$(_WasmBuildBootJsonPath)GeneratePublishWasmBootJson: Touch$(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)Incrementalism Proof
Binlog analysis of Wasm.Browser.Sample — build 1 (clean) vs build 2 (no-op rebuild):
Build Path
Publish Path
Overall
Design Notes
Why split instead of just adding Inputs/Outputs?
MSBuild's
Inputs/Outputsmechanism skips the entire target body when outputs are up-to-date. Since_ResolveWasmOutputsand_GenerateBuildWasmBootJsonboth contained file-writing tasks AND item-defining tasks (DefineStaticWebAssets), making them incremental would break downstream targets that depend on the items they produce. The split separates file I/O (incremental) from item definitions (always-run).Why Touch after task execution?
Both
ConvertDllsToWebcilandGenerateWasmBootJsonimplement "write only if content changed" patterns internally. While this is good for avoiding unnecessary downstream rebuilds in the general case, it defeats MSBuild's Inputs/Outputs check because unchanged outputs retain timestamps from a previous build session. The<Touch>ensures output timestamps always reflect the current build, allowing MSBuild to correctly determine that outputs are up-to-date on subsequent no-op builds.MSBuild Condition evaluation order
_ConvertBuildDllsToWebcilhasCondition="'$(_WasmEnableWebcil)' == 'true'"but MSBuild evaluatesConditionbeforeDependsOnTargets. Since_WasmEnableWebcilis set by_ResolveWasmConfiguration(inside_ComputeWasmBuildCandidates), the parent_ResolveWasmOutputsexplicitly lists_ComputeWasmBuildCandidatesin itsDependsOnTargetsbefore_ConvertBuildDllsToWebcil.Culture/non-culture DLL separation
Non-culture DLLs and culture-specific DLLs have different metadata (
RelatedAssetonly exists on culture items). Using%(RelatedAsset)in item transforms causes MSB4096 batching errors when applied to items without that metadata. The fix uses separate intermediate item names (_WasmWebcilConvertedNonCulture,_WasmWebcilConvertedCulture) to avoid cross-contamination.Testing
_ConvertBuildDllsToWebcil,_WriteBuildWasmBootJsonFile, andGeneratePublishWasmBootJson