Skip to content

[browser] Wasm boot JSON: touch a stamp, not the boot JSON itself (fix #129280)#129591

Open
lewing wants to merge 2 commits into
dotnet:mainfrom
lewing:fix/wasm-bootjson-touch-cascade
Open

[browser] Wasm boot JSON: touch a stamp, not the boot JSON itself (fix #129280)#129591
lewing wants to merge 2 commits into
dotnet:mainfrom
lewing:fix/wasm-bootjson-touch-cascade

Conversation

@lewing

@lewing lewing commented Jun 18, 2026

Copy link
Copy Markdown
Member

Problem

Wasm.Build.Tests.WasmTemplateTests.TypeScriptDefinitionsCopiedToWwwrootOnBuild(config: Debug, emitTypeScriptDts: True) fails its dotnet build -question rebuild check with:

MSBUILD : error : Building target "_BuildCopyStaticWebAssetsPreserveNewest" partially,
because some output files are out of date with respect to their input files.
[_BuildStaticWebAssetsPreserveNewest:
  Input  = obj\Debug\net11.0\compressed\{hash}-{0}-{fp}-{fp}.gz,
  Output = bin\Debug\net11.0\wwwroot\_framework\dotnet.{fp}.js.gz]
Input file is newer than output file.

This is the same family of bug as #129280 (different surface: upstream variant where MSBuild flags _WriteBuildWasmBootJsonFile itself as stale; ours is the downstream cascade through compression).

Root cause

Binlog analysis:

  1. _WriteBuildWasmBootJsonFile runs GenerateWasmBootJson (uses ArtifactWriter.PersistFileIfChanged, preserves old mtime on unchanged content) followed by <Touch Files="$(_WasmBuildBootJsonPath)" /> (added in [browser] WebAssembly SDK targets more incremental #125367 to keep MSBuild's I/O check on this target happy).
  2. $(_WasmBuildBootJsonPath)obj/{cfg}/{tfm}/dotnet.js — is also a source for the StaticWebAssets compression pipeline (GenerateBuildCompressedStaticWebAssetsGZipCompress).
  3. GZipCompress.cs skips when input.mtime < output.mtime (strict <). When Touch lands close enough in time to the .gz write (or any subsequent target re-stamps dotnet.js), equal/newer mtimes send the next build's compression task down the re-compress path.
  4. Re-compression on build Get core-setup building in the consolidated repo. #2 bumps obj/.../{0}.gz mtime past bin/.../dotnet.{fp}.js.gz (which was copied during build WIP: repo consolidation scouting kick-off - make clr build locally on Windows #1 and isn't touched since), so _BuildCopyStaticWebAssetsPreserveNewest reports its input newer than its output. -question fails.

This is a regression of #118637 (Aug 2025, "Override boot config only when the content changes"), which fixed the exact same _BuildCopyStaticWebAssetsPreserveNewest symptom in dotnet/aspnetcore#63207 by introducing ArtifactWriter.PersistFileIfChanged. PR #125367's <Touch> undid that protection.

Fix

Touch a separate wasm-bootjson-{build,publish}.complete.stamp file rather than the boot JSON itself, and use the stamp as the target's Outputs=. MSBuild's incrementality check on the boot-JSON target stays correct (its declared output has a current mtime after every successful run), while the boot JSON's mtime remains content-derived — preserving #118637's invariant for downstream consumers.

Applied to both:

  • _WriteBuildWasmBootJsonFile (build)
  • GeneratePublishWasmBootJson (publish)

Both have identical shape and the same downstream consumers (StaticWebAssets compression / Copy targets).

What's not fixed here

  • _ConvertBuildDllsToWebcil (line 431) uses the same <Touch> pattern but on per-item-batched @(_WasmConvertedWebcilOutputs). It is probably exposed to the same cascade for the webcil files; not addressed here pending a per-item analysis.
  • Defense-in-depth in dotnet/sdk: GZipCompress/BrotliCompress < mtime check could become <= to harden the entire StaticWebAssets pipeline against this class of cascade. Worth a separate dotnet/sdk PR.

Verification

Fixes #129280

Note

This pull request was created with the assistance of GitHub Copilot.

The build and publish boot JSON targets used <Touch Files="$(_WasmBuildBootJsonPath)" />
to give MSBuild's Inputs/Outputs check a current output timestamp despite
GenerateWasmBootJson's content-preservation behavior (PR dotnet#125367). However the
boot JSON ($(_WasmBuildBootJsonPath), typically obj/.../dotnet.js, and the
publish equivalent) is also a source for the StaticWebAssets compression
pipeline and the PreserveNewest copy target. Bumping its mtime on every
successful build defeats their incrementality checks and breaks
`dotnet build -question`:

  Building target "_BuildCopyStaticWebAssetsPreserveNewest" partially, because
  some output files are out of date with respect to their input files.
  [_BuildStaticWebAssetsPreserveNewest:
    Input  = obj/.../compressed/{hash}-{0}-{fp}-{fp}.gz,
    Output = bin/.../wwwroot/_framework/dotnet.{fp}.js.gz]
  Input file is newer than output file.

This is the same regression that PR dotnet#118637 (Aug 2025) originally fixed by
introducing ArtifactWriter.PersistFileIfChanged. PR dotnet#125367's <Touch> on the
boot JSON re-broke it by bumping the file's mtime unconditionally after every
successful run.

Replace the <Touch> on the boot JSON with a <Touch> on a separate
`wasm-bootjson-{build,publish}.complete.stamp` file, and use that stamp as the
target's Outputs=. MSBuild now sees a current output timestamp for its own
incrementality check on the boot-JSON target, without touching the boot JSON
itself — preserving content-mtime semantics for downstream consumers.

Fixes dotnet#129280
Fixes the downstream cascade observed on PR dotnet#129454 (WBT
`TypeScriptDefinitionsCopiedToWwwrootOnBuild(Debug, emitTypeScriptDts:True)`).

Same fix applied to both the build (`_WriteBuildWasmBootJsonFile`) and
publish (`GeneratePublishWasmBootJson`) paths since both have identical
shape and the same downstream consumers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the WebAssembly Browser MSBuild targets to avoid touching the boot JSON itself (which can cascade timestamp changes into StaticWebAssets compression/copy), and instead uses a separate “complete” stamp file as the incremental Outputs= marker for boot JSON generation targets.

Changes:

  • Switch _WriteBuildWasmBootJsonFile and GeneratePublishWasmBootJson to use wasm-bootjson-{build,publish}.complete.stamp as Outputs=.
  • Touch the new stamp files (instead of the boot JSON) to satisfy MSBuild’s timestamp-based incremental checks.
  • Track the new stamp files in FileWrites (note: build wrapper update suggested; publish wrapper update still needed).

Comment on lines +760 to +764
build defeats their incrementality checks and breaks `dotnet build -question` (see
dotnet/runtime#129280, dotnet/runtime#124729, dotnet/aspnetcore#63207).
Touch a separate stamp file that is the target's Outputs= so MSBuild sees a current
output timestamp without affecting downstream consumers of the boot JSON. -->
<Touch Files="$(IntermediateOutputPath)wasm-bootjson-build.complete.stamp" AlwaysCreate="true" />

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 97a975c — added wasm-bootjson-{build,publish}.complete.stamp to FileWrites in _GenerateBuildWasmBootJson and _AddPublishWasmBootJsonToStaticWebAssets so dotnet clean removes the stamps even when the incremental targets skip.

Note

This comment was AI/Copilot-generated.

Comment on lines +1234 to +1237
<!-- See _WriteBuildWasmBootJsonFile for rationale: touch a separate stamp file rather than
the boot JSON itself to avoid cascading mtime bumps into downstream StaticWebAssets
compression / copy targets. -->
<Touch Files="$(IntermediateOutputPath)wasm-bootjson-publish.complete.stamp" AlwaysCreate="true" />

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 97a975c — added wasm-bootjson-{build,publish}.complete.stamp to FileWrites in _GenerateBuildWasmBootJson and _AddPublishWasmBootJsonToStaticWebAssets so dotnet clean removes the stamps even when the incremental targets skip.

Note

This comment was AI/Copilot-generated.

@lewing lewing requested a review from maraf June 18, 2026 19:18
Addresses Copilot review feedback on dotnet#129591: the
.complete.stamp files added in the prior commit are only listed in
FileWrites inside the incremental targets (_WriteBuildWasmBootJsonFile,
GeneratePublishWasmBootJson). When those targets are skipped on
incremental builds, the in-body <ItemGroup> entries are not populated,
so `dotnet clean` would leave the stamps behind.

Mirror the existing pattern from PR dotnet#125367: also add the stamp paths
to FileWrites in the always-run wrapper targets
(_GenerateBuildWasmBootJson and _AddPublishWasmBootJsonToStaticWebAssets)
that track the boot JSON itself for the same reason.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_WriteBuildWasmBootJsonFile is not incremental

2 participants