Skip to content

[browser] Don't copy framework assets to output during build#126407

Merged
maraf merged 17 commits intomainfrom
maraf/WasmSdkCopyToBin
Apr 21, 2026
Merged

[browser] Don't copy framework assets to output during build#126407
maraf merged 17 commits intomainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf
Copy link
Copy Markdown
Member

@maraf maraf commented Apr 1, 2026

Note

This PR was created with the assistance of GitHub Copilot.

Summary

During build, the WebAssembly SDK copies all .wasm and .js framework assets (~178 files) 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 via the manifest (staticwebassets.runtime.json).

Changes

Set CopyToOutputDirectory=Never (was PreserveNewest) for three DefineStaticWebAssets / item-update calls in Microsoft.NET.Sdk.WebAssembly.Browser.targets:

  • Webcil-converted assets (Computed source type) — the .wasm files produced from .dll
  • Framework assets (Framework source type) — dotnet.js, dotnet.native.wasm, runtime JS, etc.
  • Materialized framework assets (post-UpdatePackageStaticWebAssets) — the per-project copies in obj/fx/

Validation

  • ✅ Build succeeds (dotnet build with TargetOS=browser)
  • dotnet run starts WasmAppHost dev server correctly
  • ✅ Playwright headless browser test confirms the WASM app loads and executes (outputs 42)
  • bin/wwwroot/_framework/ drops from ~178 files to 2 (only hot-reload module + dotnet.js from a separate code path)
  • CopyToPublishDirectory was already Never — publish is unaffected

Notes

  • The previous PreserveNewest was added for Blazor WASM hosted scenarios where a server project serves the client's framework files. This scenario needs separate validation.
  • Static web assets middleware resolves files from their source locations (obj/ dirs), so physical copies in bin/ are not needed for dotnet run.

Downstream impact: dotnet/sdk BlazorWasm test baselines

