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)