Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,6 @@
<PropertyGroup>
<_WasmBuildWebCilPath>$([MSBuild]::NormalizeDirectory($(IntermediateOutputPath), 'webcil'))</_WasmBuildWebCilPath>
<_WasmBuildTmpWebCilPath>$([MSBuild]::NormalizeDirectory($(IntermediateOutputPath), 'tmp-webcil'))</_WasmBuildTmpWebCilPath>
<_WasmBuildOuputPath>$([MSBuild]::NormalizeDirectory('$(OutputPath)', 'wwwroot'))</_WasmBuildOuputPath>
</PropertyGroup>

<ConvertDllsToWebCil Candidates="@(_BuildAssetsCandidates)" IntermediateOutputPath="$(_WasmBuildTmpWebCilPath)" OutputPath="$(_WasmBuildWebCilPath)" IsEnabled="$(_WasmEnableWebcil)">
Expand All @@ -358,6 +357,8 @@
</ConvertDllsToWebCil>

<ItemGroup>
<!-- Set per-item ContentRoot so each asset's Identity matches its actual file on disk -->
<_WebCilAssetsCandidates Update="@(_WebCilAssetsCandidates)" ContentRoot="%(RootDir)%(Directory)" />
<_WasmFingerprintPatterns Include="WasmFiles" Pattern="*.wasm" Expression="#[.{fingerprint}]!" />
<_WasmFingerprintPatterns Include="DllFiles" Pattern="*.dll" Expression="#[.{fingerprint}]!" />
<_WasmFingerprintPatterns Include="DatFiles" Pattern="*.dat" Expression="#[.{fingerprint}]!" />
Expand All @@ -372,7 +373,6 @@
AssetRole="Primary"
CopyToOutputDirectory="PreserveNewest"
CopyToPublishDirectory="Never"
ContentRoot="$(_WasmBuildOuputPath)"
FingerprintCandidates="$(_WasmFingerprintAssets)"
FingerprintPatterns="@(FingerprintPatterns);@(_WasmFingerprintPatterns)"
BasePath="$(StaticWebAssetBasePath)"
Expand Down Expand Up @@ -407,6 +407,10 @@

<Target Name="_AddWasmStaticWebAssets" DependsOnTargets="$(AddWasmStaticWebAssetsDependsOn)">
<ItemGroup>
<!-- Remove SDK-defined assets that we're redefining as WasmStaticWebAsset to avoid
duplicate Identity collisions and duplicate endpoint entries. This follows the
same pattern used above for _WasmJsConfigStaticWebAsset. -->
<StaticWebAsset Remove="@(WasmStaticWebAsset)" />
<StaticWebAsset Include="@(WasmStaticWebAsset)" />
<StaticWebAsset Include="@(_WasmBuildBootConfigStaticWebAsset)" />
<StaticWebAssetEndpoint Include="@(WasmStaticWebAssetEndpoint)" />
Expand Down Expand Up @@ -441,17 +445,35 @@
<Output TaskParameter="FilteredEndpoints" ItemName="_WasmJsModuleCandidatesForBuildEndpoint" />
</FilterStaticWebAssetEndpoints>

<!-- Filter out SDK endpoints for assets we're redefining as WasmStaticWebAsset.
Per-item ContentRoot can make WasmStaticWebAsset Identity equal the original
DLL path, which the SDK already has endpoints for. We must remove these before
DefineStaticWebAssetEndpoints so it creates fresh _framework/ prefixed endpoints,
and before the resolver so it doesn't produce duplicate AssetFile keys. -->
<FilterStaticWebAssetEndpoints
Endpoints="@(StaticWebAssetEndpoint)"
Assets="@(WasmStaticWebAsset)"
Filters=""
>
<Output TaskParameter="FilteredEndpoints" ItemName="_WasmOverlappingSdkEndpoint" />
</FilterStaticWebAssetEndpoints>

<ItemGroup>
<_WasmNonOverlappingSdkEndpoint Include="@(StaticWebAssetEndpoint)" />
<_WasmNonOverlappingSdkEndpoint Remove="@(_WasmOverlappingSdkEndpoint)" />
</ItemGroup>

<DefineStaticWebAssetEndpoints
CandidateAssets="@(WasmStaticWebAsset)"
ExistingEndpoints="@(StaticWebAssetEndpoint)"
ExistingEndpoints="@(_WasmNonOverlappingSdkEndpoint)"
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
>
<Output TaskParameter="Endpoints" ItemName="WasmStaticWebAssetEndpoint" />
</DefineStaticWebAssetEndpoints>

