@@ -289,7 +289,9 @@ Copyright (c) .NET Foundation. All rights reserved.
289289 </ItemGroup >
290290 </Target >
291291
292- <Target Name =" _ResolveWasmOutputs" DependsOnTargets =" ResolveReferences;PrepareResourceNames;ComputeIntermediateSatelliteAssemblies;_ResolveWasmConfiguration;_WasmNativeForBuild" >
292+ <!-- Resolve and classify build asset candidates.
293+ This target always runs to ensure candidate items are populated for downstream consumers. -->
294+ <Target Name =" _ComputeWasmBuildCandidates" DependsOnTargets =" ResolveReferences;PrepareResourceNames;ComputeIntermediateSatelliteAssemblies;_ResolveWasmConfiguration;_WasmNativeForBuild" >
293295 <PropertyGroup >
294296 <_WasmNativeAssetFileNames >;@(WasmNativeAsset->'%(FileName)%(Extension)');@(WasmAssembliesFinal->'%(FileName)%(Extension)');</_WasmNativeAssetFileNames >
295297 <_WasmIntermediateAssemblyFileNames Condition =" @(WasmAssembliesFinal->Count()) != 0" >;@(IntermediateAssembly->'%(FileName)%(Extension)');</_WasmIntermediateAssemblyFileNames >
@@ -353,17 +355,99 @@ Copyright (c) .NET Foundation. All rights reserved.
353355 <_WasmBuildTmpWebcilPath >$([MSBuild]::NormalizeDirectory($(IntermediateOutputPath), 'tmp-webcil'))</_WasmBuildTmpWebcilPath >
354356 </PropertyGroup >
355357
356- <ConvertDllsToWebcil Candidates =" @(_BuildAssetsCandidates)" IntermediateOutputPath =" $(_WasmBuildTmpWebcilPath)" OutputPath =" $(_WasmBuildWebcilPath)" IsEnabled =" $(_WasmEnableWebcil)" >
357- <Output TaskParameter =" WebcilCandidates" ItemName =" _WebcilAssetsCandidates" />
358- <Output TaskParameter =" PassThroughCandidates" ItemName =" _WasmFrameworkCandidates" />
358+ <!-- Identify DLL candidates that need webcil conversion, separate culture from non-culture
359+ DLLs, and compute their expected output paths for use as Outputs in the incremental
360+ _ConvertBuildDllsToWebcil target. Culture and non-culture DLLs are separated into
361+ distinct item groups to avoid MSBuild batching errors on metadata (like RelatedAsset)
362+ that only culture items define.
363+ Also pre-classify non-DLL candidates: items with WasmNativeBuildOutput metadata are
364+ already per-project (Computed); items without are Framework assets needing per-project
365+ materialization. Pre-filtering avoids MSBuild batching errors on WasmNativeBuildOutput
366+ metadata that only WasmNativeAsset items define. -->
367+ <ItemGroup Condition =" '$(_WasmEnableWebcil)' == 'true'" >
368+ <_WasmDllBuildCandidates Include =" @(_BuildAssetsCandidates)" Condition =" '%(Extension)' == '.dll'" />
369+ <_WasmDllBuildCandidatesNonCulture Include =" @(_WasmDllBuildCandidates)" Condition =" '%(AssetTraitName)' != 'Culture'" />
370+ <_WasmDllBuildCandidatesCulture Include =" @(_WasmDllBuildCandidates)" Condition =" '%(AssetTraitName)' == 'Culture'" />
371+ <_WasmExpectedWebcilOutputs Include =" @(_WasmDllBuildCandidatesNonCulture->'$(_WasmBuildWebcilPath)%(FileName).wasm')" />
372+ <_WasmExpectedWebcilOutputs Include =" @(_WasmDllBuildCandidatesCulture->'$(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm')" />
373+ </ItemGroup >
374+
375+ <!-- Separate non-DLL items into native build outputs (per-project, Computed) and
376+ framework candidates (shared, need materialization). WasmNativeBuildOutput metadata
377+ is only set on WasmNativeAsset items so we filter from the source to avoid batching
378+ errors on _BuildAssetsCandidates. -->
379+ <ItemGroup >
380+ <_WasmNativeBuildOutputCandidates Include =" @(_BuildAssetsCandidates)" Condition =" '%(Extension)' != '.dll' and '%(_BuildAssetsCandidates.WasmNativeBuildOutput)' != ''" />
381+ <_WasmNonDllNonNativeCandidates Include =" @(_BuildAssetsCandidates)" Condition =" '%(Extension)' != '.dll'" />
382+ <_WasmNonDllNonNativeCandidates Remove =" @(_WasmNativeBuildOutputCandidates)" />
383+ </ItemGroup >
384+ </Target >
385+
386+ <!-- Convert DLL assemblies to webcil format.
387+ This target is incremental: when all input DLLs are older than their corresponding
388+ webcil outputs, the entire target is skipped, saving the conversion task overhead.
389+ The task also has internal per-file timestamp checks as a secondary optimization. -->
390+ <Target Name =" _ConvertBuildDllsToWebcil"
391+ DependsOnTargets =" _ComputeWasmBuildCandidates"
392+ Condition =" '$(_WasmEnableWebcil)' == 'true'"
393+ Inputs =" @(_WasmDllBuildCandidates);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)"
394+ Outputs =" @(_WasmExpectedWebcilOutputs)" >
395+
396+ <ConvertDllsToWebcil Candidates =" @(_WasmDllBuildCandidates)" IntermediateOutputPath =" $(_WasmBuildTmpWebcilPath)" OutputPath =" $(_WasmBuildWebcilPath)" IsEnabled =" $(_WasmEnableWebcil)" >
359397 <Output TaskParameter =" FileWrites" ItemName =" FileWrites" />
398+ <Output TaskParameter =" FileWrites" ItemName =" _WasmConvertedWebcilOutputs" />
360399 </ConvertDllsToWebcil >
361400
362- <!-- Remove pass-throughs from webcil candidates so each file is classified exactly once:
363- webcil-converted files → Computed (per-project in obj/webcil/)
364- pass-through files → Framework (materialized per-project by UpdatePackageStaticWebAssets) -->
365- <ItemGroup >
366- <_WebcilAssetsCandidates Remove =" @(_WasmFrameworkCandidates)" />
401+ <!-- The ConvertDllsToWebcil task uses content comparison and preserves old timestamps when
402+ the output content is unchanged. Touch the files that were actually written so
403+ MSBuild's Inputs/Outputs check sees current timestamps and can correctly skip this
404+ target on subsequent builds. -->
405+ <Touch Files =" @(_WasmConvertedWebcilOutputs)" Condition =" '@(_WasmConvertedWebcilOutputs)' != ''" />
406+ </Target >
407+
408+ <!-- Resolve webcil candidate items and define static web assets for the build.
409+ This target always runs because it populates item groups consumed by downstream targets.
410+ It reconstructs the webcil candidate items from _BuildAssetsCandidates using the same
411+ path logic as the ConvertDllsToWebcil task, so items are correct whether the conversion
412+ target ran or was skipped due to incrementalism.
413+ Pass-through files are classified as Framework assets for per-project materialization. -->
414+ <Target Name =" _ResolveWasmOutputs" DependsOnTargets =" _ComputeWasmBuildCandidates;_ConvertBuildDllsToWebcil" >
415+
416+ <!-- When webcil is enabled, transform DLL candidates to their webcil output paths and fix
417+ metadata. Non-culture and culture DLLs use separate intermediate items to avoid MSBuild
418+ batching errors on metadata (like RelatedAsset) that only culture items define.
419+ Only webcil-converted items and WasmNativeBuildOutput items go to _WebcilAssetsCandidates
420+ (Computed SourceType). Framework candidates are classified separately. -->
421+ <ItemGroup Condition =" '$(_WasmEnableWebcil)' == 'true'" >
422+ <_WasmWebcilConvertedNonCulture Include =" @(_WasmDllBuildCandidatesNonCulture->'$(_WasmBuildWebcilPath)%(FileName).wasm')" >
423+ <RelativePath >$([System.IO.Path]::ChangeExtension(%(RelativePath), '.wasm'))</RelativePath >
424+ <OriginalItemSpec >$(_WasmBuildWebcilPath)%(FileName).wasm</OriginalItemSpec >
425+ </_WasmWebcilConvertedNonCulture >
426+ <_WasmWebcilConvertedCulture Include =" @(_WasmDllBuildCandidatesCulture->'$(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm')" >
427+ <RelativePath >$([System.IO.Path]::ChangeExtension(%(RelativePath), '.wasm'))</RelativePath >
428+ <OriginalItemSpec >$(_WasmBuildWebcilPath)%(AssetTraitValue)/%(FileName).wasm</OriginalItemSpec >
429+ <RelatedAsset >$([System.IO.Path]::ChangeExtension(%(RelatedAsset), '.wasm'))</RelatedAsset >
430+ </_WasmWebcilConvertedCulture >
431+
432+ <!-- Webcil-converted items + WasmNativeBuildOutput items → Computed (per-project already) -->
433+ <_WebcilAssetsCandidates Include =" @(_WasmNativeBuildOutputCandidates)" />
434+ <_WebcilAssetsCandidates Include =" @(_WasmWebcilConvertedNonCulture)" />
435+ <_WebcilAssetsCandidates Include =" @(_WasmWebcilConvertedCulture)" />
436+
437+ <!-- Non-DLL items without WasmNativeBuildOutput → Framework (need per-project materialization) -->
438+ <_WasmFrameworkCandidates Include =" @(_WasmNonDllNonNativeCandidates)" />
439+
440+ <!-- Track webcil files for clean operations even when the conversion target was skipped -->
441+ <FileWrites Include =" @(_WasmExpectedWebcilOutputs)" />
442+ </ItemGroup >
443+
444+ <!-- When webcil is disabled, DLLs retain their shared paths and also need Framework
445+ materialization (along with non-DLL items). Only WasmNativeBuildOutput items are
446+ already per-project (Computed). -->
447+ <ItemGroup Condition =" '$(_WasmEnableWebcil)' != 'true'" >
448+ <_WebcilAssetsCandidates Include =" @(_WasmNativeBuildOutputCandidates)" />
449+ <_WasmFrameworkCandidates Include =" @(_WasmNonDllNonNativeCandidates)" />
450+ <_WasmFrameworkCandidates Include =" @(_BuildAssetsCandidates)" Condition =" '%(Extension)' == '.dll'" />
367451 </ItemGroup >
368452
369453 <ItemGroup >
@@ -480,7 +564,9 @@ Copyright (c) .NET Foundation. All rights reserved.
480564 </ItemGroup >
481565 </Target >
482566
483- <Target Name =" _GenerateBuildWasmBootJson" DependsOnTargets =" $(GenerateBuildWasmBootJsonDependsOn)" >
567+ <!-- Resolve static web asset endpoints for the boot JSON generation.
568+ This target always runs to compute JS module candidates and wasm asset endpoints. -->
569+ <Target Name =" _ResolveBuildWasmBootJsonEndpoints" DependsOnTargets =" $(GenerateBuildWasmBootJsonDependsOn)" >
484570 <PropertyGroup >
485571 <_WasmBuildBootJsonPath >$(IntermediateOutputPath)$(_WasmBootConfigFileName)</_WasmBuildBootJsonPath >
486572 <_WasmBuildApplicationEnvironmentName >$(WasmApplicationEnvironmentName)</_WasmBuildApplicationEnvironmentName >
@@ -534,6 +620,15 @@ Copyright (c) .NET Foundation. All rights reserved.
534620 >
535621 <Output TaskParameter =" ResolvedEndpoints" ItemName =" _WasmResolvedEndpoints" />
536622 </ResolveFingerprintedStaticWebAssetEndpointsForAssets >
623+ </Target >
624+
625+ <!-- Write the boot JSON file for the build.
626+ This target is incremental: when all inputs (assemblies, static web assets, config files,
627+ extensions) are older than the output boot JSON file, the entire target is skipped. -->
628+ <Target Name =" _WriteBuildWasmBootJsonFile"
629+ DependsOnTargets =" _ResolveBuildWasmBootJsonEndpoints"
630+ Inputs =" @(IntermediateAssembly);@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)"
631+ Outputs =" $(_WasmBuildBootJsonPath)" >
537632
538633 <GenerateWasmBootJson
539634 AssemblyPath =" @(IntermediateAssembly)"
@@ -575,7 +670,19 @@ Copyright (c) .NET Foundation. All rights reserved.
575670 <FileWrites Include =" $(_WasmBuildBootJsonPath)" />
576671 </ItemGroup >
577672
673+ <!-- The GenerateWasmBootJson task uses content comparison and preserves old timestamps when
674+ the output content is unchanged. Touch the output so MSBuild's Inputs/Outputs check
675+ sees a current timestamp and can correctly skip this target on subsequent builds. -->
676+ <Touch Files =" $(_WasmBuildBootJsonPath)" />
677+ </Target >
678+
679+ <!-- Define the boot config file as a static web asset and create endpoints.
680+ This target always runs to ensure items are populated even when _WriteBuildWasmBootJsonFile is skipped. -->
681+ <Target Name =" _GenerateBuildWasmBootJson" DependsOnTargets =" _WriteBuildWasmBootJsonFile" >
682+
578683 <ItemGroup >
684+ <!-- Track boot JSON for clean operations even when _WriteBuildWasmBootJsonFile was skipped -->
685+ <FileWrites Include =" $(_WasmBuildBootJsonPath)" />
579686 <_WasmBuildBootConfigCandidate
580687 Include =" $(_WasmBuildBootJsonPath)"
581688 RelativePath =" _framework/$(_WasmBootConfigFileName)" />
@@ -887,6 +994,8 @@ Copyright (c) .NET Foundation. All rights reserved.
887994 <Target Name =" _AddPublishWasmBootJsonToStaticWebAssets" DependsOnTargets =" GeneratePublishWasmBootJson" >
888995
889996 <ItemGroup >
997+ <!-- Track boot JSON for clean operations even when GeneratePublishWasmBootJson was skipped -->
998+ <FileWrites Include =" $(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)" />
890999 <_WasmPublishBootConfigCandidate
8911000 Include =" $(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)"
8921001 RelativePath =" _framework/$(_WasmBootConfigFileName)" />
@@ -924,7 +1033,9 @@ Copyright (c) .NET Foundation. All rights reserved.
9241033
9251034 </Target >
9261035
927- <Target Name =" GeneratePublishWasmBootJson" DependsOnTargets =" $(GeneratePublishWasmBootJsonDependsOn)" >
1036+ <!-- Resolve inputs for the publish boot JSON target.
1037+ This target always runs to compute publish asset items and endpoints. -->
1038+ <Target Name =" _ResolvePublishWasmBootJsonInputs" DependsOnTargets =" $(GeneratePublishWasmBootJsonDependsOn)" >
9281039 <PropertyGroup >
9291040 <_WasmPublishApplicationEnvironmentName >$(WasmApplicationEnvironmentName)</_WasmPublishApplicationEnvironmentName >
9301041 </PropertyGroup >
@@ -957,6 +1068,15 @@ Copyright (c) .NET Foundation. All rights reserved.
9571068 >
9581069 <Output TaskParameter =" ResolvedEndpoints" ItemName =" _WasmResolvedEndpointsForPublish" />
9591070 </ResolveFingerprintedStaticWebAssetEndpointsForAssets >
1071+ </Target >
1072+
1073+ <!-- Write the publish boot JSON file.
1074+ This target is incremental: when all inputs (assemblies, publish assets, config files,
1075+ extensions) are older than the output, the target is skipped. -->
1076+ <Target Name =" GeneratePublishWasmBootJson"
1077+ DependsOnTargets =" _ResolvePublishWasmBootJsonInputs"
1078+ Inputs =" @(IntermediateAssembly);@(_WasmPublishAsset);@(_WasmJsModuleCandidatesForPublish);@(_WasmPublishConfigFile);@(_WasmDotnetJsForPublish);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)"
1079+ Outputs =" $(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)" >
9601080
9611081 <GenerateWasmBootJson
9621082 AssemblyPath =" @(IntermediateAssembly)"
@@ -998,6 +1118,11 @@ Copyright (c) .NET Foundation. All rights reserved.
9981118 <FileWrites Include =" $(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)" />
9991119 </ItemGroup >
10001120
1121+ <!-- The GenerateWasmBootJson task uses content comparison and preserves old timestamps when
1122+ the output content is unchanged. Touch the output so MSBuild's Inputs/Outputs check
1123+ sees a current timestamp and can correctly skip this target on subsequent builds. -->
1124+ <Touch Files =" $(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)" />
1125+
10011126 </Target >
10021127
10031128 <Target Name =" _WasmNative"
0 commit comments