The Microsoft.NET.Sdk.BlazorWebAssembly.Tests StaticWebAssetsBaselines/*.Build.*.json files in dotnet/sdk currently assert that framework assets live under ${ProjectPath}\bin\Debug\${Tfm}\wwwroot\_framework\ after Build. Across the Build baselines there are ~1,940 bin\Debug references covering ~481 framework files:

  • dotnet.* runtime/loader JS + .wasm + .map (+ .gz) — 12 files
  • icudt_*.dat.gz — 3 files
  • blazor.webassembly.js (+ .gz) — 2 files
  • ~233 managed framework .wasm assemblies (+ .gz copies)
  • Localized .resources.wasm satellites

Once this change flows to dotnet/sdk, these baselines will need to be regenerated so framework assets point at their intermediate/RestorePath locations instead of bin\. App-project outputs (blazorwasm.wasm/.pdb) and project references (RazorClassLibrary.*) are unaffected and remain under bin\.

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>
Copilot AI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

This comment was marked as resolved.

@maraf maraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build [browser] Don't copy framework assets to output during build Apr 1, 2026
@maraf maraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@maraf maraf added this to the 11.0.0 milestone Apr 1, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

This comment was marked as resolved.

This comment was marked as resolved.

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>
@github-actions

This comment has been minimized.

Copilot AI review requested due to automatic review settings April 10, 2026 08:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
Comment thread src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
maraf and others added 2 commits April 10, 2026 08:50
With CopyToOutputDirectory=Never, framework files are no longer copied
to bin/_framework/ during build. Update MultiClientHostedBuildAndPublish
to assert framework files exist in obj/<config>/<tfm>/fx/<ProjectName>/
_framework/ for build, matching the materialization path used by
UpdatePackageStaticWebAssets. Publish assertions remain unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
Copilot AI review requested due to automatic review settings April 14, 2026 09:44
maraf and others added 2 commits April 17, 2026 10:26
…aded 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>
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>
Copilot AI review requested due to automatic review settings April 17, 2026 10:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comment thread src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs Outdated
Comment thread src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Outdated
@github-actions
Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126407

Note

This review was AI/Copilot-generated. Models used: Claude Opus 4.6 (primary), Claude Sonnet 4.5.

Holistic Assessment

Motivation: The PR addresses a genuine build performance issue — ~178 unnecessary file copies during build. The static web assets middleware already serves files from obj/ locations via the manifest, making the bin/wwwroot/_framework/ copies redundant during build. This is well-motivated.

Approach: The approach is clean and minimal — 3 attribute value changes in production code (CopyToOutputDirectory from PreserveNewest to Never), with the remaining 8 files being test adaptations. Publish behavior is unchanged. The approach is correct.

Summary: ⚠️ Needs Human Review. The production change and test updates are sound, but there are maintainability concerns (code duplication) and a minor test coverage reduction worth human judgment. No blocking correctness issues found.


Detailed Findings

✅ Production Change — Correct and well-scoped

The three changes in Microsoft.NET.Sdk.WebAssembly.Browser.targets correctly switch CopyToOutputDirectory from PreserveNewest to Never for:

  • Webcil-converted assets (line 390, Computed static web assets)
  • Framework pass-through candidates (line 409, Framework static web assets)
  • Materialized framework assets (line 438, post-UpdatePackageStaticWebAssets)

The updated XML comments (lines 430–435) accurately describe the new behavior. CopyToPublishDirectory was already Never, and publish has its own copy mechanism, so publish is unaffected.

✅ New AssertBuildBundle method — Thorough validation

The new AssertBuildBundle method in WasmSdkBasedProjectProvider.cs (lines 205–309) is well-structured with clear sections for each asset category. It validates:

  • Files exist in the correct obj/ subdirectories
  • Files do NOT exist in wrong locations (obj root, bin/_framework/)
  • Boot config is well-formed
  • Native files match runtime pack when expected
  • Multi-threaded worker files (added in review feedback)

The nullable isNativeBuild parameter flows correctly through GetExpectedFileType which handles all three states (true, false, null) with explicit comparisons.

⚠️ Code Duplication — "Discover fx subdirectory" pattern repeated 5 times

The following pattern appears verbatim in 5 locations across test files:

string fxBaseDir = Path.Combine(objDir, "fx");
string[] fxSubDirs = Directory.GetDirectories(fxBaseDir);
Assert.True(fxSubDirs.Length == 1,
    $"Expected exactly one subdirectory under {fxBaseDir}, ...");
string fxFrameworkDir = Path.Combine(fxSubDirs[0], "_framework");

Locations: WasmSdkBasedProjectProvider.AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore.

If the obj/ layout convention ever changes, all 5 places need updating in lockstep. Consider extracting a shared helper like GetMaterializedFrameworkDir(string objDir) on the provider base class. (Flagged by both review models.)

⚠️ NonDefaultFrameworkDir skips all build assertions — coverage gap

In AssertWasmSdkBundle (lines 187–192), when NonDefaultFrameworkDir is set and IsPublish is false, zero build bundle assertions run:

else if (string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir))
{
    AssertBuildBundle(...);
}
// When NonDefaultFrameworkDir is set, skip build bundle assertions.

Before this PR, AssertBundle was called for all cases (including NonDefaultFrameworkDir). Tests like SimpleRunTests.BlazorBuildRunTest and WasmTemplateTests.BuildAndRunForDifferentOutputPaths pass NonDefaultFrameworkDir for non-artifacts builds too, so they now get no file-layout verification. The tests do verify the app runs via dotnet run, which is the primary concern, but the file-layout gap should be acknowledged. Would be worth at minimum logging a message when assertions are skipped to aid debugging.

💡 Minor: Duplicate Path.Combine in ModuleConfigTests.cs

In SymbolMapFileEmittedCore (lines 139–144), Path.Combine(objDir, "fx") is evaluated twice in the spread expression — once for Directory.Exists and once for Directory.GetDirectories. Extract to a local variable for clarity and consistency with the pattern used elsewhere.

💡 Minor: dotnet.native.worker.mjs not compared against runtime pack

In AssertBuildBundle, the multi-threaded worker file gets location assertions (exists in nativeDir, not in obj root) but is not compared against the runtime pack, unlike dotnet.native.js and dotnet.native.wasm. The publish-path AssertBundle has special-case handling that explicitly skips this comparison with an explanatory comment. Adding a similar comment in AssertBuildBundle would explain the intentional omission.

✅ Accidental symlink removal — Good cleanup

The deletion of src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost (a symlink pointing to a local Codespace path /workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug) is appropriate cleanup that was caught in review.

✅ No public API changes detected

No changes to ref/ assemblies or new public API surface. All code changes are in MSBuild targets (build infrastructure) and test files.

Generated by Code Review for issue #126407 ·

maraf and others added 2 commits April 17, 2026 11:29
Addresses review feedback: centralizes the obj/.../fx/<source-id>/_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>
- 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>
Copy link
Copy Markdown
Member Author

maraf commented Apr 17, 2026

Note

Comment authored with assistance from GitHub Copilot.

Thanks for the thorough pass. Addressing the findings in 00635ac + c6fe5e5:

✅ Code duplication (5× fx discovery) — extracted WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir) and applied it at all 5 sites (AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore). Blazor/MiscTests.MultiClientHostedBuildAndPublish also switched to the helper (was previously hardcoding Client1/Client2).

NonDefaultFrameworkDir skip — coverage gapAssertWasmSdkBundle now emits a _testOutput.WriteLine when it skips AssertBuildBundle for NonDefaultFrameworkDir builds, so the skipped file-layout verification is visible in test logs. Keeping the skip itself for now since the obj/ layout genuinely diverges for UseArtifactsOutput; a proper layout check for that shape can be a follow-up.

✅ Duplicate Path.Combine(objDir, "fx") in ModuleConfigTests — hoisted into a fxBaseDir local.

dotnet.native.worker.mjs runtime-pack comparison omission — added a comment in AssertBuildBundle explaining the intentional omission, mirroring the existing comment in the publish-path AssertBundle.

Left as-is:

  • Hosted dotnet build + dotnet run server test (raised on Microsoft.NET.Sdk.WebAssembly.Browser.targets): out of scope for this PR. Hosted publish is still exercised end-to-end by MultiClientHostedBuildAndPublish; an end-to-end build + serve test would require new Kestrel + HTTP infra in Wasm.Build.Tests. Will file a follow-up issue.

maraf and others added 2 commits April 20, 2026 12:47
…n 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>
Copilot AI review requested due to automatic review settings April 21, 2026 08:36
@maraf maraf marked this pull request as ready for review April 21, 2026 08:36
@maraf maraf requested a review from a team April 21, 2026 08:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

@maraf
Copy link
Copy Markdown
Member Author

maraf commented Apr 21, 2026

@javiercn Do we have scenario that would still rely on the actual files in the bin folder?

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
/<client>/_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>
Copy link
Copy Markdown
Member Author

maraf commented Apr 21, 2026

Note

This comment was generated with the assistance of GitHub Copilot.

CI analysis for the latest push (build 1388930):

  • runtime: 12/13 green, 1 failed
  • runtime-dev-innerloop: ✅ green
  • dotnet-linker-tests: ✅ green

The single failure is browser-wasm linux Release CoreCLR_WasmBuildTests — Helix work item WBT-NoWorkload-CLR-ST-Wasm.Build.Tests.WasmRunOutOfAppBundleTests exited with code -4:

Unable to pull image mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-26.04-helix-webassembly-amd64

This is the well-known Helix Docker-pull infrastructure flake tracked in #117164 — tests never started; the failing test (WasmRunOutOfAppBundleTests) doesn't exercise any code path touched by this PR. Unrelated to the change. Will retry with /azp run runtime if a fully green run is required before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-Build-mono os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants