Skip to content

Commit 8752339

Browse files
authored
[browser] WebAssembly SDK targets more incremental (#125367)
1 parent 129d627 commit 8752339

2 files changed

Lines changed: 394 additions & 11 deletions

File tree

src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets

Lines changed: 187 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,9 @@ Copyright (c) .NET Foundation. All rights reserved.
291291
</ItemGroup>
292292
</Target>
293293

294-
<Target Name="_ResolveWasmOutputs" DependsOnTargets="ResolveReferences;PrepareResourceNames;ComputeIntermediateSatelliteAssemblies;_ResolveWasmConfiguration;_WasmNativeForBuild">
294+
<!-- Resolve and classify build asset candidates.
295+
This target always runs to ensure candidate items are populated for downstream consumers. -->
296+
<Target Name="_ComputeWasmBuildCandidates" DependsOnTargets="ResolveReferences;PrepareResourceNames;ComputeIntermediateSatelliteAssemblies;_ResolveWasmConfiguration;_WasmNativeForBuild">
295297
<PropertyGroup>
296298
<_WasmNativeAssetFileNames>;@(WasmNativeAsset->'%(FileName)%(Extension)');@(WasmAssembliesFinal->'%(FileName)%(Extension)');</_WasmNativeAssetFileNames>
297299
<_WasmIntermediateAssemblyFileNames Condition="@(WasmAssembliesFinal->Count()) != 0">;@(IntermediateAssembly->'%(FileName)%(Extension)');</_WasmIntermediateAssemblyFileNames>
@@ -361,17 +363,116 @@ Copyright (c) .NET Foundation. All rights reserved.
361363
<_WasmFrameworkCopyToOutputDirectory Condition="'$(_WasmFrameworkCopyToOutputDirectory)' == ''">Never</_WasmFrameworkCopyToOutputDirectory>
362364
</PropertyGroup>
363365

364-
<ConvertDllsToWebcil Candidates="@(_BuildAssetsCandidates)" IntermediateOutputPath="$(_WasmBuildTmpWebcilPath)" OutputPath="$(_WasmBuildWebcilPath)" IsEnabled="$(_WasmEnableWebcil)" WebcilVersion="$(_WasmWebcilVersion)">
365-
<Output TaskParameter="WebcilCandidates" ItemName="_WebcilAssetsCandidates" />
366-
<Output TaskParameter="PassThroughCandidates" ItemName="_WasmFrameworkCandidates" />
366+
<!-- Identify DLL candidates that need webcil conversion, separate culture from non-culture
367+
DLLs, and compute their expected output paths for use as Outputs in the incremental
368+
_ConvertBuildDllsToWebcil target. Culture and non-culture DLLs are separated into
369+
distinct item groups to avoid MSBuild batching errors on metadata (like RelatedAsset)
370+
that only culture items define.
371+
Also pre-classify non-DLL candidates: items with WasmNativeBuildOutput metadata are
372+
already per-project (Computed); items without are Framework assets needing per-project
373+
materialization. Pre-filtering avoids MSBuild batching errors on WasmNativeBuildOutput
374+
metadata that only WasmNativeAsset items define. -->
375+
<ItemGroup Condition="'$(_WasmEnableWebcil)' == 'true'">
376+
<!-- Build DLL candidates with pre-computed WebcilOutputPath metadata.
377+
First filter to DLLs only, then split by culture to assign the correct output path.
378+
The combined list enables MSBuild partial target execution via Outputs transform
379+
(@(_WasmDllBuildCandidates->'%(WebcilOutputPath)')): only out-of-date input/output
380+
pairs are passed to the target body, skipping unchanged DLLs entirely. -->
381+
<_WasmDllBuildCandidates Include="@(_BuildAssetsCandidates)" Condition="'%(Extension)' == '.dll'" />
382+
<_WasmDllBuildCandidatesNonCulture Include="@(_WasmDllBuildCandidates)" Condition="'%(AssetTraitName)' != 'Culture'">
383+
<WebcilOutputPath>$(_WasmBuildWebcilPath)%(FileName).wasm</WebcilOutputPath>
384+
</_WasmDllBuildCandidatesNonCulture>
385+
<_WasmDllBuildCandidatesCulture Include="@(_WasmDllBuildCandidates)" Condition="'%(AssetTraitName)' == 'Culture'">
386+
<WebcilOutputPath>$(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm</WebcilOutputPath>
387+
</_WasmDllBuildCandidatesCulture>
388+
<!-- Rebuild _WasmDllBuildCandidates with WebcilOutputPath metadata from both groups -->
389+
<_WasmDllBuildCandidates Remove="@(_WasmDllBuildCandidates)" />
390+
<_WasmDllBuildCandidates Include="@(_WasmDllBuildCandidatesNonCulture);@(_WasmDllBuildCandidatesCulture)" />
391+
<_WasmExpectedWebcilOutputs Include="@(_WasmDllBuildCandidates->'%(WebcilOutputPath)')" />
392+
</ItemGroup>
393+
394+
<!-- Separate non-DLL items into native build outputs (per-project, Computed) and
395+
framework candidates (shared, need materialization). WasmNativeBuildOutput metadata
396+
is only set on WasmNativeAsset items so we filter from the source to avoid batching
397+
errors on _BuildAssetsCandidates. -->
398+
<ItemGroup>
399+
<_WasmNativeBuildOutputCandidates Include="@(_BuildAssetsCandidates)" Condition="'%(Extension)' != '.dll' and '%(_BuildAssetsCandidates.WasmNativeBuildOutput)' != ''" />
400+
<_WasmNonDllNonNativeCandidates Include="@(_BuildAssetsCandidates)" Condition="'%(Extension)' != '.dll'" />
401+
<_WasmNonDllNonNativeCandidates Remove="@(_WasmNativeBuildOutputCandidates)" />
402+
</ItemGroup>
403+
</Target>
404+
405+
<!-- Convert DLL assemblies to webcil format.
406+
Outputs is a direct transform of the item-based Inputs, enabling MSBuild partial target
407+
execution: when only some DLLs change, MSBuild passes only out-of-date pairs to the
408+
target body. Property inputs (project/targets/task assembly) are global dependencies —
409+
if any changes, all outputs are rebuilt.
410+
Touch is needed because the task uses content comparison (MoveIfDifferent) which preserves
411+
old timestamps when webcil content is unchanged. Without Touch, property-triggered full
412+
rebuilds would loop since skipped outputs retain pre-existing timestamps. -->
413+
<Target Name="_ConvertBuildDllsToWebcil"
414+
DependsOnTargets="_ComputeWasmBuildCandidates"
415+
Condition="'$(_WasmEnableWebcil)' == 'true'"
416+
Inputs="@(_WasmDllBuildCandidates);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)"
417+
Outputs="@(_WasmDllBuildCandidates->'%(WebcilOutputPath)')">
418+
419+
<ConvertDllsToWebcil Candidates="@(_WasmDllBuildCandidates)" IntermediateOutputPath="$(_WasmBuildTmpWebcilPath)" OutputPath="$(_WasmBuildWebcilPath)" IsEnabled="$(_WasmEnableWebcil)" WebcilVersion="$(_WasmWebcilVersion)">
367420
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
421+
<Output TaskParameter="FileWrites" ItemName="_WasmConvertedWebcilOutputs" />
368422
</ConvertDllsToWebcil>
369423

370-
<!-- Remove pass-throughs from webcil candidates so each file is classified exactly once:
371-
webcil-converted files → Computed (per-project in obj/webcil/)
372-
pass-through files → Framework (materialized per-project by UpdatePackageStaticWebAssets) -->
373-
<ItemGroup>
374-
<_WebcilAssetsCandidates Remove="@(_WasmFrameworkCandidates)" />
424+
<!-- Touch is needed for property-triggered full rebuilds: the task's content comparison
425+
(MoveIfDifferent) preserves old timestamps for unchanged webcil files, but MSBuild
426+
needs all outputs to have current timestamps to skip the target on subsequent builds.
427+
For item-triggered partial rebuilds, MSBuild only passes changed items so Touch only
428+
affects those outputs. -->
429+
<Touch Files="@(_WasmConvertedWebcilOutputs)" Condition="'@(_WasmConvertedWebcilOutputs)' != ''" />
430+
</Target>
431+
432+
<!-- Resolve webcil candidate items and define static web assets for the build.
433+
This target always runs because it populates item groups consumed by downstream targets.
434+
It reuses %(WebcilOutputPath) metadata computed in _ComputeWasmBuildCandidates to construct
435+
webcil candidate items, so items are correct whether the conversion target ran or was
436+
skipped due to incrementalism.
437+
Pass-through files are classified as Framework assets for per-project materialization. -->
438+
<Target Name="_ResolveWasmOutputs" DependsOnTargets="_ComputeWasmBuildCandidates;_ConvertBuildDllsToWebcil">
439+
440+
<!-- When webcil is enabled, transform DLL candidates to their webcil output paths (reusing
441+
%(WebcilOutputPath) computed in _ComputeWasmBuildCandidates) and fix metadata. Non-culture
442+
and culture DLLs use separate intermediate items to avoid MSBuild batching errors on
443+
metadata (like RelatedAsset) that only culture items define.
444+
Only webcil-converted items and WasmNativeBuildOutput items go to _WebcilAssetsCandidates
445+
(Computed SourceType). Framework candidates are classified separately. -->
446+
<ItemGroup Condition="'$(_WasmEnableWebcil)' == 'true'">
447+
<_WasmWebcilConvertedNonCulture Include="@(_WasmDllBuildCandidatesNonCulture->'%(WebcilOutputPath)')">
448+
<RelativePath>$([System.IO.Path]::ChangeExtension(%(RelativePath), '.wasm'))</RelativePath>
449+
<OriginalItemSpec>%(WebcilOutputPath)</OriginalItemSpec>
450+
</_WasmWebcilConvertedNonCulture>
451+
<_WasmWebcilConvertedCulture Include="@(_WasmDllBuildCandidatesCulture->'%(WebcilOutputPath)')">
452+
<RelativePath>$([System.IO.Path]::ChangeExtension(%(RelativePath), '.wasm'))</RelativePath>
453+
<OriginalItemSpec>%(WebcilOutputPath)</OriginalItemSpec>
454+
<RelatedAsset>$([System.IO.Path]::ChangeExtension(%(RelatedAsset), '.wasm'))</RelatedAsset>
455+
</_WasmWebcilConvertedCulture>
456+
457+
<!-- Webcil-converted items + WasmNativeBuildOutput items → Computed (per-project already) -->
458+
<_WebcilAssetsCandidates Include="@(_WasmNativeBuildOutputCandidates)" />
459+
<_WebcilAssetsCandidates Include="@(_WasmWebcilConvertedNonCulture)" />
460+
<_WebcilAssetsCandidates Include="@(_WasmWebcilConvertedCulture)" />
461+
462+
<!-- Non-DLL items without WasmNativeBuildOutput → Framework (need per-project materialization) -->
463+
<_WasmFrameworkCandidates Include="@(_WasmNonDllNonNativeCandidates)" />
464+
465+
<!-- Track webcil files for clean operations even when the conversion target was skipped -->
466+
<FileWrites Include="@(_WasmExpectedWebcilOutputs)" />
467+
</ItemGroup>
468+
469+
<!-- When webcil is disabled, DLLs retain their shared paths and also need Framework
470+
materialization (along with non-DLL items). Only WasmNativeBuildOutput items are
471+
already per-project (Computed). -->
472+
<ItemGroup Condition="'$(_WasmEnableWebcil)' != 'true'">
473+
<_WebcilAssetsCandidates Include="@(_WasmNativeBuildOutputCandidates)" />
474+
<_WasmFrameworkCandidates Include="@(_WasmNonDllNonNativeCandidates)" />
475+
<_WasmFrameworkCandidates Include="@(_BuildAssetsCandidates)" Condition="'%(Extension)' == '.dll'" />
375476
</ItemGroup>
376477

377478
<ItemGroup>
@@ -501,7 +602,9 @@ Copyright (c) .NET Foundation. All rights reserved.
501602
</ItemGroup>
502603
</Target>
503604

504-
<Target Name="_GenerateBuildWasmBootJson" DependsOnTargets="$(GenerateBuildWasmBootJsonDependsOn)">
605+
<!-- Resolve static web asset endpoints for the boot JSON generation.
606+
This target always runs to compute JS module candidates and wasm asset endpoints. -->
607+
<Target Name="_ResolveBuildWasmBootJsonEndpoints" DependsOnTargets="$(GenerateBuildWasmBootJsonDependsOn)">
505608
<PropertyGroup>
506609
<_WasmBuildBootJsonPath>$([MSBuild]::NormalizePath($(IntermediateOutputPath), $(_WasmBootConfigFileName)))</_WasmBuildBootJsonPath>
507610
<_WasmBuildApplicationEnvironmentName>$(WasmApplicationEnvironmentName)</_WasmBuildApplicationEnvironmentName>
@@ -555,6 +658,33 @@ Copyright (c) .NET Foundation. All rights reserved.
555658
>
556659
<Output TaskParameter="ResolvedEndpoints" ItemName="_WasmResolvedEndpoints" />
557660
</ResolveFingerprintedStaticWebAssetEndpointsForAssets>
661+
</Target>
662+
663+
<!-- Write a property stamp file that captures non-file parameters passed to GenerateWasmBootJson.
664+
This ensures property-only changes (e.g., WasmDebugLevel, environment name) invalidate the
665+
incremental boot JSON target. WriteOnlyWhenDifferent preserves the timestamp when unchanged. -->
666+
<Target Name="_WriteWasmBootJsonBuildPropertyStamp"
667+
DependsOnTargets="_ResolveBuildWasmBootJsonEndpoints">
668+
<PropertyGroup>
669+
<_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)</_WasmBootJsonBuildStampContent>
670+
</PropertyGroup>
671+
<WriteLinesToFile
672+
File="$(IntermediateOutputPath)wasm-bootjson-build.stamp"
673+
Lines="$(_WasmBootJsonBuildStampContent)"
674+
Overwrite="true"
675+
WriteOnlyWhenDifferent="true" />
676+
<ItemGroup>
677+
<FileWrites Include="$(IntermediateOutputPath)wasm-bootjson-build.stamp" />
678+
</ItemGroup>
679+
</Target>
680+
681+
<!-- Write the boot JSON file for the build.
682+
This target is incremental: when all inputs (assemblies, static web assets, VFS files,
683+
config files, extensions, property stamp) are older than the output, the target is skipped. -->
684+
<Target Name="_WriteBuildWasmBootJsonFile"
685+
DependsOnTargets="_WriteWasmBootJsonBuildPropertyStamp"
686+
Inputs="@(IntermediateAssembly);@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset);@(_WasmJsConfigStaticWebAsset);@(_WasmDotnetJsForBuild);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly);$(IntermediateOutputPath)wasm-bootjson-build.stamp"
687+
Outputs="$(_WasmBuildBootJsonPath)">
558688

559689
<GenerateWasmBootJson
560690
AssemblyPath="@(IntermediateAssembly)"
@@ -596,7 +726,19 @@ Copyright (c) .NET Foundation. All rights reserved.
596726
<FileWrites Include="$(_WasmBuildBootJsonPath)" />
597727
</ItemGroup>
598728

729+
<!-- The GenerateWasmBootJson task uses content comparison and preserves old timestamps when
730+
the output content is unchanged. Touch the output so MSBuild's Inputs/Outputs check
731+
sees a current timestamp and can correctly skip this target on subsequent builds. -->
732+
<Touch Files="$(_WasmBuildBootJsonPath)" />
733+
</Target>
734+
735+
<!-- Define the boot config file as a static web asset and create endpoints.
736+
This target always runs to ensure items are populated even when _WriteBuildWasmBootJsonFile is skipped. -->
737+
<Target Name="_GenerateBuildWasmBootJson" DependsOnTargets="_WriteBuildWasmBootJsonFile">
738+
599739
<ItemGroup>
740+
<!-- Track boot JSON for clean operations even when _WriteBuildWasmBootJsonFile was skipped -->
741+
<FileWrites Include="$(_WasmBuildBootJsonPath)" />
600742
<_WasmBuildBootConfigCandidate
601743
Include="$(_WasmBuildBootJsonPath)"
602744
RelativePath="_framework/$(_WasmBootConfigFileName)" />
@@ -914,6 +1056,8 @@ Copyright (c) .NET Foundation. All rights reserved.
9141056
<Target Name="_AddPublishWasmBootJsonToStaticWebAssets" DependsOnTargets="GeneratePublishWasmBootJson">
9151057

9161058
<ItemGroup>
1059+
<!-- Track boot JSON for clean operations even when GeneratePublishWasmBootJson was skipped -->
1060+
<FileWrites Include="$(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)" />
9171061
<_WasmPublishBootConfigCandidate
9181062
Include="$([MSBuild]::NormalizePath($(IntermediateOutputPath), $(_WasmPublishBootConfigFileName)))"
9191063
RelativePath="_framework/$(_WasmBootConfigFileName)" />
@@ -951,7 +1095,9 @@ Copyright (c) .NET Foundation. All rights reserved.
9511095

9521096
</Target>
9531097

954-
<Target Name="GeneratePublishWasmBootJson" DependsOnTargets="$(GeneratePublishWasmBootJsonDependsOn)">
1098+
<!-- Resolve inputs for the publish boot JSON target.
1099+
This target always runs to compute publish asset items and endpoints. -->
1100+
<Target Name="_ResolvePublishWasmBootJsonInputs" DependsOnTargets="$(GeneratePublishWasmBootJsonDependsOn)">
9551101
<PropertyGroup>
9561102
<_WasmPublishApplicationEnvironmentName>$(WasmApplicationEnvironmentName)</_WasmPublishApplicationEnvironmentName>
9571103
</PropertyGroup>
@@ -984,6 +1130,31 @@ Copyright (c) .NET Foundation. All rights reserved.
9841130
>
9851131
<Output TaskParameter="ResolvedEndpoints" ItemName="_WasmResolvedEndpointsForPublish" />
9861132
</ResolveFingerprintedStaticWebAssetEndpointsForAssets>
1133+
</Target>
1134+
1135+
<!-- Write a property stamp for publish boot JSON incrementalism. Same approach as build stamp. -->
1136+
<Target Name="_WriteWasmPublishBootJsonPropertyStamp"
1137+
DependsOnTargets="_ResolvePublishWasmBootJsonInputs">
1138+
<PropertyGroup>
1139+
<_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)</_WasmBootJsonPublishStampContent>
1140+
</PropertyGroup>
1141+
<WriteLinesToFile
1142+
File="$(IntermediateOutputPath)wasm-bootjson-publish.stamp"
1143+
Lines="$(_WasmBootJsonPublishStampContent)"
1144+
Overwrite="true"
1145+
WriteOnlyWhenDifferent="true" />
1146+
<ItemGroup>
1147+
<FileWrites Include="$(IntermediateOutputPath)wasm-bootjson-publish.stamp" />
1148+
</ItemGroup>
1149+
</Target>
1150+
1151+
<!-- Write the publish boot JSON file.
1152+
This target is incremental: when all inputs (assemblies, publish assets, config files,
1153+
extensions, property stamp) are older than the output, the target is skipped. -->
1154+
<Target Name="GeneratePublishWasmBootJson"
1155+
DependsOnTargets="_WriteWasmPublishBootJsonPropertyStamp"
1156+
Inputs="@(IntermediateAssembly);@(_WasmPublishAsset);@(_WasmJsModuleCandidatesForPublish);@(_WasmPublishConfigFile);@(_WasmDotnetJsForPublish);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly);$(IntermediateOutputPath)wasm-bootjson-publish.stamp"
1157+
Outputs="$(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)">
9871158

9881159
<GenerateWasmBootJson
9891160
AssemblyPath="@(IntermediateAssembly)"
@@ -1025,6 +1196,11 @@ Copyright (c) .NET Foundation. All rights reserved.
10251196
<FileWrites Include="$([MSBuild]::NormalizePath($(IntermediateOutputPath), $(_WasmPublishBootConfigFileName)))" />
10261197
</ItemGroup>
10271198

1199+
<!-- The GenerateWasmBootJson task uses content comparison and preserves old timestamps when
1200+
the output content is unchanged. Touch the output so MSBuild's Inputs/Outputs check
1201+
sees a current timestamp and can correctly skip this target on subsequent builds. -->
1202+
<Touch Files="$(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)" />
1203+
10281204
</Target>
10291205

10301206
<Target Name="_WasmNative"

0 commit comments

Comments
 (0)