<ResolveFingerprintedStaticWebAssetEndpointsForAssets
CandidateEndpoints="@(WasmStaticWebAssetEndpoint);@(_WasmJsModuleCandidatesForBuildEndpoint);@(StaticWebAssetEndpoint)"
CandidateEndpoints="@(WasmStaticWebAssetEndpoint);@(_WasmJsModuleCandidatesForBuildEndpoint);@(_WasmNonOverlappingSdkEndpoint)"
CandidateAssets="@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset)"

Check failure on line 476 in src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets

View check run for this annotation

Azure Pipelines / runtime (Build browser-wasm linux Release LibraryTestsCoreCLR)

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

src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets(476,5): error : (NETCORE_ENGINEERING_TELEMETRY=Build) No endpoint found for asset '/__w/1/s/artifacts/bin/System.Reflection.TestModule/Release/netstandard2.0/System.Reflection.TestModule.dll' with path 'System.Reflection.TestModule.dll' whose route matches its path.

Check failure on line 476 in src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets

View check run for this annotation

Azure Pipelines / runtime

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

src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets(476,5): error : (NETCORE_ENGINEERING_TELEMETRY=Build) No endpoint found for asset '/__w/1/s/artifacts/bin/System.Reflection.TestModule/Release/netstandard2.0/System.Reflection.TestModule.dll' with path 'System.Reflection.TestModule.dll' whose route matches its path.
IsStandalone="$(StaticWebAssetStandaloneHosting)"
>
<Output TaskParameter="ResolvedEndpoints" ItemName="_WasmResolvedEndpoints" />
Expand Down Expand Up @@ -518,7 +540,7 @@
AssetTraitValue="manifest"
CopyToOutputDirectory="PreserveNewest"
CopyToPublishDirectory="Never"
ContentRoot="$(OutDir)wwwroot"
ContentRoot="$(IntermediateOutputPath)"
Copy link
Member

Choose a reason for hiding this comment

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

I believe this needs to happen in other places like the definition of the .wasm files as assets (after webcil conversion).

I believe the important bit here is that ContentRoot + RelativePath must exist for all assets

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. Added a commit that sets per-item ContentRoot metadata on each _WebCilAssetsCandidates item to %(RootDir)%(Directory) (its own directory). This ensures candidateFullPath.StartsWith(normalizedContentRoot) is always true in DefineStaticWebAssets.ComputeCandidateIdentity, so Identity equals the physical file path for all candidates — webcil-converted files in obj/webcil/, runtime pack files in packs/, PDBs from obj/, etc.

The task-level ContentRoot parameter is kept as a fallback but the per-item metadata takes precedence via ComputePropertyValue.

BasePath="$(StaticWebAssetBasePath)"
>
<Output TaskParameter="Assets" ItemName="_WasmBuildBootConfigStaticWebAsset" />
Expand Down Expand Up @@ -584,18 +606,10 @@
</Target>

<Target Name="_AddWasmPreloadBuildProperties" DependsOnTargets="_WasmConfigurePreload;_AddWasmStaticWebAssets" BeforeTargets="GenerateStaticWebAssetsManifest" Condition="'$(_WasmPreloadAssets)' == 'true'">
<PropertyGroup>
<_WasmBootConfigFileNameWithoutExtension>$([System.IO.Path]::GetFileNameWithoutExtension('$(_WasmBootConfigFileName)'))</_WasmBootConfigFileNameWithoutExtension>
<_WasmBootConfigFileExtension>$([System.IO.Path]::GetExtension('$(_WasmBootConfigFileName)'))</_WasmBootConfigFileExtension>
</PropertyGroup>
<ItemGroup>
<_WasmPreloadBuildScriptAsset Include="@(StaticWebAsset)" Condition="'$(_WasmFingerprintAssets)' == 'true' and '$(_WasmFingerprintBootConfig)' == 'true' and '%(AssetKind)' != 'Publish' and '%(FileName)%(Extension)' == '$(_WasmBootConfigFileNameWithoutExtension).%(Fingerprint)$(_WasmBootConfigFileExtension)'" />
<_WasmPreloadBuildScriptAsset Include="@(StaticWebAsset)" Condition="('$(_WasmFingerprintAssets)' != 'true' or '$(_WasmFingerprintBootConfig)' != 'true') and '%(AssetKind)' != 'Publish' and '%(FileName)%(Extension)' == '$(_WasmBootConfigFileName)'" />
</ItemGroup>

<FilterStaticWebAssetEndpoints
Endpoints="@(StaticWebAssetEndpoint)"
Assets="@(_WasmPreloadBuildScriptAsset)"
Assets="@(_WasmBuildBootConfigStaticWebAsset)"
Filters="@(_WasmPreloadEndpointFilter)"
>
<Output TaskParameter="FilteredEndpoints" ItemName="_WasmPreloadBuildScriptAssetEndpoint" />
Expand All @@ -614,13 +628,10 @@
</ItemGroup>
</Target>
<Target Name="_AddWasmPreloadPublishProperties" DependsOnTargets="_WasmConfigurePreload;_AddPublishWasmBootJsonToStaticWebAssets" BeforeTargets="GenerateStaticWebAssetsPublishManifest" Condition="'$(_WasmPreloadAssets)' == 'true'">
<ItemGroup>
<_WasmPreloadPublishScriptAsset Include="@(StaticWebAsset)" Condition="'%(AssetKind)' != 'Build' and '%(FileName)%(Extension)' == '$(_WasmPublishBootConfigFileName)'" />
</ItemGroup>

<FilterStaticWebAssetEndpoints
Endpoints="@(StaticWebAssetEndpoint)"
Assets="@(_WasmPreloadPublishScriptAsset)"
Assets="@(_WasmPublishBootConfigStaticWebAsset)"
Filters="@(_WasmPreloadEndpointFilter)"
>
<Output TaskParameter="FilteredEndpoints" ItemName="_WasmPreloadPublishScriptAssetEndpoint" />
Expand Down Expand Up @@ -729,6 +740,18 @@
<_NewWebCilPublishStaticWebAssetsCandidatesNoMetadata
Include="@(_NewWebCilPublishStaticWebAssetsCandidates)"
RemoveMetadata="Integrity;Fingerprint" />
<!-- Set per-item ContentRoot so each asset's Identity matches its actual file on disk.
Clear stale Fingerprint/Integrity so DefineStaticWebAssets recomputes from the actual file. -->
<_NewWebCilPublishStaticWebAssetsCandidatesNoMetadata Update="@(_NewWebCilPublishStaticWebAssetsCandidatesNoMetadata)" ContentRoot="%(RootDir)%(Directory)" />
<_PromotedWasmPublishStaticWebAssets Update="@(_PromotedWasmPublishStaticWebAssets)" ContentRoot="%(RootDir)%(Directory)" Fingerprint="" Integrity="" />
<!-- Satellite (Culture) promoted assets are Related assets whose RelatedAsset points to a build-time path
that doesn't match any publish-time candidate. Promote them to Primary so DefineStaticWebAssets
processes them as standalone assets instead of trying to match a parent. -->
<_PromotedWasmPublishStaticWebAssets
Update="@(_PromotedWasmPublishStaticWebAssets)"
Condition="'%(_PromotedWasmPublishStaticWebAssets.AssetTraitName)' == 'Culture'"
AssetRole="Primary"
RelatedAsset="" />
</ItemGroup>

<DefineStaticWebAssets CandidateAssets="@(_NewWebCilPublishStaticWebAssetsCandidatesNoMetadata);@(_PromotedWasmPublishStaticWebAssets)">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,23 +332,11 @@ static bool IsDotNetWasm(string key)

private TaskItem CreatePromotedAsset(ITaskItem asset)
{
string newAssetItemSpec = asset.ItemSpec;
string newAssetRelativePath = asset.GetMetadata("RelativePath");

if (FingerprintAssets)
{
string assetDirectory = Path.GetDirectoryName(asset.ItemSpec);
string assetFileNameToFingerprint = Path.GetFileName(newAssetRelativePath);
string fingerprint = asset.GetMetadata("Fingerprint");
string newAssetFingerprintedFileName = assetFileNameToFingerprint.Replace("#[.{fingerprint}]!", $".{fingerprint}");
if (newAssetFingerprintedFileName != assetFileNameToFingerprint)
{
newAssetItemSpec = $"{assetDirectory}/{newAssetFingerprintedFileName}";
}
}

var newAsset = new TaskItem(newAssetItemSpec, asset.CloneCustomMetadata());
newAsset.SetMetadata("RelativePath", newAssetRelativePath);
// Keep ItemSpec pointing to the actual file on disk.
// DefineStaticWebAssets will resolve fingerprint placeholders in RelativePath
// and compute Fingerprint/Integrity from the real file.
var newAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata());
newAsset.SetMetadata("RelativePath", asset.GetMetadata("RelativePath"));

