diff --git a/src/libraries/sendtohelix-browser.targets b/src/libraries/sendtohelix-browser.targets index 5e6d3bc50e63a..11dcb6fa04d05 100644 --- a/src/libraries/sendtohelix-browser.targets +++ b/src/libraries/sendtohelix-browser.targets @@ -139,6 +139,7 @@ $(RepositoryEngineeringDir)testing\scenarios\BuildWasmAppsJobsList.txt <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' == 'true'">-notrait category=no-workload <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' != 'true'">-trait category=no-workload + <_XUnitTraitArg Condition="'$(TestUsingFingerprinting)' == 'false'">$(_XUnitTraitArg) -trait category=no-fingerprinting diff --git a/src/libraries/sendtohelix-wasm.targets b/src/libraries/sendtohelix-wasm.targets index 3fce57cf9c4e1..23fb50e216f09 100644 --- a/src/libraries/sendtohelix-wasm.targets +++ b/src/libraries/sendtohelix-wasm.targets @@ -13,6 +13,7 @@ Workloads- NoWorkload- $(WorkItemPrefix)NoWebcil- + $(WorkItemPrefix)NoFingerprint- $(WorkItemPrefix)ST- $(WorkItemPrefix)MT- @@ -49,7 +50,7 @@ - + $(_BuildWasmAppsPayloadArchive) set "HELIX_XUNIT_ARGS=-class %(Identity)" export "HELIX_XUNIT_ARGS=-class %(Identity)" @@ -57,7 +58,7 @@ $(_workItemTimeout) - + $(_BuildWasmAppsPayloadArchive) $(HelixCommand) $(_workItemTimeout) diff --git a/src/libraries/sendtohelix.proj b/src/libraries/sendtohelix.proj index 17aa91b1597bb..999d8ed9d61c7 100644 --- a/src/libraries/sendtohelix.proj +++ b/src/libraries/sendtohelix.proj @@ -87,17 +87,24 @@ <_TestUsingWorkloadsValues Include="true;false" /> <_TestUsingWebcilValues Include="true;false" Condition="'$(TargetOS)' == 'browser'" /> + <_TestUsingFingerprintingValues Include="true;false" Condition="'$(TargetOS)' == 'browser'" /> <_TestUsingCrossProductValuesTemp Include="@(_TestUsingWorkloadsValues)"> %(_TestUsingWorkloadsValues.Identity) - <_TestUsingCrossProductValues Include="@(_TestUsingCrossProductValuesTemp)"> + <_TestUsingCrossProductValuesTemp2 Include="@(_TestUsingCrossProductValuesTemp)"> %(_TestUsingWebcilValues.Identity) + + <_TestUsingCrossProductValues Include="@(_TestUsingCrossProductValuesTemp2)"> + %(_TestUsingFingerprintingValues.Identity) + + <_TestUsingCrossProductValues Remove="@(_TestUsingCrossProductValues)" Condition="'%(_TestUsingCrossProductValues.Workloads)' == 'false' and '%(_TestUsingCrossProductValues.Fingerprinting)' == 'false'" /> + <_BuildWasmAppsProjectsToBuild Include="$(PerScenarioProjectFile)"> - $(_PropertiesToPass);Scenario=BuildWasmApps;TestArchiveRuntimeFile=$(TestArchiveRuntimeFile);TestUsingWorkloads=%(_TestUsingCrossProductValues.Workloads);TestUsingWebcil=%(_TestUsingCrossProductValues.Webcil) + $(_PropertiesToPass);Scenario=BuildWasmApps;TestArchiveRuntimeFile=$(TestArchiveRuntimeFile);TestUsingWorkloads=%(_TestUsingCrossProductValues.Workloads);TestUsingWebcil=%(_TestUsingCrossProductValues.Webcil);TestUsingFingerprinting=%(_TestUsingCrossProductValues.Fingerprinting) %(_BuildWasmAppsProjectsToBuild.AdditionalProperties);NeedsToBuildWasmAppsOnHelix=$(NeedsToBuildWasmAppsOnHelix) diff --git a/src/libraries/sendtohelixhelp.proj b/src/libraries/sendtohelixhelp.proj index 81a3ac4d71097..1873cbb79e03f 100644 --- a/src/libraries/sendtohelixhelp.proj +++ b/src/libraries/sendtohelixhelp.proj @@ -157,6 +157,7 @@ + @@ -346,7 +347,7 @@ + Text="Scenario: $(Scenario), TestUsingWorkloads: $(TestUsingWorkloads), TestUsingWebcil: $(TestUsingWebcil), TestUsingFingerprinting: $(TestUsingFingerprinting)" /> diff --git a/src/mono/browser/.gitignore b/src/mono/browser/.gitignore index 6177029d9f9e1..a7c533cfd01b3 100644 --- a/src/mono/browser/.gitignore +++ b/src/mono/browser/.gitignore @@ -2,4 +2,4 @@ .stamp-wasm-install-and-select* emsdk -runtime/dotnet.d.ts.sha256 +runtime/*.d.ts.sha256 diff --git a/src/mono/browser/debugger/BrowserDebugProxy/DebugStore.cs b/src/mono/browser/debugger/BrowserDebugProxy/DebugStore.cs index dfd0f368561bd..c535f47fc7d61 100644 --- a/src/mono/browser/debugger/BrowserDebugProxy/DebugStore.cs +++ b/src/mono/browser/debugger/BrowserDebugProxy/DebugStore.cs @@ -1683,6 +1683,9 @@ public async IAsyncEnumerable Load(SessionId id, string[] loaded_fil var asm_files = new List(); List steps = new List(); + // Use System.Private.CoreLib to determine if we have a fingerprinted assemblies or not. + bool isFingerprinted = Path.GetFileNameWithoutExtension(loaded_files.FirstOrDefault(f => f.Contains("System.Private.CoreLib"))) != "System.Private.CoreLib"; + if (!useDebuggerProtocol) { var pdb_files = new List(); @@ -1698,8 +1701,17 @@ public async IAsyncEnumerable Load(SessionId id, string[] loaded_fil { try { - string candidate_pdb = Path.ChangeExtension(url, "pdb"); - string pdb = pdb_files.FirstOrDefault(n => n == candidate_pdb); + string pdb; + if (isFingerprinted) + { + string noFingerprintPdbFileName = string.Concat(Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(url)), ".pdb"); + pdb = pdb_files.FirstOrDefault(n => string.Concat(Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(n)), Path.GetExtension(n)) == noFingerprintPdbFileName); + } + else + { + string candidate_pdb = Path.ChangeExtension(url, "pdb"); + pdb = pdb_files.FirstOrDefault(n => n == candidate_pdb); + } steps.Add( new DebugItem @@ -1722,12 +1734,14 @@ public async IAsyncEnumerable Load(SessionId id, string[] loaded_fil continue; try { - string unescapedFileName = Uri.UnescapeDataString(file_name); + string unescapedFileName = Path.GetFileName(Uri.UnescapeDataString(file_name)); + if (isFingerprinted) + unescapedFileName = string.Concat(Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(unescapedFileName)), Path.GetExtension(unescapedFileName)); steps.Add( new DebugItem { Url = file_name, - DataTask = context.SdbAgent.GetDataFromAssemblyAndPdbAsync(Path.GetFileName(unescapedFileName), false, token) + DataTask = context.SdbAgent.GetDataFromAssemblyAndPdbAsync(unescapedFileName, false, token) }); } catch (Exception e) diff --git a/src/mono/browser/runtime/diagnostics-mock.d.ts b/src/mono/browser/runtime/diagnostics-mock.d.ts index 59686e8797399..d1f24c99761d4 100644 --- a/src/mono/browser/runtime/diagnostics-mock.d.ts +++ b/src/mono/browser/runtime/diagnostics-mock.d.ts @@ -71,4 +71,4 @@ interface MockEnvironment { expectAdvertise: FilterPredicate; } -export { MockEnvironment, MockScriptConnection, PromiseAndController }; +export type { MockEnvironment, MockScriptConnection, PromiseAndController }; diff --git a/src/mono/browser/runtime/diagnostics-mock.d.ts.sha256 b/src/mono/browser/runtime/diagnostics-mock.d.ts.sha256 deleted file mode 100644 index 0cfcc26b86906..0000000000000 --- a/src/mono/browser/runtime/diagnostics-mock.d.ts.sha256 +++ /dev/null @@ -1 +0,0 @@ -6d0ff454946223f77abe8e6c1e377489c33b2914da86120f6b2952b739ebec20 \ No newline at end of file diff --git a/src/mono/browser/runtime/dotnet.d.ts b/src/mono/browser/runtime/dotnet.d.ts index 94e31ff6a3e88..1357bc4a0b613 100644 --- a/src/mono/browser/runtime/dotnet.d.ts +++ b/src/mono/browser/runtime/dotnet.d.ts @@ -247,6 +247,9 @@ type ResourceExtensions = { }; interface ResourceGroups { hash?: string; + fingerprinting?: { + [name: string]: string; + }; coreAssembly?: ResourceList; assembly?: ResourceList; lazyAssembly?: ResourceList; @@ -692,4 +695,4 @@ declare global { } declare const createDotnetRuntime: CreateDotnetRuntimeType; -export { AssetBehaviors, AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; +export { type AssetBehaviors, type AssetEntry, type CreateDotnetRuntimeType, type DotnetHostBuilder, type DotnetModuleConfig, type EmscriptenModule, GlobalizationMode, type IMemoryView, type ModuleAPI, type MonoConfig, type RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; diff --git a/src/mono/browser/runtime/lazyLoading.ts b/src/mono/browser/runtime/lazyLoading.ts index f4da4521010a9..3d075b3005a8e 100644 --- a/src/mono/browser/runtime/lazyLoading.ts +++ b/src/mono/browser/runtime/lazyLoading.ts @@ -7,11 +7,23 @@ import { AssetEntry } from "./types"; export async function loadLazyAssembly (assemblyNameToLoad: string): Promise { const resources = loaderHelpers.config.resources!; + const originalAssemblyName = assemblyNameToLoad; const lazyAssemblies = resources.lazyAssembly; if (!lazyAssemblies) { throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly."); } + if (loaderHelpers.config.resources!.fingerprinting) { + const map = loaderHelpers.config.resources!.fingerprinting; + for (const fingerprintedName in map) { + const nonFingerprintedName = map[fingerprintedName]; + if (nonFingerprintedName == assemblyNameToLoad) { + assemblyNameToLoad = fingerprintedName; + break; + } + } + } + if (!lazyAssemblies[assemblyNameToLoad]) { throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`); } @@ -26,8 +38,22 @@ export async function loadLazyAssembly (assemblyNameToLoad: string): Promise { + if (resources.fingerprinting && (asset.behavior == "assembly" || asset.behavior == "pdb" || asset.behavior == "resource")) { + asset.virtualPath = getNonFingerprintedAssetName(asset.name); + } if (isCore) { asset.isCore = true; coreAssetsToLoad.push(asset); @@ -418,7 +421,7 @@ export function prepareAssets () { behavior: "icu", loadRemote: true }); - } else if (name === "segmentation-rules.json") { + } else if (name.startsWith("segmentation-rules") && name.endsWith(".json")) { assetsToLoad.push({ name, hash: resources.icu[name], @@ -460,6 +463,15 @@ export function prepareAssets () { config.assets = [...coreAssetsToLoad, ...assetsToLoad, ...modulesAssets]; } +export function getNonFingerprintedAssetName (assetName: string) { + const fingerprinting = loaderHelpers.config.resources?.fingerprinting; + if (fingerprinting && fingerprinting[assetName]) { + return fingerprinting[assetName]; + } + + return assetName; +} + export function prepareAssetsWorker () { const config = loaderHelpers.config; mono_assert(config.assets, "config.assets must be defined"); diff --git a/src/mono/browser/runtime/loader/icu.ts b/src/mono/browser/runtime/loader/icu.ts index b9bf54eb31f10..d5ad0dd3b8bbd 100644 --- a/src/mono/browser/runtime/loader/icu.ts +++ b/src/mono/browser/runtime/loader/icu.ts @@ -5,6 +5,7 @@ import { mono_log_error } from "./logging"; import { GlobalizationMode, MonoConfig } from "../types"; import { ENVIRONMENT_IS_WEB, loaderHelpers } from "./globals"; import { mono_log_info, mono_log_debug } from "./logging"; +import { getNonFingerprintedAssetName } from "./assets"; export function init_globalization () { loaderHelpers.preferredIcuAsset = getIcuResourceName(loaderHelpers.config); @@ -51,6 +52,17 @@ export function getIcuResourceName (config: MonoConfig): string | null { const culture = config.applicationCulture || (ENVIRONMENT_IS_WEB ? (globalThis.navigator && globalThis.navigator.languages && globalThis.navigator.languages[0]) : Intl.DateTimeFormat().resolvedOptions().locale); const icuFiles = Object.keys(config.resources.icu); + const fileMapping: { + [k: string]: string + } = {}; + for (let index = 0; index < icuFiles.length; index++) { + const icuFile = icuFiles[index]; + if (config.resources.fingerprinting) { + fileMapping[getNonFingerprintedAssetName(icuFile)] = icuFile; + } else { + fileMapping[icuFile] = icuFile; + } + } let icuFile = null; if (config.globalizationMode === GlobalizationMode.Custom) { @@ -65,8 +77,8 @@ export function getIcuResourceName (config: MonoConfig): string | null { icuFile = getShardedIcuResourceName(culture); } - if (icuFile && icuFiles.includes(icuFile)) { - return icuFile; + if (icuFile && fileMapping[icuFile]) { + return fileMapping[icuFile]; } } diff --git a/src/mono/browser/runtime/types/index.ts b/src/mono/browser/runtime/types/index.ts index 5e8e8c9d18bb7..c3658624dff19 100644 --- a/src/mono/browser/runtime/types/index.ts +++ b/src/mono/browser/runtime/types/index.ts @@ -203,6 +203,7 @@ export type ResourceExtensions = { [extensionName: string]: ResourceList }; export interface ResourceGroups { hash?: string; + fingerprinting?: { [name: string]: string }, coreAssembly?: ResourceList; // nullable only temporarily assembly?: ResourceList; // nullable only temporarily lazyAssembly?: ResourceList; // nullable only temporarily diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props index 45d892d2fc467..5adc448e11f4b 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props @@ -35,6 +35,7 @@ Copyright (c) .NET Foundation. All rights reserved. ComputeFilesToPublish;GetCurrentProjectPublishStaticWebAssetItems $(StaticWebAssetsAdditionalPublishProperties);BuildProjectReferences=false;ResolveAssemblyReferencesFindRelatedSatellites=true $(StaticWebAssetsAdditionalPublishPropertiesToRemove);NoBuild;RuntimeIdentifier + true 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 02688abdf80c1..22ebca926a5ce 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 @@ -65,7 +65,6 @@ Copyright (c) .NET Foundation. All rights reserved. true true - false false @@ -178,6 +177,7 @@ Copyright (c) .NET Foundation. All rights reserved. <_TargetingNET80OrLater>false <_TargetingNET80OrLater Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '8.0'))">true + <_TargetingNET90OrLater Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '9.0'))">true <_BlazorEnableTimeZoneSupport>$(BlazorEnableTimeZoneSupport) <_BlazorEnableTimeZoneSupport Condition="'$(_BlazorEnableTimeZoneSupport)' == ''">true @@ -194,6 +194,11 @@ Copyright (c) .NET Foundation. All rights reserved. <_BlazorWebAssemblyStartupMemoryCache>$(BlazorWebAssemblyStartupMemoryCache) <_BlazorWebAssemblyJiterpreter>$(BlazorWebAssemblyJiterpreter) <_BlazorWebAssemblyRuntimeOptions>$(BlazorWebAssemblyRuntimeOptions) + <_WasmFingerprintAssets>$(WasmFingerprintAssets) + <_WasmFingerprintAssets Condition="'$(_WasmFingerprintAssets)' == '' and '$(_TargetingNET90OrLater)' == 'true'">true + <_WasmFingerprintAssets Condition="'$(_WasmFingerprintAssets)' == ''">false + <_WasmFingerprintDotnetJs>$(WasmFingerprintDotnetJs) + <_WasmFingerprintDotnetJs Condition="'$(_WasmFingerprintDotnetJs)' == ''">false $(OutputPath)$(PublishDirName)\ @@ -238,12 +243,12 @@ Copyright (c) .NET Foundation. All rights reserved. InvariantGlobalization="$(_WasmInvariantGlobalization)" HybridGlobalization="$(_IsHybridGlobalization)" LoadFullICUData="$(_BlazorWebAssemblyLoadAllGlobalizationData)" - DotNetJsVersion="$(_WasmRuntimePackVersion)" CopySymbols="$(_WasmCopyOutputSymbolsToOutputDirectory)" OutputPath="$(OutputPath)" - FingerprintDotNetJs="$(WasmFingerprintDotnetJs)" EnableThreads="$(_WasmEnableThreads)" EmitSourceMap="$(_WasmEmitSourceMapBuild)" + FingerprintAssets="$(_WasmFingerprintAssets)" + FingerprintDotnetJs="$(_WasmFingerprintDotnetJs)" > @@ -260,6 +265,13 @@ Copyright (c) .NET Foundation. All rights reserved. + + <_WasmFingerprintPatterns Include="WasmFiles" Pattern="*.wasm" Expression="#[.{fingerprint}]!" /> + <_WasmFingerprintPatterns Include="DllFiles" Pattern="*.dll" Expression="#[.{fingerprint}]!" /> + <_WasmFingerprintPatterns Include="DatFiles" Pattern="*.dat" Expression="#[.{fingerprint}]!" /> + <_WasmFingerprintPatterns Include="Pdb" Pattern="*.pdb" Expression="#[.{fingerprint}]!" /> + + @@ -322,25 +336,34 @@ Copyright (c) .NET Foundation. All rights reserved. Condition="'%(StaticWebAsset.AssetTraitName)' == 'JSModule' and '%(StaticWebAsset.AssetTraitValue)' == 'JSLibraryModule' and '%(AssetKind)' != 'Publish'" /> - - - - - - - + + - - - + + + + + + + + IsMultiThreaded="$(WasmEnableThreads)" + FingerprintAssets="$(_WasmFingerprintAssets)" /> @@ -401,14 +425,6 @@ Copyright (c) .NET Foundation. All rights reserved. - - - - @@ -468,13 +484,13 @@ Copyright (c) .NET Foundation. All rights reserved. LoadFullICUData="$(_BlazorWebAssemblyLoadAllGlobalizationData)" CopySymbols="$(CopyOutputSymbolsToPublishDirectory)" ExistingAssets="@(_WasmPublishPrefilteredAssets)" - DotNetJsVersion="$(_WasmRuntimePackVersion)" - FingerprintDotNetJs="$(WasmFingerprintDotnetJs)" EnableThreads="$(_WasmEnableThreads)" EmitSourceMap="$(_WasmEmitSourceMapPublish)" IsWebCilEnabled="$(_WasmEnableWebcil)" + FingerprintAssets="$(_WasmFingerprintAssets)" > + @@ -497,7 +513,7 @@ Copyright (c) .NET Foundation. All rights reserved. RemoveMetadata="Integrity;Fingerprint" /> - + @@ -617,26 +633,28 @@ Copyright (c) .NET Foundation. All rights reserved. <_WasmJsModuleCandidatesForPublish Include="@(StaticWebAsset)" Condition="'%(StaticWebAsset.AssetTraitName)' == 'JSModule' and '%(StaticWebAsset.AssetTraitValue)' == 'JSLibraryModule' and '%(AssetKind)' != 'Build'" /> - - - <_WasmPublishAsset Remove="@(_BlazorExtensionsCandidatesForPublish)" /> - - - + + - - - + + + + IsMultiThreaded="$(WasmEnableThreads)" + FingerprintAssets="$(_WasmFingerprintAssets)" /> diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs index 10f1ff6f47848..5877b0f6a06dd 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs @@ -166,8 +166,8 @@ void AssertResourcesDlls(string basePath) { foreach (string culture in cultures) { - string resourceAssemblyPath = Path.Combine(basePath, culture, $"{id}.resources{ProjectProviderBase.WasmAssemblyExtension}"); - Assert.True(File.Exists(resourceAssemblyPath), $"Expects to have a resource assembly at {resourceAssemblyPath}"); + string? resourceAssemblyPath = Directory.EnumerateFiles(Path.Combine(basePath, culture), $"*{ProjectProviderBase.WasmAssemblyExtension}").SingleOrDefault(f => Path.GetFileNameWithoutExtension(f).StartsWith($"{id}.resources")); + Assert.True(resourceAssemblyPath != null && File.Exists(resourceAssemblyPath), $"Expects to have a resource assembly at {resourceAssemblyPath}"); } } } diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests3.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests3.cs index 31c9f0ac05f02..4d82356b10379 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests3.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests3.cs @@ -114,10 +114,10 @@ public void BugRegression_60479_WithRazorClassLib() .ExecuteWithCapturedOutput("new razorclasslib") .EnsureSuccessful(); - string razorClassLibraryFileName = $"RazorClassLibrary{ProjectProviderBase.WasmAssemblyExtension}"; + string razorClassLibraryFileNameWithoutExtension = "RazorClassLibrary"; AddItemsPropertiesToProject(wasmProjectFile, extraItems: @$" - + "); _projectDir = wasmProjectDir; @@ -140,7 +140,7 @@ public void BugRegression_60479_WithRazorClassLib() throw new XunitException($"Could not find resources.lazyAssembly object in {bootJson}"); } - Assert.Contains(razorClassLibraryFileName, lazyVal.EnumerateObject().Select(jp => jp.Name)); + Assert.True(lazyVal.EnumerateObject().Select(jp => jp.Name).FirstOrDefault(f => f.StartsWith(razorClassLibraryFileNameWithoutExtension)) != null); } private void BlazorAddRazorButton(string buttonText, string customCode, string methodName = "test", string razorPage = "Pages/Counter.razor") diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs index 6142128dd500d..0b71e4d663015 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs @@ -134,6 +134,12 @@ public BuildEnvironment() EnvVars["WasmEnableWebCil"] = "false"; } + if (!EnvironmentVariables.UseFingerprinting) + { + // Default is 'true' + EnvVars["WasmFingerprintAssets"] = "false"; + } + DotNet = Path.Combine(sdkForWorkloadPath!, "dotnet"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) DotNet += ".exe"; diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/DotNetFileName.cs b/src/mono/wasm/Wasm.Build.Tests/Common/DotNetFileName.cs index 42fd8873e78f5..0c1739d55833d 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/DotNetFileName.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/DotNetFileName.cs @@ -8,7 +8,6 @@ namespace Wasm.Build.Tests; public sealed record DotNetFileName ( string ExpectedFilename, - string? Version, string? Hash, string ActualPath ); diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs index e2cf9b3dbf8b4..d928fc1e1b406 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs @@ -22,6 +22,7 @@ internal static class EnvironmentVariables internal static readonly bool IsRunningOnCI = Environment.GetEnvironmentVariable("IS_RUNNING_ON_CI") is "true"; internal static readonly bool ShowBuildOutput = IsRunningOnCI || Environment.GetEnvironmentVariable("SHOW_BUILD_OUTPUT") is not null; internal static readonly bool UseWebcil = Environment.GetEnvironmentVariable("USE_WEBCIL_FOR_TESTS") is "true"; + internal static readonly bool UseFingerprinting = Environment.GetEnvironmentVariable("USE_FINGERPRINTING_FOR_TESTS") is "true"; internal static readonly string? SdkDirName = Environment.GetEnvironmentVariable("SDK_DIR_NAME"); internal static readonly string? WasiSdkPath = Environment.GetEnvironmentVariable("WASI_SDK_PATH"); internal static readonly bool WorkloadsTestPreviousVersions = Environment.GetEnvironmentVariable("WORKLOADS_TEST_PREVIOUS_VERSIONS") is "true"; diff --git a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs index 6be096cb67ea5..69b6d07ff2d6c 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Runtime.Serialization.Json; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.NET.Sdk.WebAssembly; using Xunit; @@ -22,7 +23,7 @@ namespace Wasm.Build.Tests; public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? _projectDir) { public static string WasmAssemblyExtension = BuildTestBase.s_buildEnv.UseWebcil ? ".wasm" : ".dll"; - protected const string s_dotnetVersionHashRegex = @"\.(?[0-9]+\.[0-9]+\.[a-zA-Z0-9\.-]+)\.(?[a-zA-Z0-9]+)\."; + protected const string s_dotnetVersionHashRegex = @"\.(?[a-zA-Z0-9]+)\."; private const string s_runtimePackPathPattern = "\\*\\* MicrosoftNetCoreAppRuntimePackDir : '([^ ']*)'"; private static Regex s_runtimePackPathRegex = new Regex(s_runtimePackPathPattern); @@ -37,6 +38,10 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? protected BuildEnvironment _buildEnv = BuildTestBase.s_buildEnv; public string BundleDirName { get; set; } = "wwwroot"; + public bool IsFingerprintingSupported { get; protected set; } + + public bool IsFingerprintingEnabled => IsFingerprintingSupported && EnvironmentVariables.UseFingerprinting; + // Returns the actual files on disk public IReadOnlyDictionary AssertBasicBundle(AssertBundleOptionsBase assertOptions) { @@ -45,17 +50,17 @@ public IReadOnlyDictionary AssertBasicBundle(AssertBundl TestUtils.AssertFilesExist(assertOptions.BinFrameworkDir, new[] { "System.Private.CoreLib.dll" }, - expectToExist: !BuildTestBase.UseWebcil); + expectToExist: IsFingerprintingEnabled ? false : !BuildTestBase.UseWebcil); TestUtils.AssertFilesExist(assertOptions.BinFrameworkDir, new[] { "System.Private.CoreLib.wasm" }, - expectToExist: BuildTestBase.UseWebcil); + expectToExist: IsFingerprintingEnabled ? false : BuildTestBase.UseWebcil); - AssertBootJson(assertOptions); + var bootJson = AssertBootJson(assertOptions); // icu if (assertOptions.AssertIcuAssets) { - AssertIcuAssets(assertOptions); + AssertIcuAssets(assertOptions, bootJson); } else { @@ -124,8 +129,7 @@ public IReadOnlyDictionary FindAndAssertDotnetFiles( return true; actual[expectedFilename] = new(ExpectedFilename: expectedFilename, - Version: match.Groups[1].Value, - Hash: match.Groups[2].Value, + Hash: match.Groups[1].Value, ActualPath: actualFile); } else @@ -134,7 +138,6 @@ public IReadOnlyDictionary FindAndAssertDotnetFiles( return true; actual[expectedFilename] = new(ExpectedFilename: expectedFilename, - Version: null, Hash: null, ActualPath: actualFile); } @@ -180,15 +183,11 @@ private void AssertDotNetFilesSet( expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, expectFingerprintForThisFile: expectFingerprint)) { - if (string.IsNullOrEmpty(actual[expectedFilename].Version)) - throw new XunitException($"Expected version in filename: {actual[expectedFilename].ActualPath}"); if (string.IsNullOrEmpty(actual[expectedFilename].Hash)) throw new XunitException($"Expected hash in filename: {actual[expectedFilename].ActualPath}"); } else { - if (!string.IsNullOrEmpty(actual[expectedFilename].Version)) - throw new XunitException($"Expected no version in filename: {actual[expectedFilename].ActualPath}"); if (!string.IsNullOrEmpty(actual[expectedFilename].Hash)) throw new XunitException($"Expected no hash in filename: {actual[expectedFilename].ActualPath}"); } @@ -322,8 +321,8 @@ public static string FindSubDirIgnoringCase(string parentDir, string dirName) return dict; } - public static bool ShouldCheckFingerprint(string expectedFilename, bool expectFingerprintOnDotnetJs, bool expectFingerprintForThisFile) => - (expectedFilename == "dotnet.js" && expectFingerprintOnDotnetJs) || expectFingerprintForThisFile; + public bool ShouldCheckFingerprint(string expectedFilename, bool expectFingerprintOnDotnetJs, bool expectFingerprintForThisFile) + => IsFingerprintingEnabled && ((expectedFilename == "dotnet.js" && expectFingerprintOnDotnetJs) || expectFingerprintForThisFile); public static void AssertRuntimePackPath(string buildOutput, string targetFramework, RuntimeVariant runtimeType = RuntimeVariant.SingleThreaded) @@ -351,7 +350,7 @@ public static void AssertDotNetJsSymbols(AssertBundleOptionsBase assertOptions) } } - public void AssertIcuAssets(AssertBundleOptionsBase assertOptions) + public void AssertIcuAssets(AssertBundleOptionsBase assertOptions, BootJsonData bootJson) { List expected = new(); switch (assertOptions.GlobalizationMode) @@ -370,7 +369,7 @@ public void AssertIcuAssets(AssertBundleOptionsBase assertOptions) throw new ArgumentException("WasmBuildTest is invalid, value for predefinedIcudt is required when GlobalizationMode=PredefinedIcu."); // predefined ICU name can be identical with the icu files from runtime pack - expected.Add(assertOptions.PredefinedIcudt); + expected.Add(Path.GetFileName(assertOptions.PredefinedIcudt)); break; case GlobalizationMode.Sharded: // icu shard chosen based on the locale @@ -384,7 +383,23 @@ public void AssertIcuAssets(AssertBundleOptionsBase assertOptions) IEnumerable actual = Directory.EnumerateFiles(assertOptions.BinFrameworkDir, "icudt*dat"); if (assertOptions.GlobalizationMode == GlobalizationMode.Hybrid) - actual = actual.Union(Directory.EnumerateFiles(assertOptions.BinFrameworkDir, "segmentation-rules.json")); + actual = actual.Union(Directory.EnumerateFiles(assertOptions.BinFrameworkDir, "segmentation-rules*json")); + + if (IsFingerprintingEnabled) + { + var expectedFingerprinted = new List(expected.Count); + foreach (var expectedItem in expected) + { + var expectedFingerprintedItem = bootJson.resources.fingerprinting.Where(kv => kv.Value == expectedItem).SingleOrDefault().Key; + if (string.IsNullOrEmpty(expectedFingerprintedItem)) + throw new XunitException($"Could not find ICU asset {expectedItem} in fingerprinting in boot config"); + + expectedFingerprinted.Add(expectedFingerprintedItem); + } + + expected = expectedFingerprinted; + } + AssertFileNames(expected, actual); if (assertOptions.GlobalizationMode is GlobalizationMode.PredefinedIcu) { @@ -396,7 +411,7 @@ public void AssertIcuAssets(AssertBundleOptionsBase assertOptions) } } - public void AssertBootJson(AssertBundleOptionsBase options) + public BootJsonData AssertBootJson(AssertBundleOptionsBase options) { EnsureProjectDirIsSet(); // string binFrameworkDir = FindBinFrameworkDir(options.Config, options.IsPublish, options.TargetFramework); @@ -406,23 +421,29 @@ public void AssertBootJson(AssertBundleOptionsBase options) BootJsonData bootJson = ParseBootData(bootJsonPath); string spcExpectedFilename = $"System.Private.CoreLib{WasmAssemblyExtension}"; + + if (IsFingerprintingEnabled) + { + spcExpectedFilename = bootJson.resources.fingerprinting.Where(kv => kv.Value == spcExpectedFilename).SingleOrDefault().Key; + if (string.IsNullOrEmpty(spcExpectedFilename)) + throw new XunitException($"Could not find an assembly System.Private.CoreLib in fingerprinting in {bootJsonPath}"); + } + string? spcActualFilename = bootJson.resources.coreAssembly.Keys - .Where(a => Path.GetFileNameWithoutExtension(a) == "System.Private.CoreLib") + .Where(a => a == spcExpectedFilename) .SingleOrDefault(); if (spcActualFilename is null) throw new XunitException($"Could not find an assembly named System.Private.CoreLib.* in {bootJsonPath}"); - if (spcExpectedFilename != spcActualFilename) - throw new XunitException($"Expected to find {spcExpectedFilename} but found {spcActualFilename} in {bootJsonPath}"); var bootJsonEntries = bootJson.resources.jsModuleNative.Keys + .Union(bootJson.resources.wasmNative.Keys) .Union(bootJson.resources.jsModuleRuntime.Keys) .Union(bootJson.resources.jsModuleWorker?.Keys ?? Enumerable.Empty()) .Union(bootJson.resources.jsModuleGlobalization?.Keys ?? Enumerable.Empty()) .Union(bootJson.resources.wasmSymbols?.Keys ?? Enumerable.Empty()) - .Union(bootJson.resources.wasmNative.Keys) .ToArray(); - var expectedEntries = new SortedDictionary>(); + var expectedEntries = new SortedDictionary>(); IReadOnlySet expected = GetDotNetFilesExpectedSet(options); var knownSet = GetAllKnownDotnetFilesToFingerprintMap(options); @@ -442,28 +463,34 @@ public void AssertBootJson(AssertBundleOptionsBase options) expectFingerprintOnDotnetJs: options.ExpectFingerprintOnDotnetJs, expectFingerprintForThisFile: expectFingerprint)) { - Assert.Matches($"{prefix}{s_dotnetVersionHashRegex}{extension}", item); + return Regex.Match(item, $"{prefix}{s_dotnetVersionHashRegex}{extension}").Success; } else { - Assert.Equal(expectedFilename, item); + return expectedFilename == item; } - - string absolutePath = Path.Combine(binFrameworkDir, item); - Assert.True(File.Exists(absolutePath), $"Expected to find '{absolutePath}'"); }; } // FIXME: maybe use custom code so the details can show up in the log - bootJsonEntries = bootJsonEntries.Order().ToArray(); + bootJsonEntries = bootJsonEntries.ToArray(); if (bootJsonEntries.Length != expectedEntries.Count) { throw new XunitException($"In {bootJsonPath}{Environment.NewLine}" + $" Expected: {string.Join(", ", expectedEntries.Keys.ToArray())}{Environment.NewLine}" + $" Actual : {string.Join(", ", bootJsonEntries)}"); + } + var expectedEntriesToCheck = expectedEntries.Values.ToList(); + foreach (var bootJsonEntry in bootJsonEntries) + { + var matcher = expectedEntriesToCheck.FirstOrDefault(c => c(bootJsonEntry)); + if (matcher == null) + throw new XunitException($"Unexpected entry in boot json '{bootJsonEntry}'. Expected files {String.Join(", ", expectedEntries.Keys)}"); + expectedEntriesToCheck.Remove(matcher); } - Assert.Collection(bootJsonEntries.Order(), expectedEntries.Values.ToArray()); + + return bootJson; } public static BootJsonData ParseBootData(string bootJsonPath) diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs index 3142bed49a35f..b32b95debd879 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using Xunit; @@ -107,7 +108,7 @@ private void UpdateMainJsEnvironmentVariables(params (string key, string value)[ File.WriteAllText(mainJsPath, mainJsContent); } - [Theory] + [Theory, TestCategory("no-fingerprinting")] [InlineData("Debug")] [InlineData("Release")] public void BrowserBuildThenPublish(string config) @@ -294,7 +295,7 @@ void AddTestData(bool forConsole, bool runOutsideProjectDirectory) return data; } - [Theory] + [Theory, TestCategory("no-fingerprinting")] [MemberData(nameof(TestDataForAppBundleDir))] public async Task RunWithDifferentAppBundleLocations(bool forConsole, bool runOutsideProjectDirectory, string extraProperties) => await (forConsole @@ -598,11 +599,12 @@ internal static void TestWasmStripILAfterAOTOutput(string objBuildDir, string fr Assert.False(Directory.Exists(strippedAssemblyDir), $"Expected {strippedAssemblyDir} to not exist"); string assemblyToExamine = "System.Private.CoreLib.dll"; + string assemblyToExamineWithoutExtension = Path.GetFileNameWithoutExtension(assemblyToExamine); string originalAssembly = Path.Combine(objBuildDir, origAssemblyDir, assemblyToExamine); string strippedAssembly = Path.Combine(objBuildDir, strippedAssemblyDir, assemblyToExamine); - string bundledAssembly = Path.Combine(frameworkDir, Path.ChangeExtension(assemblyToExamine, ProjectProviderBase.WasmAssemblyExtension)); + string? bundledAssembly = Directory.EnumerateFiles(frameworkDir, $"*{ProjectProviderBase.WasmAssemblyExtension}").FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).StartsWith(assemblyToExamineWithoutExtension)); Assert.True(File.Exists(originalAssembly), $"Expected {nameof(originalAssembly)} {originalAssembly} to exist"); - Assert.True(File.Exists(bundledAssembly), $"Expected {nameof(bundledAssembly)} {bundledAssembly} to exist"); + Assert.True(bundledAssembly != null && File.Exists(bundledAssembly), $"Expected {nameof(bundledAssembly)} {bundledAssembly} to exist"); if (expectILStripping) Assert.True(File.Exists(strippedAssembly), $"Expected {nameof(strippedAssembly)} {strippedAssembly} to exist"); else @@ -658,8 +660,8 @@ public void PublishPdb(bool copyOutputSymbolsToPublishDirectory) void AssertFile(string suffix) { - var fileName = $"{id}{suffix}"; - Assert.True(copyOutputSymbolsToPublishDirectory == File.Exists(Path.Combine(publishFrameworkPath, fileName)), $"The {fileName} file {(copyOutputSymbolsToPublishDirectory ? "should" : "shouldn't")} exist in publish folder"); + var fileName = Directory.EnumerateFiles(publishFrameworkPath, $"*{suffix}").FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).StartsWith(id)); + Assert.True(copyOutputSymbolsToPublishDirectory == (fileName != null && File.Exists(fileName)), $"The {fileName} file {(copyOutputSymbolsToPublishDirectory ? "should" : "shouldn't")} exist in publish folder"); } } } diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LazyLoadingTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LazyLoadingTests.cs index 038951e1822e6..6a167ec2de10a 100644 --- a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LazyLoadingTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LazyLoadingTests.cs @@ -20,14 +20,16 @@ public LazyLoadingTests(ITestOutputHelper output, SharedBuildPerTestClassFixture { } - [Fact] + [Fact, TestCategory("no-fingerprinting")] public async Task LoadLazyAssemblyBeforeItIsNeeded() { CopyTestAsset("WasmBasicTestApp", "LazyLoadingTests", "App"); - PublishProject("Debug"); + BuildProject("Debug"); + + var result = await RunSdkStyleAppForBuild(new(Configuration: "Debug", TestScenario: "LazyLoadingTest")); - var result = await RunSdkStyleAppForPublish(new(Configuration: "Debug", TestScenario: "LazyLoadingTest")); Assert.True(result.TestOutput.Any(m => m.Contains("FirstName")), "The lazy loading test didn't emit expected message with JSON"); + Assert.True(result.ConsoleOutput.Any(m => m.Contains("Attempting to download") && m.Contains("_framework/Json.") && m.Contains(".pdb")), "The lazy loading test didn't load PDB"); } [Fact] diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/SatelliteLoadingTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/SatelliteLoadingTests.cs index 517f34255f996..5e15cc53841ad 100644 --- a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/SatelliteLoadingTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/SatelliteLoadingTests.cs @@ -23,7 +23,7 @@ public SatelliteLoadingTests(ITestOutputHelper output, SharedBuildPerTestClassFi { } - [Fact] + [Fact, TestCategory("no-fingerprinting")] public async Task LoadSatelliteAssembly() { CopyTestAsset("WasmBasicTestApp", "SatelliteLoadingTests", "App"); diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index 62601dd54d85b..cd39a7102485d 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -94,6 +94,7 @@ <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' == 'true'">-notrait category=no-workload <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' != 'true'">-trait category=no-workload + <_XUnitTraitArg Condition="'$(TestUsingFingerprinting)' == 'false'">-trait category=no-fingerprinting @@ -112,6 +113,9 @@ + + + diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs index 77ccc61fe5ac4..bd35644b0e8d0 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -17,7 +17,9 @@ public class WasmSdkBasedProjectProvider : ProjectProviderBase { public WasmSdkBasedProjectProvider(ITestOutputHelper _testOutput, string? _projectDir = null) : base(_testOutput, _projectDir) - {} + { + IsFingerprintingSupported = true; + } protected override IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(AssertBundleOptionsBase assertOptions) => new SortedDictionary() @@ -27,7 +29,7 @@ protected override IReadOnlyDictionary GetAllKnownDotnetFilesToFin { "dotnet.native.js", true }, { "dotnet.native.js.symbols", false }, { "dotnet.globalization.js", true }, - { "dotnet.native.wasm", false }, + { "dotnet.native.wasm", true }, { "dotnet.native.worker.mjs", true }, { "dotnet.runtime.js", true }, { "dotnet.runtime.js.map", false }, diff --git a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.cmd b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.cmd index e088aa4861df4..fc66d9e745a06 100644 --- a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.cmd +++ b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.cmd @@ -56,6 +56,11 @@ if [%TEST_USING_WEBCIL%] == [false] ( ) else ( set USE_WEBCIL_FOR_TESTS=true ) +if [%TEST_USING_FINGERPRINTING%] == [false] ( + set USE_FINGERPRINTING_FOR_TESTS=false +) else ( + set USE_FINGERPRINTING_FOR_TESTS=true +) if [%HELIX_CORRELATION_PAYLOAD%] NEQ [] ( robocopy /mt /np /nfl /NDL /nc /e %BASE_DIR%\%SDK_DIR_NAME% %EXECUTION_DIR%\%SDK_DIR_NAME% diff --git a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh index 2c23e8a4185f6..bab35b29d534a 100644 --- a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh +++ b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh @@ -39,6 +39,12 @@ function set_env_vars() export USE_WEBCIL_FOR_TESTS=true fi + if [ "x$TEST_USING_FINGERPRINTING" = "xfalse" ]; then + export USE_FINGERPRINTING_FOR_TESTS=false + else + export USE_FINGERPRINTING_FOR_TESTS=true + fi + local _SDK_DIR= if [[ -n "$HELIX_WORKITEM_UPLOAD_ROOT" ]]; then cp -r $BASE_DIR/$SDK_DIR_NAME $EXECUTION_DIR diff --git a/src/mono/wasm/sln/WasmBuild.sln b/src/mono/wasm/sln/WasmBuild.sln index b84f2d7ba8c84..48bbfe555ab42 100755 --- a/src/mono/wasm/sln/WasmBuild.sln +++ b/src/mono/wasm/sln/WasmBuild.sln @@ -29,6 +29,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wasi.Build.Tests", "..\..\w EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WasmTestRunner", "..\..\..\libraries\Common\tests\WasmTestRunner\WasmTestRunner.csproj", "{2BBE4AA8-5424-44AB-933C-66554B688872}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkloadBuildTasks", "..\..\..\tasks\WorkloadBuildTasks\WorkloadBuildTasks.csproj", "{680C730D-C257-4F65-9CD9-6B9A161844B0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,6 +89,10 @@ Global {2BBE4AA8-5424-44AB-933C-66554B688872}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BBE4AA8-5424-44AB-933C-66554B688872}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BBE4AA8-5424-44AB-933C-66554B688872}.Release|Any CPU.Build.0 = Release|Any CPU + {680C730D-C257-4F65-9CD9-6B9A161844B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {680C730D-C257-4F65-9CD9-6B9A161844B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {680C730D-C257-4F65-9CD9-6B9A161844B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {680C730D-C257-4F65-9CD9-6B9A161844B0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js index 74bfbb3fe5fd2..85fd83270b273 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js @@ -31,6 +31,9 @@ switch (testCase) { case "AppSettingsTest": dotnet.withApplicationEnvironment(params.get("applicationEnvironment")); break; + case "LazyLoadingTest": + dotnet.withDiagnosticTracing(true); + break; case "DownloadResourceProgressTest": if (params.get("failAssemblyDownload") === "true") { let assemblyCounter = 0; @@ -124,7 +127,7 @@ switch (testCase) { const { setModuleImports, getAssemblyExports, getConfig, INTERNAL } = await dotnet.create(); const config = getConfig(); const exports = await getAssemblyExports(config.mainAssemblyName); -const assemblyExtension = config.resources.coreAssembly['System.Private.CoreLib.wasm'] !== undefined ? ".wasm" : ".dll"; +const assemblyExtension = Object.keys(config.resources.coreAssembly)[0].endsWith('.wasm') ? ".wasm" : ".dll"; // Run the test case try { diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs index cf78cbbed1f05..6885bf62343c7 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs @@ -93,20 +93,49 @@ private static bool IsFromMonoPackage(ITaskItem candidate) return monoPackageIds.Contains(packageId, StringComparer.Ordinal); } - public static string GetCandidateRelativePath(ITaskItem candidate) + public static string GetCandidateRelativePath(ITaskItem candidate, bool fingerprintAssets, bool fingerprintDotNetJs) { + const string optionalFingerprint = "#[.{fingerprint}]?"; + const string requiredFingerprint = "#[.{fingerprint}]!"; + + string fileName = candidate.GetMetadata("FileName"); + string extension = candidate.GetMetadata("Extension"); + string subPath = string.Empty; + var destinationSubPath = candidate.GetMetadata("DestinationSubPath"); if (!string.IsNullOrEmpty(destinationSubPath)) - return $"_framework/{destinationSubPath}"; + { + fileName = Path.GetFileNameWithoutExtension(destinationSubPath); + extension = Path.GetExtension(destinationSubPath); + subPath = destinationSubPath.Substring(fileName.Length + extension.Length); + } + + string relativePath; + if (fingerprintAssets) + { + relativePath = (fileName, extension) switch + { + ("dotnet", ".js") => string.Concat(fileName, fingerprintDotNetJs ? requiredFingerprint : optionalFingerprint, extension), + ("dotnet.runtime", ".js") => string.Concat(fileName, requiredFingerprint, extension), + ("dotnet.native", ".js") => string.Concat(fileName, requiredFingerprint, extension), + ("dotnet.native.worker", ".mjs") => string.Concat(fileName, requiredFingerprint, extension), + ("dotnet.globalization", ".js") => string.Concat(fileName, requiredFingerprint, extension), + ("segmentation-rules", ".json") => string.Concat(fileName, requiredFingerprint, extension), + _ => string.Concat(fileName, extension) + }; + } + else + { + relativePath = string.Concat(fileName, extension); + } - var relativePath = candidate.GetMetadata("FileName") + candidate.GetMetadata("Extension"); - return $"_framework/{relativePath}"; + return $"_framework/{subPath}{relativePath}"; } - public static ITaskItem GetCustomIcuAsset(ITaskItem candidate) + public static ITaskItem GetCustomIcuAsset(ITaskItem candidate, bool fingerprintAssets) { var customIcuCandidate = new TaskItem(candidate); - var relativePath = GetCandidateRelativePath(customIcuCandidate); + var relativePath = GetCandidateRelativePath(customIcuCandidate, fingerprintAssets, false); customIcuCandidate.SetMetadata("RelativePath", relativePath); customIcuCandidate.SetMetadata("AssetTraitName", "BlazorWebAssemblyResource"); customIcuCandidate.SetMetadata("AssetTraitValue", "native"); diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs index 19c0d1f02e764..8df0f567148cb 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs @@ -125,6 +125,9 @@ public class ResourcesData /// public string hash { get; set; } + [DataMember(EmitDefaultValue = false)] + public Dictionary fingerprinting { get; set; } + /// /// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc. /// diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs index 6e2962593c3a3..e8daa16e60813 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs @@ -31,9 +31,6 @@ public class ComputeWasmBuildAssets : Task [Required] public ITaskItem[] ProjectSatelliteAssemblies { get; set; } - [Required] - public string DotNetJsVersion { get; set; } - [Required] public string OutputPath { get; set; } @@ -52,12 +49,14 @@ public class ComputeWasmBuildAssets : Task [Required] public bool CopySymbols { get; set; } - public bool FingerprintDotNetJs { get; set; } - public bool EnableThreads { get; set; } public bool EmitSourceMap { get; set; } + public bool FingerprintAssets { get; set; } + + public bool FingerprintDotNetJs { get; set; } + [Output] public ITaskItem[] AssetCandidates { get; set; } @@ -117,45 +116,8 @@ public override bool Execute() continue; } - string candidateFileName = candidate.GetMetadata("FileName"); - string extension = candidate.GetMetadata("Extension"); - if (candidateFileName.StartsWith("dotnet") && (extension == ".js" || extension == ".mjs")) - { - string newDotnetJSFileName = null; - string newDotNetJSFullPath = null; - if (candidateFileName != "dotnet" || FingerprintDotNetJs) - { - var itemHash = FileHasher.GetFileHash(candidate.ItemSpec); - newDotnetJSFileName = $"{candidateFileName}.{DotNetJsVersion}.{itemHash}{extension}"; - - var originalFileFullPath = Path.GetFullPath(candidate.ItemSpec); - var originalFileDirectory = Path.GetDirectoryName(originalFileFullPath); - - newDotNetJSFullPath = Path.Combine(originalFileDirectory, newDotnetJSFileName); - } - else - { - newDotNetJSFullPath = candidate.ItemSpec; - newDotnetJSFileName = Path.GetFileName(newDotNetJSFullPath); - } - - var newDotNetJs = new TaskItem(newDotNetJSFullPath, candidate.CloneCustomMetadata()); - newDotNetJs.SetMetadata("OriginalItemSpec", candidate.ItemSpec); - - var newRelativePath = $"_framework/{newDotnetJSFileName}"; - newDotNetJs.SetMetadata("RelativePath", newRelativePath); - - newDotNetJs.SetMetadata("AssetTraitName", "WasmResource"); - newDotNetJs.SetMetadata("AssetTraitValue", "native"); - - assetCandidates.Add(newDotNetJs); - continue; - } - else - { - string relativePath = AssetsComputingHelper.GetCandidateRelativePath(candidate); - candidate.SetMetadata("RelativePath", relativePath); - } + string relativePath = AssetsComputingHelper.GetCandidateRelativePath(candidate, FingerprintAssets, FingerprintDotNetJs); + candidate.SetMetadata("RelativePath", relativePath); // Workaround for https://github.com/dotnet/aspnetcore/issues/37574. // For items added as "Reference" in project references, the OriginalItemSpec is incorrect. @@ -265,6 +227,8 @@ private static void ApplyUniqueMetadataProperties(ITaskItem candidate) break; case ".wasm": case ".blat": + case ".js" when filename.StartsWith("dotnet"): + case ".mjs" when filename.StartsWith("dotnet"): case ".dat" when filename.StartsWith("icudt"): case ".json" when filename.StartsWith("segmentation-rules"): candidate.SetMetadata("AssetTraitName", "WasmResource"); diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs index 1255494e22e29..3ea146e92e6ed 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs @@ -55,20 +55,20 @@ public class ComputeWasmPublishAssets : Task [Required] public string PublishPath { get; set; } - [Required] - public string DotNetJsVersion { get; set; } - - public bool FingerprintDotNetJs { get; set; } - public bool EnableThreads { get; set; } public bool EmitSourceMap { get; set; } public bool IsWebCilEnabled { get; set; } + public bool FingerprintAssets { get; set; } + [Output] public ITaskItem[] NewCandidates { get; set; } + [Output] + public ITaskItem[] PromotedAssets { get; set; } + [Output] public ITaskItem[] FilesToRemove { get; set; } @@ -76,6 +76,7 @@ public override bool Execute() { var filesToRemove = new List(); var newAssets = new List(); + var promotedAssets = new List(); try { @@ -109,33 +110,32 @@ public override bool Execute() symbolAssets, compressedRepresentations); - var newStaticWebAssets = ComputeUpdatedAssemblies( + ComputeUpdatedAssemblies( satelliteAssemblyToPublish, filesToRemove, resolvedAssembliesToPublish, assemblyAssets, satelliteAssemblyAssets, - compressedRepresentations); - - newAssets.AddRange(newStaticWebAssets); + compressedRepresentations, + newAssets, + promotedAssets); - var nativeStaticWebAssets = ProcessNativeAssets( + ProcessNativeAssets( nativeAssets, resolvedFilesToPublishToRemove, resolvedNativeAssetToPublish, compressedRepresentations, - filesToRemove); - - newAssets.AddRange(nativeStaticWebAssets); + filesToRemove, + newAssets, + promotedAssets); - var symbolStaticWebAssets = ProcessSymbolAssets( + ProcessSymbolAssets( symbolAssets, compressedRepresentations, resolvedFilesToPublishToRemove, resolvedSymbolsToPublish, - filesToRemove); - - newAssets.AddRange(symbolStaticWebAssets); + filesToRemove, + promotedAssets); foreach (var kvp in resolvedFilesToPublishToRemove) { @@ -151,22 +151,28 @@ public override bool Execute() FilesToRemove = filesToRemove.ToArray(); NewCandidates = newAssets.ToArray(); + PromotedAssets = promotedAssets.ToArray(); return !Log.HasLoggedErrors; } - private List ProcessNativeAssets( + private void ProcessNativeAssets( Dictionary nativeAssets, IDictionary resolvedPublishFilesToRemove, Dictionary resolvedNativeAssetToPublish, Dictionary compressedRepresentations, - List filesToRemove) + List filesToRemove, + List newAssets, + List promotedAssets) { var nativeStaticWebAssets = new List(); // Keep track of the updated assets to determine what compressed assets we can reuse var updateMap = new Dictionary(); + // Keep track of not-fingerprinted asset mapped to fingerprinted. + var mappedFingerprintedAssets = new Dictionary(); + foreach (var kvp in nativeAssets) { var key = kvp.Key; @@ -182,11 +188,14 @@ private List ProcessNativeAssets( { // This is a native asset like timezones.blat or similar that was not filtered and that needs to be updated // to a publish asset. - var newAsset = new TaskItem(asset); - ApplyPublishProperties(newAsset); + ITaskItem newAsset = CreatePromotedAsset(asset); + if (newAsset.ItemSpec != asset.ItemSpec) + mappedFingerprintedAssets[asset.ItemSpec] = newAsset.ItemSpec; + nativeStaticWebAssets.Add(newAsset); filesToRemove.Add(existing); updateMap.Add(asset.ItemSpec, newAsset); + promotedAssets.Add(newAsset); Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec); } else @@ -224,22 +233,22 @@ private List ProcessNativeAssets( newDotNetJs = new TaskItem(Path.GetFullPath(aotDotNetJs.ItemSpec), asset.CloneCustomMetadata()); newDotNetJs.SetMetadata("OriginalItemSpec", aotDotNetJs.ItemSpec); - string relativePath = baseName != "dotnet" || FingerprintDotNetJs - ? $"_framework/{$"{baseName}.{DotNetJsVersion}.{FileHasher.GetFileHash(aotDotNetJs.ItemSpec)}{extension}"}" - : $"_framework/{baseName}{extension}"; - - newDotNetJs.SetMetadata("RelativePath", relativePath); + ApplyPublishProperties(newDotNetJs); updateMap.Add(asset.ItemSpec, newDotNetJs); + newAssets.Add(newDotNetJs); Log.LogMessage(MessageImportance.Low, "Replacing asset '{0}' with AoT version '{1}'", asset.ItemSpec, newDotNetJs.ItemSpec); } else { - newDotNetJs = new TaskItem(asset); + newDotNetJs = CreatePromotedAsset(asset); + if (newDotNetJs.ItemSpec != asset.ItemSpec) + mappedFingerprintedAssets[asset.ItemSpec] = newDotNetJs.ItemSpec; + + promotedAssets.Add(newDotNetJs); Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec); } - ApplyPublishProperties(newDotNetJs); nativeStaticWebAssets.Add(newDotNetJs); if (resolvedNativeAssetToPublish.TryGetValue($"{baseName}{extension}", out var resolved)) { @@ -260,16 +269,22 @@ private List ProcessNativeAssets( { newDotNetWasm = new TaskItem(Path.GetFullPath(aotDotNetWasm.ItemSpec), asset.CloneCustomMetadata()); newDotNetWasm.SetMetadata("OriginalItemSpec", aotDotNetWasm.ItemSpec); + ApplyPublishProperties(newDotNetWasm); + updateMap.Add(asset.ItemSpec, newDotNetWasm); + newAssets.Add(newDotNetWasm); Log.LogMessage(MessageImportance.Low, "Replacing asset '{0}' with AoT version '{1}'", asset.ItemSpec, newDotNetWasm.ItemSpec); } else { - newDotNetWasm = new TaskItem(asset); + newDotNetWasm = CreatePromotedAsset(asset); + if (newDotNetWasm.ItemSpec != asset.ItemSpec) + mappedFingerprintedAssets[asset.ItemSpec] = newDotNetWasm.ItemSpec; + + promotedAssets.Add(newDotNetWasm); Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec); } - ApplyPublishProperties(newDotNetWasm); nativeStaticWebAssets.Add(newDotNetWasm); if (resolvedNativeAssetToPublish.TryGetValue("dotnet.native.wasm", out var resolved)) @@ -287,10 +302,18 @@ private List ProcessNativeAssets( var compressedUpdatedFiles = ProcessCompressedAssets(compressedRepresentations, nativeAssets, updateMap); foreach (var f in compressedUpdatedFiles) { - nativeStaticWebAssets.Add(f); - } + var compressed = f; + if (mappedFingerprintedAssets.TryGetValue(compressed.GetMetadata("RelatedAsset"), out var fingerprintedAsset)) + { + Log.LogMessage(MessageImportance.Low, "Changing related asset for compressed asset '{0}' to '{1}'.", compressed.ItemSpec, fingerprintedAsset); + + compressed = new TaskItem(compressed); + compressed.SetMetadata("RelatedAsset", fingerprintedAsset); + } - return nativeStaticWebAssets; + promotedAssets.Add(compressed); + nativeStaticWebAssets.Add(compressed); + } static bool IsAnyDotNetJs(string key) { @@ -306,17 +329,45 @@ static bool IsDotNetWasm(string key) } } - private List ProcessSymbolAssets( + 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}"; + newAssetRelativePath = newAssetRelativePath.Replace(assetFileNameToFingerprint, newAssetFingerprintedFileName); + } + } + + var newAsset = new TaskItem(newAssetItemSpec, asset.CloneCustomMetadata()); + newAsset.SetMetadata("RelativePath", newAssetRelativePath); + + ApplyPublishProperties(newAsset); + return newAsset; + } + + private void ProcessSymbolAssets( Dictionary symbolAssets, Dictionary compressedRepresentations, Dictionary resolvedPublishFilesToRemove, Dictionary resolvedSymbolAssetToPublish, - List filesToRemove) + List filesToRemove, + List promotedAssets) { var symbolStaticWebAssets = new List(); var updateMap = new Dictionary(); var existingToRemove = new Dictionary(); + var mappedFingerprintedAssets = new Dictionary(); + foreach (var kvp in symbolAssets) { var asset = kvp.Value; @@ -326,11 +377,14 @@ private List ProcessSymbolAssets( { // This is a symbol asset like classlibrary.pdb or similar that was not filtered and that needs to be updated // to a publish asset. - var newAsset = new TaskItem(asset); - ApplyPublishProperties(newAsset); + var newAsset = CreatePromotedAsset(asset); + if (newAsset.ItemSpec != asset.ItemSpec) + mappedFingerprintedAssets[asset.ItemSpec] = newAsset.ItemSpec; + symbolStaticWebAssets.Add(newAsset); updateMap.Add(newAsset.ItemSpec, newAsset); filesToRemove.Add(existing); + promotedAssets.Add(newAsset); Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec); } else @@ -350,22 +404,31 @@ private List ProcessSymbolAssets( } var compressedFiles = ProcessCompressedAssets(compressedRepresentations, symbolAssets, updateMap, existingToRemove); - - foreach (var file in compressedFiles) + foreach (var f in compressedFiles) { - symbolStaticWebAssets.Add(file); - } + var compressed = f; + if (mappedFingerprintedAssets.TryGetValue(compressed.GetMetadata("RelatedAsset"), out var fingerprintedAsset)) + { + Log.LogMessage(MessageImportance.Low, "Changing related asset for compressed asset '{0}' to '{1}'.", compressed.ItemSpec, fingerprintedAsset); - return symbolStaticWebAssets; + compressed = new TaskItem(compressed); + compressed.SetMetadata("RelatedAsset", fingerprintedAsset); + } + + promotedAssets.Add(compressed); + symbolStaticWebAssets.Add(compressed); + } } - private List ComputeUpdatedAssemblies( + private void ComputeUpdatedAssemblies( IDictionary<(string, string assemblyName), ITaskItem> satelliteAssemblies, List filesToRemove, Dictionary resolvedAssembliesToPublish, Dictionary assemblyAssets, Dictionary satelliteAssemblyAssets, - Dictionary compressedRepresentations) + Dictionary compressedRepresentations, + List newAssets, + List promotedAssets) { // All assemblies, satellite assemblies and gzip files are initially defined as build assets. // We need to update them to publish assets when they haven't changed or when they have been linked. @@ -377,7 +440,7 @@ private List ComputeUpdatedAssemblies( foreach (var kvp in assemblyAssets) { var asset = kvp.Value; - var fileName = Path.GetFileName(asset.GetMetadata("RelativePath")); + var fileName = Path.GetFileName(asset.ItemSpec); if (IsWebCilEnabled) fileName = Path.ChangeExtension(fileName, ".dll"); @@ -391,6 +454,11 @@ private List ComputeUpdatedAssemblies( linkedAssets.Add(asset.ItemSpec, existing); } } + else + { + Log.LogMessage(MessageImportance.Low, "Asset '{0}' is not present in resolved files to publish and will be omitted from publish", + asset.ItemSpec); + } } foreach (var kvp in satelliteAssemblyAssets) @@ -401,7 +469,7 @@ private List ComputeUpdatedAssemblies( { assetsToUpdate.Add(satelliteAssembly.ItemSpec, satelliteAssembly); var culture = satelliteAssembly.GetMetadata("AssetTraitValue"); - var fileName = Path.GetFileName(satelliteAssembly.GetMetadata("RelativePath")); + var fileName = Path.GetFileName(satelliteAssembly.ItemSpec); if (IsWebCilEnabled) fileName = Path.ChangeExtension(fileName, ".dll"); @@ -448,21 +516,20 @@ private List ComputeUpdatedAssemblies( } ApplyPublishProperties(newAsemblyAsset); + newAssets.Add(newAsemblyAsset); updatedAssetsMap.Add(asset.ItemSpec, newAsemblyAsset); break; default: // Satellite assembliess and compressed assets - var dependentAsset = new TaskItem(asset); - ApplyPublishProperties(dependentAsset); - UpdateRelatedAssetProperty(asset, dependentAsset, updatedAssetsMap); + TaskItem newAsset = CreatePromotedAsset(asset); + UpdateRelatedAssetProperty(asset, newAsset, updatedAssetsMap); Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec); - updatedAssetsMap.Add(asset.ItemSpec, dependentAsset); + promotedAssets.Add(newAsset); + updatedAssetsMap.Add(asset.ItemSpec, newAsset); break; } } - - return updatedAssetsMap.Values.ToList(); } private List ProcessCompressedAssets( @@ -589,7 +656,7 @@ private void GroupResolvedFilesToPublish( var resolvedFilesToPublish = ResolvedFilesToPublish.ToList(); if (AssetsComputingHelper.TryGetAssetFilename(CustomIcuCandidate, out string customIcuCandidateFilename)) { - var customIcuCandidate = AssetsComputingHelper.GetCustomIcuAsset(CustomIcuCandidate); + var customIcuCandidate = AssetsComputingHelper.GetCustomIcuAsset(CustomIcuCandidate, FingerprintAssets); resolvedFilesToPublish.Add(customIcuCandidate); } diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs index 33f3debdd4ef8..63cee84d412a8 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs @@ -34,6 +34,9 @@ public class GenerateWasmBootJson : Task [Required] public ITaskItem[] Resources { get; set; } + [Required] + public ITaskItem[] Endpoints { get; set; } + [Required] public bool DebugBuild { get; set; } @@ -81,6 +84,8 @@ public class GenerateWasmBootJson : Task public bool IsMultiThreaded { get; set; } + public bool FingerprintAssets { get; set; } + public override bool Execute() { using var fileStream = File.Create(OutputPath); @@ -169,8 +174,14 @@ public void WriteBootJson(Stream output, string entryAssemblyName) // - ContentHash (e.g., "3448f339acf512448") if (Resources != null) { + var endpointByAsset = Endpoints.ToDictionary(e => e.GetMetadata("AssetFile")); + var remainingLazyLoadAssemblies = new List(LazyLoadedAssemblies ?? Array.Empty()); var resourceData = result.resources; + + if (FingerprintAssets) + resourceData.fingerprinting = new(); + foreach (var resource in Resources) { ResourceHashesByNameDictionary resourceList = null; @@ -180,10 +191,12 @@ public void WriteBootJson(Stream output, string entryAssemblyName) var fileExtension = resource.GetMetadata("Extension"); var assetTraitName = resource.GetMetadata("AssetTraitName"); var assetTraitValue = resource.GetMetadata("AssetTraitValue"); - var resourceName = Path.GetFileName(resource.GetMetadata("RelativePath")); + var resourceName = Path.GetFileName(resource.GetMetadata("OriginalItemSpec")); + var resourceRoute = Path.GetFileName(endpointByAsset[resource.ItemSpec].ItemSpec); if (TryGetLazyLoadedAssembly(resourceName, out var lazyLoad)) { + MapFingerprintedAsset(resourceData, resourceRoute, resourceName); Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a lazy loaded assembly.", resource.ItemSpec); remainingLazyLoadAssemblies.Remove(lazyLoad); resourceData.lazyAssembly ??= new ResourceHashesByNameDictionary(); @@ -191,11 +204,12 @@ public void WriteBootJson(Stream output, string entryAssemblyName) } else if (string.Equals("Culture", assetTraitName, StringComparison.OrdinalIgnoreCase)) { + MapFingerprintedAsset(resourceData, resourceRoute, resourceName); Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as satellite assembly with culture '{1}'.", resource.ItemSpec, assetTraitValue); resourceData.satelliteResources ??= new Dictionary(StringComparer.OrdinalIgnoreCase); if (!IsTargeting80OrLater()) - resourceName = assetTraitValue + "/" + resourceName; + resourceRoute = assetTraitValue + "/" + resourceRoute; if (!resourceData.satelliteResources.TryGetValue(assetTraitValue, out resourceList)) { @@ -205,6 +219,7 @@ public void WriteBootJson(Stream output, string entryAssemblyName) } else if (string.Equals("symbol", assetTraitValue, StringComparison.OrdinalIgnoreCase)) { + MapFingerprintedAsset(resourceData, resourceRoute, resourceName); if (TryGetLazyLoadedAssembly($"{fileName}.dll", out _) || TryGetLazyLoadedAssembly($"{fileName}{Utils.WebcilInWasmExtension}", out _)) { Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a lazy loaded symbols file.", resource.ItemSpec); @@ -229,6 +244,7 @@ public void WriteBootJson(Stream output, string entryAssemblyName) } else if (string.Equals("runtime", assetTraitValue, StringComparison.OrdinalIgnoreCase)) { + MapFingerprintedAsset(resourceData, resourceRoute, resourceName); if (IsTargeting90OrLater() && (IsAot || helper.IsCoreAssembly(resourceName))) { Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as core assembly.", resource.ItemSpec); @@ -243,6 +259,7 @@ public void WriteBootJson(Stream output, string entryAssemblyName) else if (string.Equals(assetTraitName, "WasmResource", StringComparison.OrdinalIgnoreCase) && string.Equals(assetTraitValue, "native", StringComparison.OrdinalIgnoreCase)) { + MapFingerprintedAsset(resourceData, resourceRoute, resourceName); Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a native application resource.", resource.ItemSpec); if (IsTargeting80OrLater()) @@ -264,7 +281,7 @@ public void WriteBootJson(Stream output, string entryAssemblyName) { Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a library initializer resource.", resource.ItemSpec); - var targetPath = resource.GetMetadata("TargetPath"); + var targetPath = endpointByAsset[resource.ItemSpec].ItemSpec; Debug.Assert(!string.IsNullOrEmpty(targetPath), "Target path for '{0}' must exist.", resource.ItemSpec); resourceList = resourceData.libraryInitializers ??= new ResourceHashesByNameDictionary(); @@ -324,13 +341,13 @@ public void WriteBootJson(Stream output, string entryAssemblyName) if (resourceList != null) { - AddResourceToList(resource, resourceList, resourceName); + AddResourceToList(resource, resourceList, resourceRoute); } if (!string.IsNullOrEmpty(behavior)) { resourceData.runtimeAssets ??= new Dictionary(); - AddToAdditionalResources(resource, resourceData.runtimeAssets, resourceName, behavior); + AddToAdditionalResources(resource, resourceData.runtimeAssets, resourceRoute, behavior); } } @@ -406,11 +423,19 @@ void AddResourceToList(ITaskItem resource, ResourceHashesByNameDictionary resour if (!resourceList.ContainsKey(resourceKey)) { Log.LogMessage(MessageImportance.Low, "Added resource '{0}' to the manifest.", resource.ItemSpec); - resourceList.Add(resourceKey, $"sha256-{resource.GetMetadata("FileHash")}"); + resourceList.Add(resourceKey, $"sha256-{resource.GetMetadata("Integrity")}"); } } } + private void MapFingerprintedAsset(ResourcesData resources, string resourceRoute, string resourceName) + { + if (!FingerprintAssets || !IsTargeting90OrLater()) + return; + + resources.fingerprinting[resourceRoute] = resourceName; + } + private GlobalizationMode GetGlobalizationMode() { if (string.Equals(InvariantGlobalization, "true", StringComparison.OrdinalIgnoreCase))