Skip to content

[browser] WebAssembly SDK targets more incremental#125367

Draft
maraf wants to merge 9 commits intomainfrom
maraf/WasmSdkIncremental
Draft

[browser] WebAssembly SDK targets more incremental#125367
maraf wants to merge 9 commits intomainfrom
maraf/WasmSdkIncremental

Conversation

@maraf
Copy link
Copy Markdown
Member

@maraf maraf commented Mar 10, 2026

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 expensive ConvertDllsToWebcil and GenerateWasmBootJson tasks are now skipped via MSBuild's Inputs/Outputs mechanism.

Changes

Webcil Conversion

Split _ResolveWasmOutputs into 3 targets:

  • _ComputeWasmBuildCandidates (always runs) — resolves build asset candidates, classifies DLLs vs framework pass-throughs, computes expected webcil output paths
  • _ConvertBuildDllsToWebcil (incremental) — runs ConvertDllsToWebcil only when DLL inputs are newer than webcil outputs
  • _ResolveWasmOutputs (always runs) — reconstructs webcil items via MSBuild item transforms, classifies framework candidates, calls DefineStaticWebAssets

Boot JSON Generation

Split _GenerateBuildWasmBootJson into 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 output

Split GeneratePublishWasmBootJson into 2 targets:

  • _ResolvePublishWasmBootJsonInputs (always runs) — resolves publish endpoints
  • GeneratePublishWasmBootJson (incremental) — writes boot JSON only when inputs change

Touch outputs for content-comparison tasks

Both ConvertDllsToWebcil (Utils.MoveIfDifferent) and GenerateWasmBootJson (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) so dotnet clean works correctly.

Incrementalism Proof

Binlog analysis of Wasm.Browser.Sample — build 1 (clean) vs build 2 (no-op rebuild):

Build Path

Target                                   Build 1    Build 2    Status
----------------------------------------------------------------------
_ComputeWasmBuildCandidates                59ms       62ms     Runs (item producer, always needed)
_ConvertBuildDllsToWebcil                  60ms        7ms     ✅ SKIPPED — outputs up-to-date
_ResolveWasmOutputs                       138ms      116ms     Runs (DefineStaticWebAssets, item producer)
_ResolveBuildWasmBootJsonEndpoints         21ms       20ms     Runs (item producer)
_WriteBuildWasmBootJsonFile               104ms        4ms     ✅ SKIPPED — output up-to-date
_GenerateBuildWasmBootJson                  3ms        3ms     Runs (item producer)

Design Notes

Why split instead of just adding Inputs/Outputs?

MSBuild's Inputs/Outputs mechanism skips the entire target body when outputs are up-to-date. Since _ResolveWasmOutputs and _GenerateBuildWasmBootJson both 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 ConvertDllsToWebcil and GenerateWasmBootJson implement "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 _ConvertBuildDllsToWebcil target only receives DLL items, framework candidate classification is done in MSBuild item groups in _ComputeWasmBuildCandidates instead of via the task's PassThroughCandidates output. Items with WasmNativeBuildOutput metadata are per-project (Computed); items without are Framework assets needing per-project materialization.

Culture/non-culture DLL separation

Culture-specific DLLs have RelatedAsset metadata that non-culture DLLs lack. Using %(RelatedAsset) in item transforms causes MSB4096 batching errors. The fix uses separate intermediate items (_WasmWebcilConvertedNonCulture, _WasmWebcilConvertedCulture).

Testing

  • Clean build and no-op rebuild succeed with zero errors
  • No-op rebuild correctly skips _ConvertBuildDllsToWebcil and _WriteBuildWasmBootJsonFile
  • Binlog analysis verified with MSBuild.StructuredLogger

Note

This PR description was generated with assistance from GitHub Copilot.

@maraf maraf added this to the 11.0.0 milestone Mar 10, 2026
@maraf maraf self-assigned this Mar 10, 2026
@maraf maraf added arch-wasm WebAssembly architecture area-Build-mono labels Mar 10, 2026
Copilot AI review requested due to automatic review settings March 10, 2026 08:18
@maraf maraf added the os-browser Browser variant of arch-wasm label Mar 10, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 _ResolveWasmOutputs target is split into _ComputeWasmBuildCandidates (resolve/classify candidates), _ConvertBuildDllsToWebcil (incremental webcil conversion), and _ResolveWasmOutputs (reconstruct webcil metadata and define static web assets).
  • The _GenerateBuildWasmBootJson target is split into _ResolveBuildWasmBootJsonEndpoints (resolve endpoints), _WriteBuildWasmBootJsonFile (incremental boot JSON generation), and _GenerateBuildWasmBootJson (define static web asset for boot config).
  • The GeneratePublishWasmBootJson target is split into _ResolvePublishWasmBootJsonInputs (resolve inputs) and GeneratePublishWasmBootJson (incremental publish boot JSON generation).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI review requested due to automatic review settings March 12, 2026 20:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@maraf maraf force-pushed the maraf/WasmSdkIncremental branch from 084b5ee to ab57788 Compare April 1, 2026 11:42
@github-actions

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>
Copilot AI review requested due to automatic review settings April 1, 2026 11:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs
@github-actions

This comment has been minimized.

maraf and others added 2 commits April 1, 2026 14:09
…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>
Copilot AI review requested due to automatic review settings April 1, 2026 14:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs Outdated
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)"
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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, ';')"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

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>
@github-actions

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>
Copilot AI review requested due to automatic review settings April 20, 2026 14:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.

@github-actions

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>
@github-actions
Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #125367

Note

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 Assessment

