From 445d33491ba088421e16cada22b24ff19a75a913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 1 Apr 2026 14:46:33 +0000 Subject: [PATCH 01/13] Stop copying WASM assets to bin/wwwroot/_framework during build During build, the WebAssembly SDK was copying all .wasm and .js framework assets to bin/wwwroot/_framework/ via CopyToOutputDirectory=PreserveNewest. This is unnecessary because dotnet run uses the static web assets middleware, which serves files directly from their obj/ locations using the manifest. Change CopyToOutputDirectory from PreserveNewest to Never for: - Webcil-converted assets (Computed static web assets) - Materialized framework assets (dotnet.js, dotnet.native.wasm, etc.) This eliminates ~178 file copies during build while preserving correct behavior for dotnet run (static web assets middleware) and publish (CopyToPublishDirectory was already Never). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../build/Microsoft.NET.Sdk.WebAssembly.Browser.targets | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 171388aee4da54..2024fa7a8b401a 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 @@ -384,7 +384,7 @@ Copyright (c) .NET Foundation. All rights reserved. SourceType="Computed" AssetKind="Build" AssetRole="Primary" - CopyToOutputDirectory="PreserveNewest" + CopyToOutputDirectory="Never" CopyToPublishDirectory="Never" FingerprintCandidates="$(_WasmFingerprintAssets)" FingerprintPatterns="@(FingerprintPatterns);@(_WasmFingerprintPatterns)" @@ -403,7 +403,7 @@ Copyright (c) .NET Foundation. All rights reserved. SourceType="Framework" AssetKind="Build" AssetRole="Primary" - CopyToOutputDirectory="PreserveNewest" + CopyToOutputDirectory="Never" CopyToPublishDirectory="Never" FingerprintCandidates="$(_WasmFingerprintAssets)" FingerprintPatterns="@(FingerprintPatterns);@(_WasmFingerprintPatterns)" @@ -432,7 +432,7 @@ Copyright (c) .NET Foundation. All rights reserved. path (obj/fx/{SourceId}/) to bin/wwwroot/_framework/ at build time. --> <_WasmMaterializedFrameworkAssets Update="@(_WasmMaterializedFrameworkAssets)" - AssetMode="All" CopyToOutputDirectory="PreserveNewest" /> + AssetMode="All" CopyToOutputDirectory="Never" /> From 1037300dd801f1bea1704f0522b2de3a8b5b2358 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 3 Apr 2026 16:56:54 -0500 Subject: [PATCH 02/13] Skip bin/_framework/ bundle assertions for build With CopyToOutputDirectory=Never, framework assets are no longer copied to bin/wwwroot/_framework/ during build. They are served from obj/ locations via the static web assets middleware during dotnet run. Only assert bundle file layout for publish, where files are still physically copied to the output. Build-time tests that run the app (via dotnet run or xharness) still validate the app works correctly - they just don't check for files in bin/ that are intentionally no longer there. The Blazor-specific AssertBundle already had this guard WasmTemplateTestsBase path needed updating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs index 3a42708e1f4941..d9935f8ad046fd 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs @@ -255,7 +255,14 @@ public virtual (string projectDir, string buildOutput) BuildProject( if (buildOptions.AssertAppBundle) { - _provider.AssertWasmSdkBundle(configuration, buildOptions, IsUsingWorkloads, isNativeBuild, wasmFingerprintDotnetJs, res.Output); + // With CopyToOutputDirectory=Never for framework assets, build output + // no longer contains framework files in bin/_framework/. The static web + // assets middleware serves them from obj/ locations during dotnet run. + // Only assert bundle contents for publish, where files are still copied. + if (buildOptions.IsPublish) + { + _provider.AssertWasmSdkBundle(configuration, buildOptions, IsUsingWorkloads, isNativeBuild, wasmFingerprintDotnetJs, res.Output); + } } return (_projectDir, res.Output); } From b21ad54e9e098d03b842848fcc8f608148c9344b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Fri, 10 Apr 2026 08:50:12 +0000 Subject: [PATCH 03/13] Assert framework files in obj/ for MultiClientHostedBuild With CopyToOutputDirectory=Never, framework files are no longer copied to bin/_framework/ during build. Update MultiClientHostedBuildAndPublish to assert framework files exist in obj///fx// _framework/ for build, matching the materialization path used by UpdatePackageStaticWebAssets. Publish assertions remain unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../wasm/Wasm.Build.Tests/Blazor/MiscTests.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs index 8f0b6d3d7feee6..2ef979ff9f6c51 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs @@ -148,11 +148,17 @@ public void MultiClientHostedBuildAndPublish(Configuration config, bool publish) } else { - string client1Framework = Path.Combine(client1Dir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor, "wwwroot", "_framework"); - string client2Framework = Path.Combine(client2Dir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor, "wwwroot", "_framework"); - - Assert.True(Directory.Exists(client1Framework), $"Client1 framework dir missing: {client1Framework}"); - Assert.True(Directory.Exists(client2Framework), $"Client2 framework dir missing: {client2Framework}"); + // With CopyToOutputDirectory=Never, framework files are no longer copied to + // bin/_framework/ during build. UpdatePackageStaticWebAssets materializes them + // to obj///fx//_framework/ instead, and the static + // web assets middleware serves them from there during dotnet run. + string client1Framework = Path.Combine(client1Dir, "obj", config.ToString(), + DefaultTargetFrameworkForBlazor, "fx", "Client1", "_framework"); + string client2Framework = Path.Combine(client2Dir, "obj", config.ToString(), + DefaultTargetFrameworkForBlazor, "fx", "Client2", "_framework"); + + Assert.True(Directory.Exists(client1Framework), $"Client1 obj framework dir missing: {client1Framework}"); + Assert.True(Directory.Exists(client2Framework), $"Client2 obj framework dir missing: {client2Framework}"); var client1Files = Directory.GetFiles(client1Framework); var client2Files = Directory.GetFiles(client2Framework); From 41b60c3800f495d1fe13b86247b2819e1ae038b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Fri, 10 Apr 2026 10:02:38 +0000 Subject: [PATCH 04/13] Assert build bundle files in obj/ instead of skipping for non-publish Revert the IsPublish guard that skipped AssertWasmSdkBundle for builds. Instead, add AssertBuildBundle that validates framework files are in their correct obj/ subdirectories with CopyToOutputDirectory=Never: - dotnet.js (boot config) in obj/{config}/{tfm}/ - dotnet.runtime.js, maps, ICU in obj/{config}/{tfm}/fx/{name}/_framework/ - dotnet.native.* in fx/_framework/ (non-native) or wasm/for-build/ (native) - webcil assemblies in obj/{config}/{tfm}/webcil/ - framework files NOT in bin/_framework/ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Templates/WasmTemplateTestsBase.cs | 9 +- .../WasmSdkBasedProjectProvider.cs | 125 +++++++++++++++++- 2 files changed, 125 insertions(+), 9 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs index d9935f8ad046fd..3a42708e1f4941 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs @@ -255,14 +255,7 @@ public virtual (string projectDir, string buildOutput) BuildProject( if (buildOptions.AssertAppBundle) { - // With CopyToOutputDirectory=Never for framework assets, build output - // no longer contains framework files in bin/_framework/. The static web - // assets middleware serves them from obj/ locations during dotnet run. - // Only assert bundle contents for publish, where files are still copied. - if (buildOptions.IsPublish) - { - _provider.AssertWasmSdkBundle(configuration, buildOptions, IsUsingWorkloads, isNativeBuild, wasmFingerprintDotnetJs, res.Output); - } + _provider.AssertWasmSdkBundle(configuration, buildOptions, IsUsingWorkloads, isNativeBuild, wasmFingerprintDotnetJs, res.Output); } return (_projectDir, res.Output); } diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs index 213960b59d7f82..5ea80bf803b797 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Collections.Generic; +using Microsoft.NET.Sdk.WebAssembly; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; @@ -178,7 +179,129 @@ public void AssertWasmSdkBundle(Configuration config, MSBuildOptions buildOption // In no-workload case, the path would be from a restored nuget ProjectProviderBase.AssertRuntimePackPath(buildOutput, buildOptions.TargetFramework ?? DefaultTargetFramework, buildOptions.RuntimeType); } - AssertBundle(config, buildOptions, isUsingWorkloads, isNativeBuild, wasmFingerprintDotnetJs); + + if (buildOptions.IsPublish) + { + AssertBundle(config, buildOptions, isUsingWorkloads, isNativeBuild, wasmFingerprintDotnetJs); + } + else if (string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir)) + { + AssertBuildBundle(config, buildOptions, isUsingWorkloads, isNativeBuild); + } + // When NonDefaultFrameworkDir is set (e.g. UseArtifactsOutput), the obj/ layout + // may not follow the standard path convention, so skip build bundle assertions. + } + + /// + /// Asserts that build-time framework assets are in their expected obj/ subdirectories + /// and NOT in the wrong directories. With CopyToOutputDirectory=Never, framework files + /// live in obj/ subdirectories instead of being collected in bin/_framework/: + /// + /// obj/{config}/{tfm}/ → dotnet.js (Computed, boot config entry point) + /// obj/{config}/{tfm}/fx/{name}/_framework/ → all materialized framework files: dotnet.runtime.js, dotnet.native.*, ICU, maps + /// obj/{config}/{tfm}/webcil/ → assembly .wasm files (webcil-converted) + /// obj/{config}/{tfm}/wasm/for-build/ → native assets only when native build (AOT/relink) + /// + private void AssertBuildBundle(Configuration config, MSBuildOptions buildOptions, bool isUsingWorkloads, bool? isNativeBuild) + { + EnsureProjectDirIsSet(); + + string tfm = buildOptions.TargetFramework; + string objDir = Path.Combine(ProjectDir!, "obj", config.ToString(), tfm); + string webcilDir = Path.Combine(objDir, "webcil"); + + // Discover the materialized framework directory: obj/{config}/{tfm}/fx/{ProjectName}/_framework/ + string fxBaseDir = Path.Combine(objDir, "fx"); + Assert.True(Directory.Exists(fxBaseDir), $"Expected materialized framework base directory: {fxBaseDir}"); + string[] fxSubDirs = Directory.GetDirectories(fxBaseDir); + Assert.True(fxSubDirs.Length == 1, $"Expected exactly one subdirectory under {fxBaseDir}, found: {string.Join(", ", fxSubDirs.Select(Path.GetFileName))}"); + string fxFrameworkDir = Path.Combine(fxSubDirs[0], "_framework"); + Assert.True(Directory.Exists(fxFrameworkDir), $"Expected materialized framework dir: {fxFrameworkDir}"); + + // --- Computed assets: dotnet.js lives in objDir root --- + AssertFileExists(objDir, "dotnet.js"); + AssertFileNotExists(fxFrameworkDir, "dotnet.js", "fx/_framework"); + + // --- Materialized framework assets: JS modules and source maps in fx/_framework/ --- + string[] materializedFiles = ["dotnet.runtime.js", "dotnet.runtime.js.map", "dotnet.js.map"]; + if (buildOptions.EnableDiagnostics || EnvironmentVariables.RuntimeFlavor == "CoreCLR") + { + materializedFiles = [.. materializedFiles, "dotnet.diagnostics.js", "dotnet.diagnostics.js.map"]; + } + foreach (string file in materializedFiles) + { + AssertFileExists(fxFrameworkDir, file); + AssertFileNotExists(objDir, file, "obj root"); + } + + // --- Native assets: dotnet.native.* --- + // For non-native builds, native files are materialized in fx/_framework/ from runtime pack. + // For native builds (AOT/relink), they are rebuilt and placed in wasm/for-build/. + string[] nativeFiles = ["dotnet.native.js", "dotnet.native.wasm"]; + var expectedFileType = isUsingWorkloads + ? GetExpectedFileType(config, buildOptions.AOT, isPublish: false, isUsingWorkloads, isNativeBuild) + : NativeFilesType.FromRuntimePack; + bool isNativeRebuild = expectedFileType is NativeFilesType.Relinked or NativeFilesType.AOT; + string nativeDir = isNativeRebuild + ? Path.Combine(objDir, "wasm", "for-build") + : fxFrameworkDir; + string nativeDirLabel = isNativeRebuild ? "wasm/for-build" : "fx/_framework"; + + foreach (string file in nativeFiles) + { + AssertFileExists(nativeDir, file); + AssertFileNotExists(objDir, file, "obj root"); + if (!isNativeRebuild) + AssertFileNotExists(Path.Combine(objDir, "wasm", "for-build"), file, "wasm/for-build"); + } + + // --- Webcil-converted assemblies in webcil/ --- + Assert.True(Directory.Exists(webcilDir), $"Expected webcil directory: {webcilDir}"); + string spcFile = "System.Private.CoreLib" + (BuildTestBase.UseWebcil ? ".wasm" : ".dll"); + AssertFileExists(webcilDir, spcFile); + AssertFileNotExists(fxFrameworkDir, spcFile, "fx/_framework"); + + // --- Boot config: parse from obj/dotnet.js to validate boot JSON is well-formed --- + string bootConfigPath = GetBootConfigPath(objDir, "dotnet.js"); + BootJsonData bootJson = GetBootJson(bootConfigPath); + Assert.NotNull(bootJson.resources); + + // --- Framework files (native, runtime, assemblies) must NOT be in bin/_framework/ --- + // dotnet.js (boot config) IS expected in bin/_framework/ since it keeps CopyToOutputDirectory=PreserveNewest. + string binFrameworkDir = GetBinFrameworkDir(config, forPublish: false, tfm); + if (Directory.Exists(binFrameworkDir)) + { + foreach (string file in nativeFiles.Concat(materializedFiles)) + { + AssertFileNotExists(binFrameworkDir, file, "bin/_framework"); + } + } + + // --- Native file comparison against runtime pack --- + if (isUsingWorkloads) + { + string runtimeNativeDir = BuildTestBase.s_buildEnv.GetRuntimeNativeDir(tfm, buildOptions.RuntimeType); + foreach (string nativeFilename in nativeFiles) + { + string actualPath = Path.Combine(nativeDir, nativeFilename); + if (expectedFileType == NativeFilesType.FromRuntimePack) + { + TestUtils.AssertSameFile(Path.Combine(runtimeNativeDir, nativeFilename), actualPath, "build"); + } + } + } + } + + private static void AssertFileExists(string dir, string filename) + { + Assert.True(File.Exists(Path.Combine(dir, filename)), + $"Expected {filename} in {dir}"); + } + + private static void AssertFileNotExists(string dir, string filename, string dirLabel) + { + Assert.False(File.Exists(Path.Combine(dir, filename)), + $"Did not expect {filename} in {dirLabel} ({dir})"); } public BuildPaths GetBuildPaths(Configuration configuration, bool forPublish, string? projectDir = null) From c5040e2b6bbecdbc50320e05d37f941a21b767d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 15 Apr 2026 11:42:38 +0000 Subject: [PATCH 05/13] Fix AssertBuildBundle for NoWebcil configs and satellite test paths - Gate webcil directory assertion on UseWebcil; when disabled, assemblies are materialized as DLLs in fx/_framework/ instead of webcil/. - Update SatelliteAssembliesFromPackageReference to check obj/ paths (webcil/{locale}/ or fx/{name}/_framework/{locale}/) instead of bin/_framework/ which is no longer populated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Wasm.Build.Tests/SatelliteLoadingTests.cs | 23 +++++++++++++++---- .../WasmSdkBasedProjectProvider.cs | 18 +++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs index 10976b41087d3e..23c3487ee07263 100644 --- a/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs @@ -110,15 +110,30 @@ public void SatelliteAssembliesFromPackageReference() ProjectInfo info = CopyTestAsset(config, false, TestAsset.WasmBasicTestApp, "SatelliteLoadingTestsFromPackageReference"); BuildProject(info, config, new BuildOptions(ExtraMSBuildArgs: "-p:TestSatelliteAssembliesFromPackage=true")); - string binFrameworkDir = GetBinFrameworkDir(config, forPublish: false); + // With CopyToOutputDirectory=Never, satellite assemblies are no longer in + // bin/_framework/. When webcil is enabled they're in obj/{config}/{tfm}/webcil/{locale}/; + // when disabled they're materialized in obj/{config}/{tfm}/fx/{name}/_framework/{locale}/. + string objDir = Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFramework); + string satelliteBaseDir; + if (UseWebcil) + { + satelliteBaseDir = Path.Combine(objDir, "webcil"); + } + else + { + string fxBaseDir = Path.Combine(objDir, "fx"); + string[] fxSubDirs = Directory.GetDirectories(fxBaseDir); + Assert.True(fxSubDirs.Length == 1, + $"Expected exactly one subdirectory under {fxBaseDir}, found: {string.Join(", ", fxSubDirs.Select(Path.GetFileName))}"); + satelliteBaseDir = Path.Combine(fxSubDirs[0], "_framework"); + } // Microsoft.CodeAnalysis.CSharp has satellite assemblies for multiple locales - // Verify that at least some of them are present in the AppBundle string[] expectedLocales = ["cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant"]; foreach (string locale in expectedLocales) { - string satelliteDir = Path.Combine(binFrameworkDir, locale); - Assert.True(Directory.Exists(satelliteDir), $"Expected satellite directory '{locale}' to exist in {binFrameworkDir}"); + string satelliteDir = Path.Combine(satelliteBaseDir, locale); + Assert.True(Directory.Exists(satelliteDir), $"Expected satellite directory '{locale}' to exist in {satelliteBaseDir}"); string[] satelliteFiles = Directory.GetFiles(satelliteDir, "Microsoft.CodeAnalysis.CSharp.resources*"); Assert.True(satelliteFiles.Length > 0, $"Expected Microsoft.CodeAnalysis.CSharp.resources.dll in {satelliteDir}"); diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs index 5ea80bf803b797..62032523b4b7f1 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -255,11 +255,19 @@ private void AssertBuildBundle(Configuration config, MSBuildOptions buildOptions AssertFileNotExists(Path.Combine(objDir, "wasm", "for-build"), file, "wasm/for-build"); } - // --- Webcil-converted assemblies in webcil/ --- - Assert.True(Directory.Exists(webcilDir), $"Expected webcil directory: {webcilDir}"); - string spcFile = "System.Private.CoreLib" + (BuildTestBase.UseWebcil ? ".wasm" : ".dll"); - AssertFileExists(webcilDir, spcFile); - AssertFileNotExists(fxFrameworkDir, spcFile, "fx/_framework"); + // --- Assembly files: webcil-converted in webcil/ or materialized DLLs in fx/_framework/ --- + if (BuildTestBase.UseWebcil) + { + Assert.True(Directory.Exists(webcilDir), $"Expected webcil directory: {webcilDir}"); + AssertFileExists(webcilDir, "System.Private.CoreLib.wasm"); + AssertFileNotExists(fxFrameworkDir, "System.Private.CoreLib.wasm", "fx/_framework"); + } + else + { + // When webcil is disabled, assembly DLLs are framework pass-through candidates + // and get materialized alongside other framework files in fx/_framework/. + AssertFileExists(fxFrameworkDir, "System.Private.CoreLib.dll"); + } // --- Boot config: parse from obj/dotnet.js to validate boot JSON is well-formed --- string bootConfigPath = GetBootConfigPath(objDir, "dotnet.js"); From f2395713d66e8d4929dba8bed98f389870ce3e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 15 Apr 2026 11:49:32 +0000 Subject: [PATCH 06/13] Apply PR review feedback: fix comment, named args, remove unused var - Update targets comment to reflect CopyToOutputDirectory=Never behavior - Use named arguments for isUsingWorkloads/isNativeBuild parameters - Remove unused nativeDirLabel variable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost | 1 + .../build/Microsoft.NET.Sdk.WebAssembly.Browser.targets | 6 +++--- .../wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 120000 src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost new file mode 120000 index 00000000000000..8ec4c510fc1ffb --- /dev/null +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost @@ -0,0 +1 @@ +/workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug \ No newline at end of file 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 f04586d8948e94..f187c46725077d 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 @@ -427,9 +427,9 @@ Copyright (c) .NET Foundation. All rights reserved. + to Never. Override AssetMode=All so assets flow through project references. Framework + files stay in obj/fx/{SourceId}/ and are served by the static web assets middleware + during dotnet run — they are not copied to bin/wwwroot/_framework/ at build time. --> <_WasmMaterializedFrameworkAssets Update="@(_WasmMaterializedFrameworkAssets)" AssetMode="All" CopyToOutputDirectory="Never" /> diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs index 62032523b4b7f1..0f8c0c9323a025 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -239,13 +239,12 @@ private void AssertBuildBundle(Configuration config, MSBuildOptions buildOptions // For native builds (AOT/relink), they are rebuilt and placed in wasm/for-build/. string[] nativeFiles = ["dotnet.native.js", "dotnet.native.wasm"]; var expectedFileType = isUsingWorkloads - ? GetExpectedFileType(config, buildOptions.AOT, isPublish: false, isUsingWorkloads, isNativeBuild) + ? GetExpectedFileType(config, buildOptions.AOT, isPublish: false, isUsingWorkloads: isUsingWorkloads, isNativeBuild: isNativeBuild) : NativeFilesType.FromRuntimePack; bool isNativeRebuild = expectedFileType is NativeFilesType.Relinked or NativeFilesType.AOT; string nativeDir = isNativeRebuild ? Path.Combine(objDir, "wasm", "for-build") : fxFrameworkDir; - string nativeDirLabel = isNativeRebuild ? "wasm/for-build" : "fx/_framework"; foreach (string file in nativeFiles) { From 6815cdb61e80ab24848939f9f2442cf6f4cdf3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 15 Apr 2026 11:55:49 +0000 Subject: [PATCH 07/13] Add .wasm assertion to multi-client build path Restore dotnet.native.wasm check in the build branch of MultiClientHostedBuildAndPublish to match the publish branch, ensuring both clients have native assets materialized independently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs index 2ef979ff9f6c51..2cf8865b683e5e 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs @@ -164,6 +164,8 @@ public void MultiClientHostedBuildAndPublish(Configuration config, bool publish) var client2Files = Directory.GetFiles(client2Framework); Assert.Contains(client1Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js")); Assert.Contains(client2Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js")); + Assert.Contains(client1Files, f => Path.GetFileName(f).Contains("dotnet.native") && f.EndsWith(".wasm")); + Assert.Contains(client2Files, f => Path.GetFileName(f).Contains("dotnet.native") && f.EndsWith(".wasm")); } } } From 07d1d49d2c704d17862a89932000fe049e3c6c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Wed, 15 Apr 2026 17:28:09 +0000 Subject: [PATCH 08/13] Apply review feedback: remove accidental symlink, validate multi-threaded worker - Remove accidentally committed WasmAppHost symlink pointing to local Codespace build artifacts path - Add dotnet.native.worker.mjs validation in AssertBuildBundle for multi-threaded build scenarios to match publish-path coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost | 1 - .../wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 120000 src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost deleted file mode 120000 index 8ec4c510fc1ffb..00000000000000 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost +++ /dev/null @@ -1 +0,0 @@ -/workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug \ No newline at end of file diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs index 0f8c0c9323a025..f6454bfb35c426 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -254,6 +254,15 @@ private void AssertBuildBundle(Configuration config, MSBuildOptions buildOptions AssertFileNotExists(Path.Combine(objDir, "wasm", "for-build"), file, "wasm/for-build"); } + if (buildOptions.RuntimeType == RuntimeVariant.MultiThreaded) + { + const string multiThreadedWorkerFile = "dotnet.native.worker.mjs"; + AssertFileExists(nativeDir, multiThreadedWorkerFile); + AssertFileNotExists(objDir, multiThreadedWorkerFile, "obj root"); + if (!isNativeRebuild) + AssertFileNotExists(Path.Combine(objDir, "wasm", "for-build"), multiThreadedWorkerFile, "wasm/for-build"); + } + // --- Assembly files: webcil-converted in webcil/ or materialized DLLs in fx/_framework/ --- if (BuildTestBase.UseWebcil) { From 9e284fd11f9551790f2b34daab8193e96b73e4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Fri, 17 Apr 2026 10:24:51 +0000 Subject: [PATCH 09/13] Fix wasm build tests for CopyToOutputDirectory=Never With framework assets no longer copied to bin/_framework/ during build, update the three remaining failing tests to look for files in the obj subdirectories where they actually live: - Blazor BuildPublishTests.DefaultTemplate_WithResources_Publish: probe satellite assemblies under obj/{config}/{tfm}/webcil/{locale}/ (webcil) or obj/{config}/{tfm}/fx/{name}/_framework/{locale}/ (non-webcil) for the build-time assertion. - BuildPublishTests.BuildThenPublishWithAOT: stat framework files for the first build from obj/{config}/{tfm}/fx/{name}/_framework/ (JS, source maps) and obj/{config}/{tfm}/wasm/for-build/ (native outputs). Boot config is read from obj/{config}/{tfm}/dotnet.js via a new optional bootConfigDir parameter on GetFilesTable used by fingerprint resolution. - ModuleConfigTests.SymbolMapFileEmitted(isPublish=false): search for dotnet.native*.js.symbols in obj/wasm/for-build/ (native rebuild) and in obj/fx/*/_framework/ as a fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Blazor/BuildPublishTests.cs | 20 ++++++++++-- .../Wasm.Build.Tests/BuildPublishTests.cs | 24 +++++++++++++- .../Wasm.Build.Tests/ModuleConfigTests.cs | 32 ++++++++++++++++--- .../Wasm.Build.Tests/ProjectProviderBase.cs | 4 +-- .../Templates/WasmTemplateTestsBase.cs | 4 +-- 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs index b1e19c334fc624..3a3403054fa4ed 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs @@ -95,14 +95,30 @@ public void DefaultTemplate_WithResources_Publish(Configuration config) Utils.DirectoryCopy(resxSourcePath, Path.Combine(_projectDir, "resx")); - // Build and assert resource dlls + // Build and assert resource dlls. With CopyToOutputDirectory=Never, framework files + // (including satellite assemblies) are no longer copied to bin/_framework/ during build. + // Webcil-converted assemblies live under obj/{config}/{tfm}/webcil/{locale}/; when webcil + // is disabled they're materialized under obj/{config}/{tfm}/fx/{name}/_framework/{locale}/. BlazorBuild(info, config); - AssertResourcesDlls(GetBlazorBinFrameworkDir(config, forPublish: false)); + AssertResourcesDlls(GetBuildSatelliteBaseDir()); // Publish and assert resource dlls BlazorPublish(info, config, new PublishOptions(UseCache: false)); AssertResourcesDlls(GetBlazorBinFrameworkDir(config, forPublish: true)); + string GetBuildSatelliteBaseDir() + { + string objDir = Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFrameworkForBlazor); + if (UseWebcil) + return Path.Combine(objDir, "webcil"); + + string fxBaseDir = Path.Combine(objDir, "fx"); + string[] fxSubDirs = Directory.GetDirectories(fxBaseDir); + Assert.True(fxSubDirs.Length == 1, + $"Expected exactly one subdirectory under {fxBaseDir}, found: {string.Join(", ", fxSubDirs.Select(Path.GetFileName))}"); + return Path.Combine(fxSubDirs[0], "_framework"); + } + void AssertResourcesDlls(string basePath) { foreach (string culture in cultures) diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs index 23dd76ebce99f2..1ba7a8fcd257ac 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs @@ -60,8 +60,30 @@ public async Task BuildThenPublishWithAOT(Configuration config, bool aot) (_, string output) = BuildProject(info, config, new BuildOptions(Label: "first_build", AOT: aot), isNativeBuild: aot); BuildPaths paths = GetBuildPaths(config, forPublish: isPublish); + // With CopyToOutputDirectory=Never, framework files aren't copied to bin/_framework/ + // during build. They live in obj subdirs: dotnet.native.* in obj/wasm/for-build/ + // (for native rebuilds like Release+AOT), and JS/source maps in obj/{config}/{tfm}/fx/{name}/_framework/. + // The boot config (dotnet.js) is at obj/{config}/{tfm}/dotnet.js. + string fxBaseDir = Path.Combine(paths.ObjDir, "fx"); + string[] fxSubDirs = Directory.GetDirectories(fxBaseDir); + Assert.True(fxSubDirs.Length == 1, + $"Expected exactly one subdirectory under {fxBaseDir}, found: {string.Join(", ", fxSubDirs.Select(Path.GetFileName))}"); + string fxFrameworkDir = Path.Combine(fxSubDirs[0], "_framework"); + + BuildPaths buildObjPaths = paths with { BinFrameworkDir = fxFrameworkDir }; IDictionary pathsDict = - GetFilesTable(info.ProjectName, aot, paths, unchanged: false); + GetFilesTable(info.ProjectName, aot, buildObjPaths, unchanged: false, bootConfigDir: paths.ObjDir); + + // dotnet.native.* are produced by the native rebuild into obj/wasm/for-build/ using + // their canonical (non-fingerprinted) names — fingerprinting is applied later when + // publishing to bin. + foreach (var nativeName in new[] { "dotnet.native.wasm", "dotnet.native.js" }) + { + if (pathsDict.TryGetValue(nativeName, out var entry)) + { + pathsDict[nativeName] = (Path.Combine(paths.ObjWasmDir, nativeName), entry.unchanged); + } + } string mainDll = $"{info.ProjectName}.dll"; var firstBuildStat = StatFiles(pathsDict); diff --git a/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs b/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs index 699cc706756a8a..3759d4819b11e7 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs @@ -120,11 +120,33 @@ private void SymbolMapFileEmittedCore(bool emitSymbolMap, bool isPublish) else BuildProject(info, config, new BuildOptions(AssertAppBundle: false)); - string frameworkDir = GetBinFrameworkDir(config, forPublish: isPublish); - - // The file may be fingerprinted (e.g. dotnet.native..js.symbols), - // so use a glob pattern to find it. - bool symbolsFileExists = Directory.EnumerateFiles(frameworkDir, "dotnet.native*.js.symbols").Any(); + // Locate the emitted symbols file. With CopyToOutputDirectory=Never, framework files are + // no longer in bin/_framework during build: the native symbols file lives in + // obj/{config}/{tfm}/wasm/for-build/ (native rebuild) and the materialized copy ends up + // in obj/{config}/{tfm}/fx/{name}/_framework/. The publish path still has them in + // bin/{config}/{tfm}/publish/wwwroot/_framework/. + // The file may be fingerprinted (e.g. dotnet.native..js.symbols), so use a glob. + const string symbolsPattern = "dotnet.native*.js.symbols"; + bool symbolsFileExists; + if (isPublish) + { + string frameworkDir = GetBinFrameworkDir(config, forPublish: true); + symbolsFileExists = Directory.EnumerateFiles(frameworkDir, symbolsPattern).Any(); + } + else + { + string objDir = Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFramework); + string[] searchDirs = [ + Path.Combine(objDir, "wasm", "for-build"), + .. Directory.Exists(Path.Combine(objDir, "fx")) + ? Directory.GetDirectories(Path.Combine(objDir, "fx")) + .Select(d => Path.Combine(d, "_framework")) + : Array.Empty() + ]; + symbolsFileExists = searchDirs + .Where(Directory.Exists) + .Any(d => Directory.EnumerateFiles(d, symbolsPattern).Any()); + } Assert.Equal(emitSymbolMap, symbolsFileExists); } } diff --git a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs index 6e1ecfe6e8e8e6..ceec74289c4f90 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs @@ -338,7 +338,7 @@ private string[] GetFilesMatchingNameConsideringFingerprinting(string filePath, return dict; } - public IDictionary GetFilesTable(string projectName, bool isAOT, BuildPaths paths, bool unchanged) + public IDictionary GetFilesTable(string projectName, bool isAOT, BuildPaths paths, bool unchanged, string? bootConfigDir = null) { List files = new() { @@ -379,7 +379,7 @@ private string[] GetFilesMatchingNameConsideringFingerprinting(string filePath, if (IsFingerprintingEnabled) { - string bootJsonPath = GetBootConfigPath(paths.BinFrameworkDir, "dotnet.js"); + string bootJsonPath = GetBootConfigPath(bootConfigDir ?? paths.BinFrameworkDir, "dotnet.js"); BootJsonData bootJson = GetBootJson(bootJsonPath); AssetsData assets = (AssetsData)bootJson.resources; var keysToUpdate = new List(); diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs index 3a42708e1f4941..e01779346a2c93 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs @@ -488,8 +488,8 @@ public string GetObjDir(Configuration config, string? framework = null, string? public BuildPaths GetBuildPaths(Configuration config, bool forPublish, string? projectDir = null) => _provider.GetBuildPaths(config, forPublish, projectDir); - public IDictionary GetFilesTable(string projectName, bool isAOT, BuildPaths paths, bool unchanged) => - _provider.GetFilesTable(projectName, isAOT, paths, unchanged); + public IDictionary GetFilesTable(string projectName, bool isAOT, BuildPaths paths, bool unchanged, string? bootConfigDir = null) => + _provider.GetFilesTable(projectName, isAOT, paths, unchanged, bootConfigDir); public IDictionary StatFiles(IDictionary fullpaths) => _provider.StatFiles(fullpaths); From 00635acdf36ccbabfda2d4e40c94b0c800dc1b19 Mon Sep 17 00:00:00 2001 From: maraf Date: Fri, 17 Apr 2026 11:29:11 +0000 Subject: [PATCH 10/13] Extract materialized framework dir discovery into shared helper Addresses review feedback: centralizes the obj/.../fx//_framework/ discovery logic in WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir and uses it from AssertBuildBundle, BuildPublishTests and Blazor/MiscTests so the per-project fx subdir name is discovered instead of hardcoded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../wasm/Wasm.Build.Tests/Blazor/MiscTests.cs | 17 +++++------ .../Wasm.Build.Tests/BuildPublishTests.cs | 8 ++--- .../WasmSdkBasedProjectProvider.cs | 29 ++++++++++++++----- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs index 2cf8865b683e5e..f3e7ef895fb32e 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs @@ -150,15 +150,14 @@ public void MultiClientHostedBuildAndPublish(Configuration config, bool publish) { // With CopyToOutputDirectory=Never, framework files are no longer copied to // bin/_framework/ during build. UpdatePackageStaticWebAssets materializes them - // to obj///fx//_framework/ instead, and the static - // web assets middleware serves them from there during dotnet run. - string client1Framework = Path.Combine(client1Dir, "obj", config.ToString(), - DefaultTargetFrameworkForBlazor, "fx", "Client1", "_framework"); - string client2Framework = Path.Combine(client2Dir, "obj", config.ToString(), - DefaultTargetFrameworkForBlazor, "fx", "Client2", "_framework"); - - Assert.True(Directory.Exists(client1Framework), $"Client1 obj framework dir missing: {client1Framework}"); - Assert.True(Directory.Exists(client2Framework), $"Client2 obj framework dir missing: {client2Framework}"); + // under obj///fx//_framework/ instead (the source-id + // folder name comes from static web assets metadata and isn't necessarily the + // project directory name), and the static web assets middleware serves them + // from there during dotnet run. + string client1ObjDir = Path.Combine(client1Dir, "obj", config.ToString(), DefaultTargetFrameworkForBlazor); + string client2ObjDir = Path.Combine(client2Dir, "obj", config.ToString(), DefaultTargetFrameworkForBlazor); + string client1Framework = WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(client1ObjDir); + string client2Framework = WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(client2ObjDir); var client1Files = Directory.GetFiles(client1Framework); var client2Files = Directory.GetFiles(client2Framework); diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs index 1ba7a8fcd257ac..439cd256a5fee2 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs @@ -62,13 +62,9 @@ public async Task BuildThenPublishWithAOT(Configuration config, bool aot) BuildPaths paths = GetBuildPaths(config, forPublish: isPublish); // With CopyToOutputDirectory=Never, framework files aren't copied to bin/_framework/ // during build. They live in obj subdirs: dotnet.native.* in obj/wasm/for-build/ - // (for native rebuilds like Release+AOT), and JS/source maps in obj/{config}/{tfm}/fx/{name}/_framework/. + // (for native rebuilds like Release+AOT), and JS/source maps in obj/{config}/{tfm}/fx/{source-id}/_framework/. // The boot config (dotnet.js) is at obj/{config}/{tfm}/dotnet.js. - string fxBaseDir = Path.Combine(paths.ObjDir, "fx"); - string[] fxSubDirs = Directory.GetDirectories(fxBaseDir); - Assert.True(fxSubDirs.Length == 1, - $"Expected exactly one subdirectory under {fxBaseDir}, found: {string.Join(", ", fxSubDirs.Select(Path.GetFileName))}"); - string fxFrameworkDir = Path.Combine(fxSubDirs[0], "_framework"); + string fxFrameworkDir = WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(paths.ObjDir); BuildPaths buildObjPaths = paths with { BinFrameworkDir = fxFrameworkDir }; IDictionary pathsDict = diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs index f6454bfb35c426..7448e2fc76b5af 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -26,6 +26,26 @@ public WasmSdkBasedProjectProvider(ITestOutputHelper _testOutput, string default protected override string BundleDirName { get { return "wwwroot"; } } + /// + /// Discovers the single materialized framework directory produced by + /// UpdatePackageStaticWebAssets for a built (non-published) project: + /// /fx/<source-id>/_framework/. + /// + /// The per-project folder name under obj/.../fx/ is derived from static web assets + /// metadata (SourceId / PackageId), so tests should not assume it matches the + /// project directory name. + /// + public static string GetMaterializedFrameworkDir(string objDir) + { + string fxBaseDir = Path.Combine(objDir, "fx"); + Assert.True(Directory.Exists(fxBaseDir), $"Expected materialized framework base directory: {fxBaseDir}"); + string[] fxSubDirs = Directory.GetDirectories(fxBaseDir); + Assert.True(fxSubDirs.Length == 1, $"Expected exactly one subdirectory under {fxBaseDir}, found: {string.Join(", ", fxSubDirs.Select(Path.GetFileName))}"); + string fxFrameworkDir = Path.Combine(fxSubDirs[0], "_framework"); + Assert.True(Directory.Exists(fxFrameworkDir), $"Expected materialized framework dir: {fxFrameworkDir}"); + return fxFrameworkDir; + } + protected override IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(AssertBundleOptions assertOptions) { var result = new SortedDictionary() @@ -210,13 +230,8 @@ private void AssertBuildBundle(Configuration config, MSBuildOptions buildOptions string objDir = Path.Combine(ProjectDir!, "obj", config.ToString(), tfm); string webcilDir = Path.Combine(objDir, "webcil"); - // Discover the materialized framework directory: obj/{config}/{tfm}/fx/{ProjectName}/_framework/ - string fxBaseDir = Path.Combine(objDir, "fx"); - Assert.True(Directory.Exists(fxBaseDir), $"Expected materialized framework base directory: {fxBaseDir}"); - string[] fxSubDirs = Directory.GetDirectories(fxBaseDir); - Assert.True(fxSubDirs.Length == 1, $"Expected exactly one subdirectory under {fxBaseDir}, found: {string.Join(", ", fxSubDirs.Select(Path.GetFileName))}"); - string fxFrameworkDir = Path.Combine(fxSubDirs[0], "_framework"); - Assert.True(Directory.Exists(fxFrameworkDir), $"Expected materialized framework dir: {fxFrameworkDir}"); + // Discover the materialized framework directory: obj/{config}/{tfm}/fx/{source-id}/_framework/ + string fxFrameworkDir = GetMaterializedFrameworkDir(objDir); // --- Computed assets: dotnet.js lives in objDir root --- AssertFileExists(objDir, "dotnet.js"); From c6fe5e5443217f25511009d5a254c97cc7edb877 Mon Sep 17 00:00:00 2001 From: maraf Date: Fri, 17 Apr 2026 11:31:49 +0000 Subject: [PATCH 11/13] Address Copilot code-review bot findings - Apply GetMaterializedFrameworkDir helper to the remaining three locations flagged by the review bot: Blazor/BuildPublishTests GetBuildSatelliteBaseDir, SatelliteLoadingTests, and ModuleConfigTests (the last still inlines the extra 'wasm/for-build' search path but deduplicates the fx-base Path.Combine). - Explain why dotnet.native.worker.mjs is not compared against the runtime pack in AssertBuildBundle (parity with AssertBundle's publish path comment). - Log when AssertWasmSdkBundle skips AssertBuildBundle because NonDefaultFrameworkDir is set, so the coverage gap is visible in test output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Wasm.Build.Tests/Blazor/BuildPublishTests.cs | 6 +----- .../wasm/Wasm.Build.Tests/ModuleConfigTests.cs | 6 +++--- .../Wasm.Build.Tests/SatelliteLoadingTests.cs | 16 +++------------- .../WasmSdkBasedProjectProvider.cs | 14 ++++++++++++-- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs index 3a3403054fa4ed..e3e2f437b4eb8f 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs @@ -112,11 +112,7 @@ string GetBuildSatelliteBaseDir() if (UseWebcil) return Path.Combine(objDir, "webcil"); - string fxBaseDir = Path.Combine(objDir, "fx"); - string[] fxSubDirs = Directory.GetDirectories(fxBaseDir); - Assert.True(fxSubDirs.Length == 1, - $"Expected exactly one subdirectory under {fxBaseDir}, found: {string.Join(", ", fxSubDirs.Select(Path.GetFileName))}"); - return Path.Combine(fxSubDirs[0], "_framework"); + return WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir); } void AssertResourcesDlls(string basePath) diff --git a/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs b/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs index 3759d4819b11e7..8fd34df6ecdda0 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs @@ -136,11 +136,11 @@ private void SymbolMapFileEmittedCore(bool emitSymbolMap, bool isPublish) else { string objDir = Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFramework); + string fxBaseDir = Path.Combine(objDir, "fx"); string[] searchDirs = [ Path.Combine(objDir, "wasm", "for-build"), - .. Directory.Exists(Path.Combine(objDir, "fx")) - ? Directory.GetDirectories(Path.Combine(objDir, "fx")) - .Select(d => Path.Combine(d, "_framework")) + .. Directory.Exists(fxBaseDir) + ? Directory.GetDirectories(fxBaseDir).Select(d => Path.Combine(d, "_framework")) : Array.Empty() ]; symbolsFileExists = searchDirs diff --git a/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs index 23c3487ee07263..f1f24cef8d34db 100644 --- a/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs @@ -114,19 +114,9 @@ public void SatelliteAssembliesFromPackageReference() // bin/_framework/. When webcil is enabled they're in obj/{config}/{tfm}/webcil/{locale}/; // when disabled they're materialized in obj/{config}/{tfm}/fx/{name}/_framework/{locale}/. string objDir = Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFramework); - string satelliteBaseDir; - if (UseWebcil) - { - satelliteBaseDir = Path.Combine(objDir, "webcil"); - } - else - { - string fxBaseDir = Path.Combine(objDir, "fx"); - string[] fxSubDirs = Directory.GetDirectories(fxBaseDir); - Assert.True(fxSubDirs.Length == 1, - $"Expected exactly one subdirectory under {fxBaseDir}, found: {string.Join(", ", fxSubDirs.Select(Path.GetFileName))}"); - satelliteBaseDir = Path.Combine(fxSubDirs[0], "_framework"); - } + string satelliteBaseDir = UseWebcil + ? Path.Combine(objDir, "webcil") + : WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir); // Microsoft.CodeAnalysis.CSharp has satellite assemblies for multiple locales string[] expectedLocales = ["cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant"]; diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs index 7448e2fc76b5af..167ba598e99a96 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -208,8 +208,14 @@ public void AssertWasmSdkBundle(Configuration config, MSBuildOptions buildOption { AssertBuildBundle(config, buildOptions, isUsingWorkloads, isNativeBuild); } - // When NonDefaultFrameworkDir is set (e.g. UseArtifactsOutput), the obj/ layout - // may not follow the standard path convention, so skip build bundle assertions. + else + { + // When NonDefaultFrameworkDir is set (e.g. UseArtifactsOutput), the obj/ layout + // may not follow the standard path convention, so skip build bundle assertions. + _testOutput.WriteLine( + $"Skipping build bundle assertions: NonDefaultFrameworkDir='{buildOptions.NonDefaultFrameworkDir}' " + + "points to a non-standard obj/ layout. File-layout verification is not performed for this build."); + } } /// @@ -271,6 +277,10 @@ private void AssertBuildBundle(Configuration config, MSBuildOptions buildOptions if (buildOptions.RuntimeType == RuntimeVariant.MultiThreaded) { + // dotnet.native.worker.mjs is validated for location only and not compared against + // the runtime pack — the publish-path AssertBundle skips the runtime-pack comparison + // for the same reason (the runtime-pack file has the same size as the relinked file, + // so the check is not meaningful). const string multiThreadedWorkerFile = "dotnet.native.worker.mjs"; AssertFileExists(nativeDir, multiThreadedWorkerFile); AssertFileNotExists(objDir, multiThreadedWorkerFile, "obj root"); From 906e5840a6cadd782e8a43c70c9d1077797e04b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Mon, 20 Apr 2026 12:47:18 +0000 Subject: [PATCH 12/13] Fix BuildThenPublishWithAOT: stat unfingerprinted dotnet.runtime.js in obj/fx With CopyToOutputDirectory=Never, the materialized framework dir under obj/{config}/{tfm}/fx/{source-id}/_framework/ contains dotnet.runtime.js under its canonical (non-fingerprinted) name during build. Fingerprinting is applied later when publishing to bin. GetFilesTable rewrites the entry from the boot config (which already reflects the publish layout), yielding a fingerprinted path that does not exist in obj/fx and causes CompareStat to report the file as missing. Override the dotnet.runtime.js entry to the unfingerprinted obj/fx path for the build-phase stat, mirroring the existing native-file override. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs index 439cd256a5fee2..ca966802eddc27 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs @@ -80,6 +80,15 @@ public async Task BuildThenPublishWithAOT(Configuration config, bool aot) pathsDict[nativeName] = (Path.Combine(paths.ObjWasmDir, nativeName), entry.unchanged); } } + + // dotnet.runtime.js lives in the materialized fx dir under its canonical (non-fingerprinted) + // name during build — fingerprinting is applied later when publishing to bin. GetFilesTable + // rewrites this entry to the fingerprinted name (from the boot config, which already reflects + // the publish layout), so override it back to the unfingerprinted obj/fx path. + if (pathsDict.TryGetValue("dotnet.runtime.js", out var runtimeJsEntry)) + { + pathsDict["dotnet.runtime.js"] = (Path.Combine(fxFrameworkDir, "dotnet.runtime.js"), runtimeJsEntry.unchanged); + } string mainDll = $"{info.ProjectName}.dll"; var firstBuildStat = StatFiles(pathsDict); From 8eb1a802b9967a0444df6edd7266a68daaee69a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Tue, 21 Apr 2026 10:07:42 +0000 Subject: [PATCH 13/13] Add hosted-scenario runtime check to MultiClientHostedBuildAndPublish Run the server via 'dotnet run --no-build' and use Playwright's APIRequestContext to verify that each referenced client's framework files are served from the obj/ materialized directory at the //_framework/* base path. This guards the hosted scenario from PR #126407 review feedback (build-only, server statically serves client assets). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../wasm/Wasm.Build.Tests/Blazor/MiscTests.cs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs index f3e7ef895fb32e..ff1bc308227ccb 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs @@ -8,6 +8,7 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.NET.Sdk.WebAssembly; +using Microsoft.Playwright; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; @@ -107,7 +108,7 @@ public void BugRegression_60479_WithRazorClassLib() [InlineData(Configuration.Release, false)] [InlineData(Configuration.Debug, true)] [InlineData(Configuration.Release, true)] - public void MultiClientHostedBuildAndPublish(Configuration config, bool publish) + public async Task MultiClientHostedBuildAndPublish(Configuration config, bool publish) { // Test that two Blazor WASM client projects can be built/published by a single server // project without duplicate static web asset Identity collisions. This validates the @@ -165,6 +166,35 @@ public void MultiClientHostedBuildAndPublish(Configuration config, bool publish) Assert.Contains(client2Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js")); Assert.Contains(client1Files, f => Path.GetFileName(f).Contains("dotnet.native") && f.EndsWith(".wasm")); Assert.Contains(client2Files, f => Path.GetFileName(f).Contains("dotnet.native") && f.EndsWith(".wasm")); + + // Start the server via `dotnet run --no-build` and verify that the static web assets + // middleware serves each referenced client's framework files from its obj/ materialized + // directory. This guards the hosted scenario the inline comment in + // Microsoft.NET.Sdk.WebAssembly.Browser.targets calls out: a server project consuming + // client framework files after `dotnet build` only (no publish). The Development + // environment is required so the Web SDK auto-invokes UseStaticWebAssets() and loads + // the client projects' static web assets manifests. + using RunCommand runCmd = new RunCommand(s_buildEnv, _testOutput); + runCmd.WithEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + ToolCommand serverCmd = runCmd.WithWorkingDirectory(serverDir); + await using BrowserRunner runner = new BrowserRunner(_testOutput); + string serverUrl = await runner.StartServerAndGetUrlAsync( + serverCmd, $"run -c {config} --no-build"); + // Kestrel logs the listening URL as http://[::]: (wildcard) which is not a valid + // Host header — replace the host with localhost for client requests. + string clientBaseUrl = serverUrl.Replace("[::]", "localhost"); + + IBrowser browser = await runner.SpawnBrowserAsync(serverUrl); + IBrowserContext context = await browser.NewContextAsync( + new() { BaseURL = clientBaseUrl, IgnoreHTTPSErrors = true }); + foreach (string clientBase in new[] { "client1", "client2" }) + { + string assetUrl = $"/{clientBase}/_framework/dotnet.js"; + _testOutput.WriteLine($"Fetching {clientBaseUrl}{assetUrl} via Playwright"); + var response = await context.APIRequest.GetAsync(assetUrl); + Assert.True(response.Ok, + $"Expected 2xx for {clientBaseUrl}{assetUrl} but got {response.Status} {response.StatusText}"); + } } } }