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