Motivation: 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: ⚠️ Needs Human Review. The code is well-structured and the approach is correct. There are a few missing inputs in the incremental tracking that could cause stale outputs in edge cases. The test coverage validates the happy path but doesn’t exercise the property-change or culture-assembly scenarios. A human reviewer familiar with the WASM SDK should evaluate whether the missing inputs matter in practice.


Detailed Findings

✅ Target splitting pattern — Correct

The decomposition of monolithic targets is well-executed:

  • _ComputeWasmBuildCandidates (always-run) → _ConvertBuildDllsToWebcil (incremental) → _ResolveWasmOutputs (always-run)
  • _ResolveBuildWasmBootJsonEndpoints (always-run) → _WriteWasmBootJsonBuildPropertyStamp_WriteBuildWasmBootJsonFile (incremental) → _GenerateBuildWasmBootJson (always-run)
  • Same pattern for publish path

FileWrites are correctly tracked in always-run wrapper targets so dotnet clean works even when incremental targets are skipped.

✅ Touch pattern — Correct

Both _ConvertBuildDllsToWebcil and _WriteBuildWasmBootJsonFile use Touch after tasks that employ content comparison (MoveIfDifferent). This is necessary: without Touch, MSBuild would see old timestamps on unchanged outputs and re-run the target on the next build. The Condition="'@(_WasmConvertedWebcilOutputs)' != ''" guard on the webcil Touch prevents errors on empty output.

✅ UseWebcil guard — Correct

The guard is placed correctly. _ConvertBuildDllsToWebcil has Condition="'$(_WasmEnableWebcil)' == 'true'", so when webcil is disabled the target doesn’t appear in the binlog at all. The test guards in RebuildTests.cs correctly match the target’s Condition. This follows the same pattern used in WasmTemplateTests.cs.

The publish tests (IncrementalPublish_*) correctly do NOT need the guard — they only assert on GeneratePublishWasmBootJson, which runs regardless of webcil setting.

✅ CopyToPublishDirectory=Never filter — Correct

The filter correctly excludes build-only assets (e.g., HotReload DLL) from the publish pipeline. Without this, ComputeWasmPublishAssets could match them by filename and cause duplicate Identity crashes in multi-client hosted scenarios.

✅ Culture/non-culture DLL separation — Correct

The split into _WasmDllBuildCandidatesNonCulture and _WasmDllBuildCandidatesCulture correctly handles the MSBuild batching issue where RelatedAsset metadata only exists on culture items. The _ResolveWasmOutputs target mirrors this split for path reconstruction.

⚠️ Boot JSON stamp missing some non-file inputs — Advisory, follow-up

Flagged by: Goldeneye + primary reviewer

The property stamp files (wasm-bootjson-build.stamp, wasm-bootjson-publish.stamp) capture 15 properties but omit several inputs consumed by GenerateWasmBootJson:

  • @(BlazorWebAssemblyLazyLoad) — item group for lazy loading
  • @(WasmEnvironmentVariable) — environment variables
  • @(WasmModuleAfterConfigLoaded) / @(WasmModuleAfterRuntimeReady) — JS module hooks
  • WasmTest* properties: ExitOnUnhandledError, AppendElementOnExit, LogExitCode, AsyncFlushOnExit, ForwardConsole

Mitigating factors: Most of these are covered indirectly:

  • Item groups set in the csproj are covered by $(MSBuildProjectFullPath) in Inputs
  • Item groups computed by targets are covered by $(MSBuildThisFileFullPath) in Inputs
  • WasmEnvironmentVariable is populated from $(DiagnosticPorts) which IS in the stamp
  • WasmTest* properties are test-infrastructure-only and don’t change during normal development

The gap only manifests when these values change via /p: on the command line without any file changes. This is an uncommon development pattern, and the pre-PR behavior (always re-running) was worse. Consider tracking @(BlazorWebAssemblyLazyLoad) and @(WasmModuleAfter*) in the Inputs (as item groups) in a follow-up if lazy-load scenarios become important for incrementalism.

⚠️ _WasmWebcilVersion not tracked in webcil target Inputs — Advisory, follow-up

Flagged by: Goldeneye + primary reviewer

_WasmWebcilVersion is passed to ConvertDllsToWebcil but is not in the target’s Inputs list or a property stamp. If this property changes without any file modifications, the target would be skipped and produce stale webcil files.

Mitigating factor: _WasmWebcilVersion is computed from RuntimeFlavor (CoreCLR→1, Mono→0), which is fixed for a given build configuration. Changes via the csproj are covered by $(MSBuildProjectFullPath) in Inputs. This is unlikely to cause real-world issues, but a property stamp for _WasmWebcilVersion would make the incrementalism more robust.

💡 Test name mismatch — IncrementalPublish_NoChanges_SkipsWebcilAndBootJson

This test only asserts that GeneratePublishWasmBootJson is skipped — it does not verify any webcil target behavior. The name "SkipsWebcilAndBootJson" is misleading. Consider renaming to IncrementalPublish_NoChanges_SkipsBootJson for accuracy.

💡 Test coverage could be broader — Follow-up

All new tests use aot: false and don’t exercise:

  • Culture/satellite assembly changes (the culture-split logic is new and untested)
  • Property-stamp-triggered rebuilds (e.g., changing WasmDebugLevel between builds)
  • Multi-project/hosted publish scenarios

The current tests validate the core incrementalism happy path (no-change skip, source-change rebuild), which is the most important case. The above scenarios would strengthen confidence but aren’t blocking.

Generated by Code Review for issue #125367 ·

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>
Copilot AI review requested due to automatic review settings April 21, 2026 14:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 3 changed files in this pull request and generated 1 comment.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-Build-mono os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants