From ab5778850de57b5b3056f241a43033ee3eeec08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 1 Apr 2026 11:18:04 +0000 Subject: [PATCH 1/9] Add MSBuild incrementalism to WASM build targets 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> --- ...rosoft.NET.Sdk.WebAssembly.Browser.targets | 147 ++++++++++++++++-- 1 file changed, 136 insertions(+), 11 deletions(-) diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index 171388aee4da54..397bbe968cd5bd 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -289,7 +289,9 @@ Copyright (c) .NET Foundation. All rights reserved. - + + <_WasmNativeAssetFileNames>;@(WasmNativeAsset->'%(FileName)%(Extension)');@(WasmAssembliesFinal->'%(FileName)%(Extension)'); <_WasmIntermediateAssemblyFileNames Condition="@(WasmAssembliesFinal->Count()) != 0">;@(IntermediateAssembly->'%(FileName)%(Extension)'); @@ -353,17 +355,99 @@ Copyright (c) .NET Foundation. All rights reserved. <_WasmBuildTmpWebcilPath>$([MSBuild]::NormalizeDirectory($(IntermediateOutputPath), 'tmp-webcil')) - - - + + + <_WasmDllBuildCandidates Include="@(_BuildAssetsCandidates)" Condition="'%(Extension)' == '.dll'" /> + <_WasmDllBuildCandidatesNonCulture Include="@(_WasmDllBuildCandidates)" Condition="'%(AssetTraitName)' != 'Culture'" /> + <_WasmDllBuildCandidatesCulture Include="@(_WasmDllBuildCandidates)" Condition="'%(AssetTraitName)' == 'Culture'" /> + <_WasmExpectedWebcilOutputs Include="@(_WasmDllBuildCandidatesNonCulture->'$(_WasmBuildWebcilPath)%(FileName).wasm')" /> + <_WasmExpectedWebcilOutputs Include="@(_WasmDllBuildCandidatesCulture->'$(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm')" /> + + + + + <_WasmNativeBuildOutputCandidates Include="@(_BuildAssetsCandidates)" Condition="'%(Extension)' != '.dll' and '%(_BuildAssetsCandidates.WasmNativeBuildOutput)' != ''" /> + <_WasmNonDllNonNativeCandidates Include="@(_BuildAssetsCandidates)" Condition="'%(Extension)' != '.dll'" /> + <_WasmNonDllNonNativeCandidates Remove="@(_WasmNativeBuildOutputCandidates)" /> + + + + + + + + - - - <_WebcilAssetsCandidates Remove="@(_WasmFrameworkCandidates)" /> + + + + + + + + + + <_WasmWebcilConvertedNonCulture Include="@(_WasmDllBuildCandidatesNonCulture->'$(_WasmBuildWebcilPath)%(FileName).wasm')"> + $([System.IO.Path]::ChangeExtension(%(RelativePath), '.wasm')) + $(_WasmBuildWebcilPath)%(FileName).wasm + + <_WasmWebcilConvertedCulture Include="@(_WasmDllBuildCandidatesCulture->'$(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm')"> + $([System.IO.Path]::ChangeExtension(%(RelativePath), '.wasm')) + $(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm + $([System.IO.Path]::ChangeExtension(%(RelatedAsset), '.wasm')) + + + + <_WebcilAssetsCandidates Include="@(_WasmNativeBuildOutputCandidates)" /> + <_WebcilAssetsCandidates Include="@(_WasmWebcilConvertedNonCulture)" /> + <_WebcilAssetsCandidates Include="@(_WasmWebcilConvertedCulture)" /> + + + <_WasmFrameworkCandidates Include="@(_WasmNonDllNonNativeCandidates)" /> + + + + + + + + <_WebcilAssetsCandidates Include="@(_WasmNativeBuildOutputCandidates)" /> + <_WasmFrameworkCandidates Include="@(_WasmNonDllNonNativeCandidates)" /> + <_WasmFrameworkCandidates Include="@(_BuildAssetsCandidates)" Condition="'%(Extension)' == '.dll'" /> @@ -480,7 +564,9 @@ Copyright (c) .NET Foundation. All rights reserved. - + + <_WasmBuildBootJsonPath>$(IntermediateOutputPath)$(_WasmBootConfigFileName) <_WasmBuildApplicationEnvironmentName>$(WasmApplicationEnvironmentName) @@ -534,6 +620,15 @@ Copyright (c) .NET Foundation. All rights reserved. > + + + + + + + + + + + + + <_WasmBuildBootConfigCandidate Include="$(_WasmBuildBootJsonPath)" RelativePath="_framework/$(_WasmBootConfigFileName)" /> @@ -887,6 +994,8 @@ Copyright (c) .NET Foundation. All rights reserved. + + <_WasmPublishBootConfigCandidate Include="$(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)" RelativePath="_framework/$(_WasmBootConfigFileName)" /> @@ -924,7 +1033,9 @@ Copyright (c) .NET Foundation. All rights reserved. - + + <_WasmPublishApplicationEnvironmentName>$(WasmApplicationEnvironmentName) @@ -957,6 +1068,15 @@ Copyright (c) .NET Foundation. All rights reserved. > + + + + + + + Date: Wed, 1 Apr 2026 11:57:51 +0000 Subject: [PATCH 2/9] Add Wasm.Build.Tests for incremental build verification 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> --- .../wasm/Wasm.Build.Tests/RebuildTests.cs | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs index a3b8c189f26531..0a8311cb63d714 100644 --- a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs @@ -9,6 +9,7 @@ using Xunit; using Xunit.Abstractions; using Xunit.Sdk; +using SL = Microsoft.Build.Logging.StructuredLogger; #nullable enable @@ -46,5 +47,127 @@ public async Task NoOpRebuild(Configuration config, bool aot) PublishProject(info, config, new PublishOptions(UseCache: false)); await RunForPublishWithWebServer(runOptions); } + + [Theory] + [InlineData(Configuration.Debug)] + [InlineData(Configuration.Release)] + public void IncrementalBuild_NoChanges_SkipsWebcilAndBootJson(Configuration config) + { + ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.WasmBasicTestApp, "incremental_noop"); + UpdateFile(Path.Combine("Common", "Program.cs"), s_mainReturns42); + + (_, string firstBinlog) = BuildProjectWithoutAssert(config, info.ProjectName, new BuildOptions(Label: "first")); + _testOutput.WriteLine($"First build binlog: {firstBinlog}"); + + // no-op rebuild + (_, string secondBinlog) = BuildProjectWithoutAssert(config, info.ProjectName, new BuildOptions(UseCache: false, Label: "second")); + _testOutput.WriteLine($"Second build binlog: {secondBinlog}"); + + AssertTargetSkipped(secondBinlog, "_ConvertBuildDllsToWebcil"); + AssertTargetSkipped(secondBinlog, "_WriteBuildWasmBootJsonFile"); + } + + [Theory] + [InlineData(Configuration.Debug)] + [InlineData(Configuration.Release)] + public void IncrementalBuild_SourceChange_RunsWebcilForAppOnly(Configuration config) + { + ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.WasmBasicTestApp, "incremental_src"); + UpdateFile(Path.Combine("Common", "Program.cs"), s_mainReturns42); + + BuildProjectWithoutAssert(config, info.ProjectName, new BuildOptions(Label: "first")); + + // modify app source to trigger recompilation + string programPath = Path.Combine(_projectDir, "Common", "Program.cs"); + File.AppendAllText(programPath, Environment.NewLine + "// incremental rebuild trigger"); + + (_, string secondBinlog) = BuildProjectWithoutAssert(config, info.ProjectName, new BuildOptions(UseCache: false, Label: "second")); + _testOutput.WriteLine($"Second build binlog: {secondBinlog}"); + + // Webcil and boot JSON targets must run because the app assembly changed + AssertTargetRan(secondBinlog, "_ConvertBuildDllsToWebcil"); + AssertTargetRan(secondBinlog, "_WriteBuildWasmBootJsonFile"); + + // Only the app assembly should have been re-converted (not framework DLLs) + var convertedFiles = GetConvertedWebcilFiles(secondBinlog); + _testOutput.WriteLine($"Webcil-converted files: {string.Join(", ", convertedFiles)}"); + Assert.Single(convertedFiles); + Assert.Contains(convertedFiles, f => f.Contains(info.ProjectName, StringComparison.OrdinalIgnoreCase)); + } + + private static void AssertTargetSkipped(string binlogPath, string targetName) + { + var build = SL.BinaryLog.ReadBuild(binlogPath); + SL.BuildAnalyzer.AnalyzeBuild(build); + + bool found = false; + bool skipped = false; + build.VisitAllChildren(t => + { + if (t.Name == targetName) + { + found = true; + if (t.Children.OfType().Any(m => + m.Text is not null && m.Text.Contains("Skipping target"))) + { + skipped = true; + } + } + }); + + Assert.True(found, $"Target '{targetName}' was not found in the binlog '{binlogPath}'."); + Assert.True(skipped, $"Target '{targetName}' was expected to be skipped but it ran."); + } + + private static void AssertTargetRan(string binlogPath, string targetName) + { + var build = SL.BinaryLog.ReadBuild(binlogPath); + SL.BuildAnalyzer.AnalyzeBuild(build); + + bool found = false; + bool ran = false; + build.VisitAllChildren(t => + { + if (t.Name == targetName) + { + found = true; + if (t.Children.OfType().Any(m => + m.Text is not null && m.Text.Contains("Building target"))) + { + ran = true; + } + } + }); + + Assert.True(found, $"Target '{targetName}' was not found in the binlog '{binlogPath}'."); + Assert.True(ran, $"Target '{targetName}' was expected to run but it was skipped."); + } + + private static List GetConvertedWebcilFiles(string binlogPath) + { + var build = SL.BinaryLog.ReadBuild(binlogPath); + SL.BuildAnalyzer.AnalyzeBuild(build); + + var converted = new List(); + build.VisitAllChildren(t => + { + if (t.Name != "_ConvertBuildDllsToWebcil") + return; + + foreach (var child in t.Children) + { + if (child is not SL.Task { Name: "ConvertDllsToWebcil" } convertTask) + continue; + + convertTask.VisitAllChildren(m => + { + if (m.Text is not null && m.Text.StartsWith("Converting to Webcil:")) + converted.Add(m.Text); + }); + } + }); + + return converted; + } } } From 7297b6b2f5d0fb213ac679db1160cd0db46e05aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 1 Apr 2026 14:09:06 +0000 Subject: [PATCH 3/9] Address review feedback: add missing boot JSON inputs and property stamps - 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> --- ...rosoft.NET.Sdk.WebAssembly.Browser.targets | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index 397bbe968cd5bd..b5e0644fd4de1a 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -622,12 +622,27 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + + + + + This target is incremental: when all inputs (assemblies, static web assets, VFS files, + config files, extensions, property stamp) are older than the output, the target is skipped. --> + + + + + + + + + extensions, property stamp) are older than the output, the target is skipped. --> Date: Wed, 1 Apr 2026 14:27:09 +0000 Subject: [PATCH 4/9] Add publish incrementalism tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../wasm/Wasm.Build.Tests/RebuildTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs index 0a8311cb63d714..fea75afb54b4c1 100644 --- a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs @@ -48,6 +48,44 @@ public async Task NoOpRebuild(Configuration config, bool aot) await RunForPublishWithWebServer(runOptions); } + [Theory] + [InlineData(Configuration.Debug)] + [InlineData(Configuration.Release)] + public void IncrementalPublish_NoChanges_SkipsWebcilAndBootJson(Configuration config) + { + ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.WasmBasicTestApp, "incremental_pub_noop"); + UpdateFile(Path.Combine("Common", "Program.cs"), s_mainReturns42); + + (_, string firstBinlog) = BuildProjectWithoutAssert(config, info.ProjectName, new PublishOptions(Label: "first")); + _testOutput.WriteLine($"First publish binlog: {firstBinlog}"); + + // no-op publish + (_, string secondBinlog) = BuildProjectWithoutAssert(config, info.ProjectName, new PublishOptions(UseCache: false, Label: "second")); + _testOutput.WriteLine($"Second publish binlog: {secondBinlog}"); + + AssertTargetSkipped(secondBinlog, "GeneratePublishWasmBootJson"); + } + + [Theory] + [InlineData(Configuration.Debug)] + [InlineData(Configuration.Release)] + public void IncrementalPublish_SourceChange_RunsBootJson(Configuration config) + { + ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.WasmBasicTestApp, "incremental_pub_src"); + UpdateFile(Path.Combine("Common", "Program.cs"), s_mainReturns42); + + BuildProjectWithoutAssert(config, info.ProjectName, new PublishOptions(Label: "first")); + + // modify app source to trigger recompilation + string programPath = Path.Combine(_projectDir, "Common", "Program.cs"); + File.AppendAllText(programPath, Environment.NewLine + "// incremental publish trigger"); + + (_, string secondBinlog) = BuildProjectWithoutAssert(config, info.ProjectName, new PublishOptions(UseCache: false, Label: "second")); + _testOutput.WriteLine($"Second publish binlog: {secondBinlog}"); + + AssertTargetRan(secondBinlog, "GeneratePublishWasmBootJson"); + } + [Theory] [InlineData(Configuration.Debug)] [InlineData(Configuration.Release)] From a82d469d2a14fd2b9bdf39cfd52b371e5bfc54a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 1 Apr 2026 14:48:20 +0000 Subject: [PATCH 5/9] Enable MSBuild partial execution for webcil conversion 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> --- ...rosoft.NET.Sdk.WebAssembly.Browser.targets | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index b5e0644fd4de1a..039ac65ef35dba 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -365,11 +365,22 @@ Copyright (c) .NET Foundation. All rights reserved. materialization. Pre-filtering avoids MSBuild batching errors on WasmNativeBuildOutput metadata that only WasmNativeAsset items define. --> + <_WasmDllBuildCandidates Include="@(_BuildAssetsCandidates)" Condition="'%(Extension)' == '.dll'" /> - <_WasmDllBuildCandidatesNonCulture Include="@(_WasmDllBuildCandidates)" Condition="'%(AssetTraitName)' != 'Culture'" /> - <_WasmDllBuildCandidatesCulture Include="@(_WasmDllBuildCandidates)" Condition="'%(AssetTraitName)' == 'Culture'" /> - <_WasmExpectedWebcilOutputs Include="@(_WasmDllBuildCandidatesNonCulture->'$(_WasmBuildWebcilPath)%(FileName).wasm')" /> - <_WasmExpectedWebcilOutputs Include="@(_WasmDllBuildCandidatesCulture->'$(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm')" /> + <_WasmDllBuildCandidatesNonCulture Include="@(_WasmDllBuildCandidates)" Condition="'%(AssetTraitName)' != 'Culture'"> + $(_WasmBuildWebcilPath)%(FileName).wasm + + <_WasmDllBuildCandidatesCulture Include="@(_WasmDllBuildCandidates)" Condition="'%(AssetTraitName)' == 'Culture'"> + $(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm + + + <_WasmDllBuildCandidates Remove="@(_WasmDllBuildCandidates)" /> + <_WasmDllBuildCandidates Include="@(_WasmDllBuildCandidatesNonCulture);@(_WasmDllBuildCandidatesCulture)" /> + <_WasmExpectedWebcilOutputs Include="@(_WasmDllBuildCandidates->'%(WebcilOutputPath)')" /> + Outputs is a direct transform of the item-based Inputs, enabling MSBuild partial target + execution: when only some DLLs change, MSBuild passes only out-of-date pairs to the + target body. Property inputs (project/targets/task assembly) are global dependencies — + if any changes, all outputs are rebuilt. + Touch is needed because the task uses content comparison (MoveIfDifferent) which preserves + old timestamps when webcil content is unchanged. Without Touch, property-triggered full + rebuilds would loop since skipped outputs retain pre-existing timestamps. --> + Outputs="@(_WasmDllBuildCandidates->'%(WebcilOutputPath)')"> - + From fa45651ec95cdb9effddc5176c8de6c9ecfa8d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Tue, 21 Apr 2026 08:31:10 +0000 Subject: [PATCH 6/9] Guard webcil target assertions behind UseWebcil in RebuildTests 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> --- .../wasm/Wasm.Build.Tests/RebuildTests.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs index fea75afb54b4c1..7d16048cde384d 100644 --- a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs @@ -101,7 +101,8 @@ public void IncrementalBuild_NoChanges_SkipsWebcilAndBootJson(Configuration conf (_, string secondBinlog) = BuildProjectWithoutAssert(config, info.ProjectName, new BuildOptions(UseCache: false, Label: "second")); _testOutput.WriteLine($"Second build binlog: {secondBinlog}"); - AssertTargetSkipped(secondBinlog, "_ConvertBuildDllsToWebcil"); + if (UseWebcil) + AssertTargetSkipped(secondBinlog, "_ConvertBuildDllsToWebcil"); AssertTargetSkipped(secondBinlog, "_WriteBuildWasmBootJsonFile"); } @@ -122,15 +123,20 @@ public void IncrementalBuild_SourceChange_RunsWebcilForAppOnly(Configuration con (_, string secondBinlog) = BuildProjectWithoutAssert(config, info.ProjectName, new BuildOptions(UseCache: false, Label: "second")); _testOutput.WriteLine($"Second build binlog: {secondBinlog}"); - // Webcil and boot JSON targets must run because the app assembly changed - AssertTargetRan(secondBinlog, "_ConvertBuildDllsToWebcil"); + // Boot JSON target must run because the app assembly changed AssertTargetRan(secondBinlog, "_WriteBuildWasmBootJsonFile"); - // Only the app assembly should have been re-converted (not framework DLLs) - var convertedFiles = GetConvertedWebcilFiles(secondBinlog); - _testOutput.WriteLine($"Webcil-converted files: {string.Join(", ", convertedFiles)}"); - Assert.Single(convertedFiles); - Assert.Contains(convertedFiles, f => f.Contains(info.ProjectName, StringComparison.OrdinalIgnoreCase)); + if (UseWebcil) + { + // Webcil conversion target must run because the app assembly changed + AssertTargetRan(secondBinlog, "_ConvertBuildDllsToWebcil"); + + // Only the app assembly should have been re-converted (not framework DLLs) + var convertedFiles = GetConvertedWebcilFiles(secondBinlog); + _testOutput.WriteLine($"Webcil-converted files: {string.Join(", ", convertedFiles)}"); + Assert.Single(convertedFiles); + Assert.Contains(convertedFiles, f => f.Contains(info.ProjectName, StringComparison.OrdinalIgnoreCase)); + } } private static void AssertTargetSkipped(string binlogPath, string targetName) From 58d8153f8937f45ab13344c06a9851bc2130b09c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Tue, 21 Apr 2026 14:03:34 +0000 Subject: [PATCH 7/9] Address review: add missing non-file inputs to boot JSON stamps 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> --- .../build/Microsoft.NET.Sdk.WebAssembly.Browser.targets | 4 ++-- src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index 3382f8a036f142..fa7f3ac6bc4629 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -648,7 +648,7 @@ Copyright (c) .NET Foundation. All rights reserved. DependsOnTargets="_ResolveBuildWasmBootJsonEndpoints"> @@ -1115,7 +1115,7 @@ Copyright (c) .NET Foundation. All rights reserved. DependsOnTargets="_ResolvePublishWasmBootJsonInputs"> diff --git a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs index 7d16048cde384d..6174afab601d1a 100644 --- a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs @@ -51,7 +51,7 @@ public async Task NoOpRebuild(Configuration config, bool aot) [Theory] [InlineData(Configuration.Debug)] [InlineData(Configuration.Release)] - public void IncrementalPublish_NoChanges_SkipsWebcilAndBootJson(Configuration config) + public void IncrementalPublish_NoChanges_SkipsBootJson(Configuration config) { ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.WasmBasicTestApp, "incremental_pub_noop"); UpdateFile(Path.Combine("Common", "Program.cs"), s_mainReturns42); From 61635e32743be98bcba8b369024dc5c909e26fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 22 Apr 2026 15:36:45 +0000 Subject: [PATCH 8/9] Fix MSB4012 in boot JSON property stamp targets 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> --- .../Microsoft.NET.Sdk.WebAssembly.Browser.targets | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index fa7f3ac6bc4629..bcafb291ee756c 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -646,9 +646,12 @@ Copyright (c) .NET Foundation. All rights reserved. incremental boot JSON target. WriteOnlyWhenDifferent preserves the timestamp when unchanged. --> + + <_WasmBootJsonBuildStampContent>$(WasmDebugLevel)|$(_WasmBuildApplicationEnvironmentName)|$(_BlazorCacheBootResources)|$(InvariantGlobalization)|$(_LoadCustomIcuData)|$(_BlazorWebAssemblyLoadAllGlobalizationData)|$(_BlazorWebAssemblyJiterpreter)|$(_BlazorWebAssemblyRuntimeOptions)|$(RunAOTCompilation)|$(WasmEnableThreads)|$(_WasmFingerprintAssets)|$(_WasmBundlerFriendlyBootConfig)|$(WasmProfilers)|$(TargetFrameworkVersion)|$(DiagnosticPorts)|@(WasmEnvironmentVariable->'%(Identity)=%(Value)')|@(BlazorWebAssemblyLazyLoad)|@(WasmModuleAfterConfigLoaded)|@(WasmModuleAfterRuntimeReady)|$(WasmTestExitOnUnhandledError)|$(WasmTestAppendElementOnExit)|$(WasmTestLogExitCode)|$(WasmTestAsyncFlushOnExit)|$(WasmTestForwardConsole) + @@ -1113,9 +1116,12 @@ Copyright (c) .NET Foundation. All rights reserved. + + <_WasmBootJsonPublishStampContent>$(WasmDebugLevel)|$(_WasmPublishApplicationEnvironmentName)|$(_BlazorCacheBootResources)|$(InvariantGlobalization)|$(_LoadCustomIcuData)|$(_BlazorWebAssemblyLoadAllGlobalizationData)|$(_BlazorWebAssemblyJiterpreter)|$(_BlazorWebAssemblyRuntimeOptions)|$(RunAOTCompilation)|$(WasmEnableThreads)|$(_WasmFingerprintAssets)|$(_WasmBundlerFriendlyBootConfig)|$(WasmProfilers)|$(TargetFrameworkVersion)|$(DiagnosticPorts)|@(WasmEnvironmentVariable->'%(Identity)=%(Value)')|@(BlazorWebAssemblyLazyLoad)|@(WasmModuleAfterConfigLoaded)|@(WasmModuleAfterRuntimeReady)|$(WasmTestExitOnUnhandledError)|$(WasmTestAppendElementOnExit)|$(WasmTestLogExitCode)|$(WasmTestAsyncFlushOnExit)|$(WasmTestForwardConsole) + From c09aa29e6fdfc44bedfcb3eb2243f3085f44ef80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Fri, 24 Apr 2026 10:42:19 +0000 Subject: [PATCH 9/9] Address review feedback: reuse WebcilOutputPath, add StaticWebAssetStandaloneHosting to stamps - Reuse %(WebcilOutputPath) metadata in _ResolveWasmOutputs instead of recomputing paths from _WasmBuildWebcilPath + FileName/AssetTraitValue, eliminating duplicated path logic and risk of future divergence. - Add to both build and publish boot JSON property stamps so standalone-vs-hosted hosting mode changes correctly invalidate the incremental boot JSON targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...rosoft.NET.Sdk.WebAssembly.Browser.targets | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index 3e48e75279b8c0..63554a5b72e5e7 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -432,25 +432,26 @@ Copyright (c) .NET Foundation. All rights reserved. - - <_WasmWebcilConvertedNonCulture Include="@(_WasmDllBuildCandidatesNonCulture->'$(_WasmBuildWebcilPath)%(FileName).wasm')"> + <_WasmWebcilConvertedNonCulture Include="@(_WasmDllBuildCandidatesNonCulture->'%(WebcilOutputPath)')"> $([System.IO.Path]::ChangeExtension(%(RelativePath), '.wasm')) - $(_WasmBuildWebcilPath)%(FileName).wasm + %(WebcilOutputPath) - <_WasmWebcilConvertedCulture Include="@(_WasmDllBuildCandidatesCulture->'$(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm')"> + <_WasmWebcilConvertedCulture Include="@(_WasmDllBuildCandidatesCulture->'%(WebcilOutputPath)')"> $([System.IO.Path]::ChangeExtension(%(RelativePath), '.wasm')) - $(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm + %(WebcilOutputPath) $([System.IO.Path]::ChangeExtension(%(RelatedAsset), '.wasm')) @@ -653,7 +654,7 @@ Copyright (c) .NET Foundation. All rights reserved. - <_WasmBootJsonBuildStampContent>$(WasmDebugLevel)|$(_WasmBuildApplicationEnvironmentName)|$(_BlazorCacheBootResources)|$(InvariantGlobalization)|$(_LoadCustomIcuData)|$(_BlazorWebAssemblyLoadAllGlobalizationData)|$(_BlazorWebAssemblyJiterpreter)|$(_BlazorWebAssemblyRuntimeOptions)|$(RunAOTCompilation)|$(WasmEnableThreads)|$(_WasmFingerprintAssets)|$(_WasmBundlerFriendlyBootConfig)|$(WasmProfilers)|$(TargetFrameworkVersion)|$(DiagnosticPorts)|@(WasmEnvironmentVariable->'%(Identity)=%(Value)')|@(BlazorWebAssemblyLazyLoad)|@(WasmModuleAfterConfigLoaded)|@(WasmModuleAfterRuntimeReady)|$(WasmTestExitOnUnhandledError)|$(WasmTestAppendElementOnExit)|$(WasmTestLogExitCode)|$(WasmTestAsyncFlushOnExit)|$(WasmTestForwardConsole) + <_WasmBootJsonBuildStampContent>$(WasmDebugLevel)|$(_WasmBuildApplicationEnvironmentName)|$(_BlazorCacheBootResources)|$(InvariantGlobalization)|$(_LoadCustomIcuData)|$(_BlazorWebAssemblyLoadAllGlobalizationData)|$(_BlazorWebAssemblyJiterpreter)|$(_BlazorWebAssemblyRuntimeOptions)|$(RunAOTCompilation)|$(WasmEnableThreads)|$(_WasmFingerprintAssets)|$(_WasmBundlerFriendlyBootConfig)|$(WasmProfilers)|$(TargetFrameworkVersion)|$(DiagnosticPorts)|$(StaticWebAssetStandaloneHosting)|@(WasmEnvironmentVariable->'%(Identity)=%(Value)')|@(BlazorWebAssemblyLazyLoad)|@(WasmModuleAfterConfigLoaded)|@(WasmModuleAfterRuntimeReady)|$(WasmTestExitOnUnhandledError)|$(WasmTestAppendElementOnExit)|$(WasmTestLogExitCode)|$(WasmTestAsyncFlushOnExit)|$(WasmTestForwardConsole) - <_WasmBootJsonPublishStampContent>$(WasmDebugLevel)|$(_WasmPublishApplicationEnvironmentName)|$(_BlazorCacheBootResources)|$(InvariantGlobalization)|$(_LoadCustomIcuData)|$(_BlazorWebAssemblyLoadAllGlobalizationData)|$(_BlazorWebAssemblyJiterpreter)|$(_BlazorWebAssemblyRuntimeOptions)|$(RunAOTCompilation)|$(WasmEnableThreads)|$(_WasmFingerprintAssets)|$(_WasmBundlerFriendlyBootConfig)|$(WasmProfilers)|$(TargetFrameworkVersion)|$(DiagnosticPorts)|@(WasmEnvironmentVariable->'%(Identity)=%(Value)')|@(BlazorWebAssemblyLazyLoad)|@(WasmModuleAfterConfigLoaded)|@(WasmModuleAfterRuntimeReady)|$(WasmTestExitOnUnhandledError)|$(WasmTestAppendElementOnExit)|$(WasmTestLogExitCode)|$(WasmTestAsyncFlushOnExit)|$(WasmTestForwardConsole) + <_WasmBootJsonPublishStampContent>$(WasmDebugLevel)|$(_WasmPublishApplicationEnvironmentName)|$(_BlazorCacheBootResources)|$(InvariantGlobalization)|$(_LoadCustomIcuData)|$(_BlazorWebAssemblyLoadAllGlobalizationData)|$(_BlazorWebAssemblyJiterpreter)|$(_BlazorWebAssemblyRuntimeOptions)|$(RunAOTCompilation)|$(WasmEnableThreads)|$(_WasmFingerprintAssets)|$(_WasmBundlerFriendlyBootConfig)|$(WasmProfilers)|$(TargetFrameworkVersion)|$(DiagnosticPorts)|$(StaticWebAssetStandaloneHosting)|@(WasmEnvironmentVariable->'%(Identity)=%(Value)')|@(BlazorWebAssemblyLazyLoad)|@(WasmModuleAfterConfigLoaded)|@(WasmModuleAfterRuntimeReady)|$(WasmTestExitOnUnhandledError)|$(WasmTestAppendElementOnExit)|$(WasmTestLogExitCode)|$(WasmTestAsyncFlushOnExit)|$(WasmTestForwardConsole)