[browser] WebAssembly SDK targets more incremental#125367
[browser] WebAssembly SDK targets more incremental#125367
Conversation
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).
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.
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.
Split monolithic targets into incremental chains: Webcil conversion: - _ComputeWasmBuildCandidates (always runs): resolves candidates, classifies DLLs vs framework pass-throughs, computes expected webcil output paths - _ConvertBuildDllsToWebcil (incremental): DLL-to-webcil conversion with Inputs/Outputs, Touch to fix content-comparison timestamp preservation - _ResolveWasmOutputs (always runs): reconstructs webcil items, classifies framework candidates, defines static web assets Build boot JSON: - _ResolveBuildWasmBootJsonEndpoints (always runs): endpoint resolution - _WriteBuildWasmBootJsonFile (incremental): JSON file generation with Inputs/Outputs, Touch for timestamp fix - _GenerateBuildWasmBootJson (always runs): static web asset registration Publish boot JSON: - _ResolvePublishWasmBootJsonInputs (always runs): input resolution - GeneratePublishWasmBootJson (incremental): JSON file generation with Inputs/Outputs, Touch for timestamp fix FileWrites are added to always-run wrapper targets so dotnet clean works correctly even when incremental targets are skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
084b5ee to
ab57788
Compare
This comment has been minimized.
This comment has been minimized.
Two new tests verify WASM build incrementalism via binlog analysis: - IncrementalBuild_NoChanges_SkipsWebcilAndBootJson: Builds twice with no changes, asserts _ConvertBuildDllsToWebcil and _WriteBuildWasmBootJsonFile are skipped on the second build. - IncrementalBuild_SourceChange_RunsWebcilForAppOnly: Builds, modifies a C# source file, rebuilds, then asserts the webcil/boot JSON targets run but only the app assembly is re-converted (framework DLLs skipped by the task's internal per-file timestamp check). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
…amps - Add missing file inputs to _WriteBuildWasmBootJsonFile: VFS assets, config files, and dotnet.js template - Add property stamp files for both build and publish boot JSON targets using WriteOnlyWhenDifferent to detect property-only changes (e.g., WasmDebugLevel, environment name, globalization flags) - Build stamp: wasm-bootjson-build.stamp - Publish stamp: wasm-bootjson-publish.stamp Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| DependsOnTargets="_ResolvePublishWasmBootJsonInputs"> | ||
| <WriteLinesToFile | ||
| File="$(IntermediateOutputPath)wasm-bootjson-publish.stamp" | ||
| Lines="$(WasmDebugLevel)|$(_WasmPublishApplicationEnvironmentName)|$(_BlazorCacheBootResources)|$(InvariantGlobalization)|$(_LoadCustomIcuData)|$(_BlazorWebAssemblyLoadAllGlobalizationData)|$(_BlazorWebAssemblyJiterpreter)|$(_BlazorWebAssemblyRuntimeOptions)|$(RunAOTCompilation)|$(WasmEnableThreads)|$(_WasmFingerprintAssets)|$(_WasmBundlerFriendlyBootConfig)|$(WasmProfilers)|$(TargetFrameworkVersion)|$(DiagnosticPorts)" |
There was a problem hiding this comment.
Same issue as the build stamp: the publish boot JSON stamp omits several non-file inputs that affect GenerateWasmBootJson output (notably @(WasmEnvironmentVariable) + %(Value), @(BlazorWebAssemblyLazyLoad), module lists, and WasmTest* properties). If any of these change without touching the listed file inputs, GeneratePublishWasmBootJson can be incorrectly skipped and keep a stale boot JSON. Consider incorporating these values into the stamp so incrementalism remains correct.
| Lines="$(WasmDebugLevel)|$(_WasmPublishApplicationEnvironmentName)|$(_BlazorCacheBootResources)|$(InvariantGlobalization)|$(_LoadCustomIcuData)|$(_BlazorWebAssemblyLoadAllGlobalizationData)|$(_BlazorWebAssemblyJiterpreter)|$(_BlazorWebAssemblyRuntimeOptions)|$(RunAOTCompilation)|$(WasmEnableThreads)|$(_WasmFingerprintAssets)|$(_WasmBundlerFriendlyBootConfig)|$(WasmProfilers)|$(TargetFrameworkVersion)|$(DiagnosticPorts)" | |
| Lines="$(WasmDebugLevel)|$(_WasmPublishApplicationEnvironmentName)|$(_BlazorCacheBootResources)|$(InvariantGlobalization)|$(_LoadCustomIcuData)|$(_BlazorWebAssemblyLoadAllGlobalizationData)|$(_BlazorWebAssemblyJiterpreter)|$(_BlazorWebAssemblyRuntimeOptions)|$(RunAOTCompilation)|$(WasmEnableThreads)|$(_WasmFingerprintAssets)|$(_WasmBundlerFriendlyBootConfig)|$(WasmProfilers)|$(TargetFrameworkVersion)|$(DiagnosticPorts)|@(WasmEnvironmentVariable->'%(Identity)=%(Value)', ';')|@(BlazorWebAssemblyLazyLoad, ';')|@(_WasmJsModuleCandidatesForPublish, ';')|@(_WasmJsModuleCandidatesForPublishEndpoint, ';')" |
There was a problem hiding this comment.
Note
This reply was generated by GitHub Copilot.
Fixed in the same commit (58d8153) — the publish stamp now includes the same additional inputs: @(WasmEnvironmentVariable->'%(Identity)=%(Value)'), @(BlazorWebAssemblyLazyLoad), @(WasmModuleAfterConfigLoaded), @(WasmModuleAfterRuntimeReady), and all WasmTest* properties.
This comment has been minimized.
This comment has been minimized.
Add WebcilOutputPath metadata to DLL candidates so Outputs is a direct transform of Inputs. This enables MSBuild partial target execution: when only some DLLs change (e.g., app assembly after source edit), MSBuild passes only out-of-date pairs to the target body instead of all 174 DLLs. Touch is retained for property-triggered full rebuilds where MoveIfDifferent preserves old timestamps on unchanged webcil files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Resolve conflict in Microsoft.NET.Sdk.WebAssembly.Browser.targets: keep incremental build refactoring and add WebcilVersion parameter from main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
The _ConvertBuildDllsToWebcil target has Condition for webcil being enabled, so it doesn't appear in the binlog when running in the NoWebcil test configuration. Guard the webcil-specific assertions (target skipped/ran and converted file checks) behind UseWebcil so the tests pass in both Webcil and NoWebcil configurations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🤖 Copilot Code Review — PR #125367Note This review was generated by Copilot using multi-model analysis (Claude Opus 4.6 primary + Goldeneye sub-agent; GPT-5.3-Codex and Claude Sonnet 4.6 timed out). Holistic AssessmentMotivation: The PR is well-justified — the WASM build targets were monolithic, causing unnecessary webcil conversion and boot JSON regeneration on every build even when nothing changed. MSBuild incrementalism is the standard solution. Approach: The split into "resolve/compute inputs" (always-run) → "do work" (incremental with Inputs/Outputs) → "register outputs" (always-run) is the correct MSBuild pattern. Property stamp files for non-file inputs and Touch for content-comparison tasks are sound techniques. Summary: Detailed Findings✅ Target splitting pattern — CorrectThe decomposition of monolithic targets is well-executed:
FileWrites are correctly tracked in always-run wrapper targets so ✅ Touch pattern — CorrectBoth ✅ UseWebcil guard — CorrectThe guard is placed correctly. The publish tests ( ✅ CopyToPublishDirectory=Never filter — CorrectThe filter correctly excludes build-only assets (e.g., HotReload DLL) from the publish pipeline. Without this, ✅ Culture/non-culture DLL separation — CorrectThe split into
|
Add WasmEnvironmentVariable (with Value metadata), BlazorWebAssemblyLazyLoad, WasmModuleAfterConfigLoaded, WasmModuleAfterRuntimeReady, and WasmTest* properties to both build and publish boot JSON property stamp files. Rename IncrementalPublish_NoChanges_SkipsWebcilAndBootJson to IncrementalPublish_NoChanges_SkipsBootJson since publish webcil conversion is not a separate incremental target. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The WriteLinesToFile Lines parameter is an item-list context where @() item transforms cannot be concatenated with string literals using a non-semicolon separator. Pre-compute the stamp string in a PropertyGroup (string context) and pass the resulting property to Lines. Fixes both _WriteWasmBootJsonBuildPropertyStamp (build) and _WriteWasmPublishBootJsonPropertyStamp (publish) targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Improves MSBuild incrementalism for WebAssembly browser build targets in
Microsoft.NET.Sdk.WebAssembly.Browser.targets. On no-op rebuilds where inputs have not changed, the expensiveConvertDllsToWebcilandGenerateWasmBootJsontasks are now skipped via MSBuild'sInputs/Outputsmechanism.Changes
Webcil Conversion
Split
_ResolveWasmOutputsinto 3 targets:_ComputeWasmBuildCandidates(always runs) — resolves build asset candidates, classifies DLLs vs framework pass-throughs, computes expected webcil output paths_ConvertBuildDllsToWebcil(incremental) — runsConvertDllsToWebcilonly when DLL inputs are newer than webcil outputs_ResolveWasmOutputs(always runs) — reconstructs webcil items via MSBuild item transforms, classifies framework candidates, callsDefineStaticWebAssetsBoot JSON Generation
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 changeTouch outputs for content-comparison tasks
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. Added<Touch>after each task invocation to ensure output timestamps reflect the current build session.FileWrites in always-run wrapper targets
When incremental targets are skipped,
<FileWrites>items inside their body are not populated. Added<FileWrites>to the always-run wrapper targets (_GenerateBuildWasmBootJson,_AddPublishWasmBootJsonToStaticWebAssets,_ResolveWasmOutputs) sodotnet cleanworks correctly.Incrementalism Proof
Binlog analysis of Wasm.Browser.Sample — build 1 (clean) vs build 2 (no-op rebuild):
Build Path
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 avoids unnecessary downstream rebuilds, 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.Framework asset classification
The Framework SourceType classification (from #125329) is preserved. Since the incremental
_ConvertBuildDllsToWebciltarget only receives DLL items, framework candidate classification is done in MSBuild item groups in_ComputeWasmBuildCandidatesinstead of via the task'sPassThroughCandidatesoutput. Items withWasmNativeBuildOutputmetadata are per-project (Computed); items without are Framework assets needing per-project materialization.Culture/non-culture DLL separation
Culture-specific DLLs have
RelatedAssetmetadata that non-culture DLLs lack. Using%(RelatedAsset)in item transforms causes MSB4096 batching errors. The fix uses separate intermediate items (_WasmWebcilConvertedNonCulture,_WasmWebcilConvertedCulture).Testing
_ConvertBuildDllsToWebciland_WriteBuildWasmBootJsonFileNote
This PR description was generated with assistance from GitHub Copilot.