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 3a89fbcc2d9d15..bd63688adeed59 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
@@ -387,7 +387,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)"
@@ -406,7 +406,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)"
@@ -430,12 +430,12 @@ 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="PreserveNewest" />
+ AssetMode="All" CopyToOutputDirectory="Never" />
diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs
index b1e19c334fc624..e3e2f437b4eb8f 100644
--- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs
@@ -95,14 +95,26 @@ 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");
+
+ return WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir);
+ }
+
void AssertResourcesDlls(string basePath)
{
foreach (string culture in cultures)
diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs
index 8f0b6d3d7feee6..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
@@ -148,16 +149,52 @@ 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
+ // 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);
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"));
+
+ // 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}");
+ }
}
}
}
diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs
index 23dd76ebce99f2..ca966802eddc27 100644
--- a/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs
@@ -60,8 +60,35 @@ 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/{source-id}/_framework/.
+ // The boot config (dotnet.js) is at obj/{config}/{tfm}/dotnet.js.
+ string fxFrameworkDir = WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(paths.ObjDir);
+
+ 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);
+ }
+ }
+
+ // 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);
diff --git a/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs b/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs
index 699cc706756a8a..8fd34df6ecdda0 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 fxBaseDir = Path.Combine(objDir, "fx");
+ string[] searchDirs = [
+ Path.Combine(objDir, "wasm", "for-build"),
+ .. Directory.Exists(fxBaseDir)
+ ? Directory.GetDirectories(fxBaseDir).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/SatelliteLoadingTests.cs b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs
index 10976b41087d3e..f1f24cef8d34db 100644
--- a/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs
@@ -110,15 +110,20 @@ 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 = UseWebcil
+ ? Path.Combine(objDir, "webcil")
+ : WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir);
// 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/Templates/WasmTemplateTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs
index 0760f1d12d2f62..dd89863382693b 100644
--- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs
@@ -497,8 +497,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);
diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs
index 213960b59d7f82..167ba598e99a96 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;
@@ -25,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()
@@ -178,7 +199,150 @@ 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);
+ }
+ 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.");
+ }
+ }
+
+ ///
+ /// 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/{source-id}/_framework/
+ string fxFrameworkDir = GetMaterializedFrameworkDir(objDir);
+
+ // --- 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: isUsingWorkloads, isNativeBuild: isNativeBuild)
+ : NativeFilesType.FromRuntimePack;
+ bool isNativeRebuild = expectedFileType is NativeFilesType.Relinked or NativeFilesType.AOT;
+ string nativeDir = isNativeRebuild
+ ? Path.Combine(objDir, "wasm", "for-build")
+ : fxFrameworkDir;
+
+ 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");
+ }
+
+ 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");
+ 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)
+ {
+ 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");
+ 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)