ApplyPublishProperties(newAsset);
return newAsset;
Expand Down Expand Up @@ -446,6 +434,10 @@ private void ComputeUpdatedAssemblies(
// when the original assembly they depend on has been linked out.
var assetsToUpdate = new Dictionary<string, ITaskItem>();
var linkedAssets = new Dictionary<string, ITaskItem>();
// Secondary lookup by normalized filename for satellite matching.
// RelatedAsset may use a different base path (e.g., OutputPath/wwwroot)
// than the asset's build-time Identity (e.g., IntermediateOutputPath/webcil).
var assetsToUpdateByFileName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

foreach (var kvp in assemblyAssets)
{
Expand All @@ -459,6 +451,8 @@ private void ComputeUpdatedAssemblies(
{
// We found the assembly, so it'll have to be updated.
assetsToUpdate.Add(assetToUpdateItemSpec, asset);
if (!assetsToUpdateByFileName.ContainsKey(fileName))
assetsToUpdateByFileName[fileName] = assetToUpdateItemSpec;
filesToRemove.Add(existing);
if (!string.Equals(asset.ItemSpec, existing.GetMetadata("FullPath"), StringComparison.Ordinal))
{
Expand All @@ -477,6 +471,18 @@ private void ComputeUpdatedAssemblies(
var satelliteAssembly = kvp.Value;
var relatedAsset = satelliteAssembly.GetMetadata("RelatedAsset");

// Try exact match first, then fall back to filename-based lookup.
// Normalize to .dll when webcil is enabled since assetsToUpdateByFileName
// keys are normalized to .dll (line 448) but RelatedAsset paths use .wasm.
Comment on lines +475 to +476
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The comment references a specific source line number ("line 448"), which will become inaccurate as the file changes and makes the explanation harder to maintain. Consider rewriting this to refer to the normalization step without hard-coded line numbers (e.g., "above" or describing the condition).

Suggested change
// Normalize to .dll when webcil is enabled since assetsToUpdateByFileName
// keys are normalized to .dll (line 448) but RelatedAsset paths use .wasm.
// Normalize to .dll when webcil is enabled since assemblies tracked in
// assetsToUpdateByFileName use .dll extensions, while RelatedAsset paths may use .wasm.

Copilot uses AI. Check for mistakes.
var relatedAssetFileName = Path.GetFileName(relatedAsset);
if (IsWebCilEnabled)
relatedAssetFileName = Path.ChangeExtension(relatedAssetFileName, ".dll");
if (!assetsToUpdate.ContainsKey(relatedAsset)
&& assetsToUpdateByFileName.TryGetValue(relatedAssetFileName, out var matchedKey))
{
relatedAsset = matchedKey;
}

if (assetsToUpdate.ContainsKey(relatedAsset))
{
assetsToUpdate.Add(satelliteAssembly.ItemSpec, satelliteAssembly);
Expand Down Expand Up @@ -590,9 +596,24 @@ private List<ITaskItem> ProcessCompressedAssets(

private static void UpdateRelatedAssetProperty(ITaskItem asset, TaskItem newAsset, Dictionary<string, ITaskItem> updatedAssetsMap)
{
if (!updatedAssetsMap.TryGetValue(asset.GetMetadata("RelatedAsset"), out var updatedRelatedAsset))
var relatedAsset = asset.GetMetadata("RelatedAsset");
if (!updatedAssetsMap.TryGetValue(relatedAsset, out var updatedRelatedAsset))
{
throw new InvalidOperationException("Related asset not found.");
// Fall back to filename matching when RelatedAsset uses a different base path
// than the asset's build-time Identity (e.g., OutputPath/wwwroot vs obj/webcil).
var relatedBaseName = Path.GetFileNameWithoutExtension(relatedAsset);
foreach (var kvp in updatedAssetsMap)
{
if (string.Equals(Path.GetFileNameWithoutExtension(kvp.Key), relatedBaseName, StringComparison.OrdinalIgnoreCase))
{
updatedRelatedAsset = kvp.Value;
break;
}
}
Comment on lines +599 to +612
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

UpdateRelatedAssetProperty does a linear scan over updatedAssetsMap when the exact RelatedAsset key isn’t found. On larger apps this can become O(n^2) across assets and is also harder to reason about when multiple entries share the same base name. Consider building a secondary lookup (e.g., normalized file name → updated asset) once and using that here, similar to the approach used in ComputeUpdatedAssemblies.

Copilot uses AI. Check for mistakes.
if (updatedRelatedAsset == null)
{
throw new InvalidOperationException("Related asset not found.");
Comment on lines +613 to +615
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The InvalidOperationException message ("Related asset not found.") doesn't include which RelatedAsset value failed to resolve, which makes diagnosing build/publish failures harder—especially now that there is fallback matching logic. Consider including the missing RelatedAsset value (and possibly the current asset ItemSpec) in the exception message.

Suggested change
if (updatedRelatedAsset == null)
{
throw new InvalidOperationException("Related asset not found.");
if (updatedRelatedAsset is null)
{
throw new InvalidOperationException($"Related asset '{relatedAsset}' for asset '{asset.ItemSpec}' not found.");

Copilot uses AI. Check for mistakes.
}
}

newAsset.SetMetadata("RelatedAsset", updatedRelatedAsset.ItemSpec);
Expand Down
Loading