From 75fcf55d9a1425ac32c61e25dd6166d68b9cce1c Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Fri, 12 Jun 2026 17:28:24 +0200 Subject: [PATCH 01/13] [wasm] Re-enable System.Formats.Nrbf tests on browser CoreCLR The System.Formats.Nrbf.Tests suite was excluded on browser-wasm + CoreCLR because test discovery crashed with a FileNotFoundException for System.Runtime.Serialization.Formatters, Version=11.0.0.0. Root cause: for a self-contained browser-wasm app, @(ReferenceCopyLocalPaths) contains two copies of System.Runtime.Serialization.Formatters - the shared framework's non-functional 8.1.0.0 stub (from the runtime pack) and the functional 11.0.0.0 build the test references app-local. The WebAssembly SDK task ComputeWasmBuildAssets dedupes webcil bundle candidates by relative path on a first-wins, version-blind basis, so the 8.1.0.0 stub got bundled into _framework and shadowed the app-local 11.0.0.0 copy. xunit discovery then materializes [InlineData(FormatterTypeStyle.X)] attribute blobs, triggering an Assembly.Load of the 11.0.0.0 build, which is not present - aborting discovery. Unlike desktop (framework-dependent deployment, or single-file publish whose bundler consumes the already version-resolved ResolvedFileToPublish set), the wasm webcil bundle is assembled at build time from candidates that are not version-conflict-resolved against ProjectReferences. Fix: add a CoreCLR-wasm target that applies copy-local-wins semantics before the bundle candidates are gathered - it drops runtime-pack copy-local assemblies that are shadowed by a same-named app-local copy. Apps without an app-local copy of a framework assembly are unaffected. With the fix the bundled Formatters webcil is the functional 11.0.0.0 build, discovery finds 153 test cases (was 6) and the suite passes; the ReadTests hierarchy correctly skips on browser since BinaryFormatter is unsupported there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/libraries/tests.proj | 2 -- .../build/BrowserWasmApp.CoreCLR.targets | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 4de29857494251..3a733a0604e79f 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -185,8 +185,6 @@ - - diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets index e94f759abc4f69..b83bf16ec25a33 100644 --- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets +++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets @@ -178,6 +178,40 @@ '$(WasmBuildingForNestedPublish)' != 'true' and '$(UsingBrowserRuntimeWorkload)' != 'true'" /> + + + + + <_CoreCLRAppLocalAssembly Include="@(ReferenceCopyLocalPaths)" + Condition="'%(Extension)' == '.dll' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(MicrosoftNetCoreAppRuntimePackDir)'))" /> + + + <_CoreCLRAppLocalAssemblyNames>;@(_CoreCLRAppLocalAssembly->'%(FileName)%(Extension)'); + + + + + + + From 564acab1c1756bce783721cb319acecf083cd622 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 15 Jun 2026 10:43:54 +0200 Subject: [PATCH 02/13] Feedback Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/libraries/tests.proj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 3a733a0604e79f..c9711aff2d5bfb 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -183,7 +183,7 @@ - + From 1a4d41f993d16487f7ddfb646862eb8f4ce551ac Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 15 Jun 2026 10:49:01 +0200 Subject: [PATCH 03/13] Normalize runtime pack dir before StartsWith checks $(MicrosoftNetCoreAppRuntimePackDir) is not normalized in this file (it can come straight from %(ResolvedRuntimePack.PackageDirectory) or a global property), so a missing trailing separator or non-native path separators could make the StartsWith checks fail to identify runtime-pack assemblies, silently skipping the dedupe. Normalize it once with [MSBuild]::NormalizeDirectory and use that for both checks so it reliably matches %(FullPath). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/mono/browser/build/BrowserWasmApp.CoreCLR.targets | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets index b83bf16ec25a33..b5a004c061f140 100644 --- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets +++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets @@ -195,10 +195,15 @@ + + + <_CoreCLRRuntimePackDirNormalized>$([MSBuild]::NormalizeDirectory('$(MicrosoftNetCoreAppRuntimePackDir)')) + <_CoreCLRAppLocalAssembly Include="@(ReferenceCopyLocalPaths)" - Condition="'%(Extension)' == '.dll' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(MicrosoftNetCoreAppRuntimePackDir)'))" /> + Condition="'%(Extension)' == '.dll' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDirNormalized)'))" /> <_CoreCLRAppLocalAssemblyNames>;@(_CoreCLRAppLocalAssembly->'%(FileName)%(Extension)'); @@ -207,7 +212,7 @@ From df2f4e587d47288e1f1d852c4a0d6942c95bf066 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 15 Jun 2026 12:07:12 +0200 Subject: [PATCH 04/13] Resolve runtime pack dir with ResolvedRuntimePack fallback The target previously only ran when $(MicrosoftNetCoreAppRuntimePackDir) was set, but WBT/Helix-created CoreCLR-wasm projects can leave that property empty and rely on %(ResolvedRuntimePack.PackageDirectory) instead, so the target was skipped and the runtime-pack vs app-local shadowing persisted for them. Resolve the runtime pack dir inside the target the same way the WebAssembly SDK does (Browser.targets): prefer the property, fall back to the ResolvedRuntimePack metadata, then normalize. Both ItemGroups are guarded so an unresolved dir is a safe no-op. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../build/BrowserWasmApp.CoreCLR.targets | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets index b5a004c061f140..354e89cc9d903c 100644 --- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets +++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets @@ -194,25 +194,29 @@ --> + Condition="'$(IsBrowserWasmProject)' == 'true'"> - - <_CoreCLRRuntimePackDirNormalized>$([MSBuild]::NormalizeDirectory('$(MicrosoftNetCoreAppRuntimePackDir)')) + + <_CoreCLRRuntimePackDir>$(MicrosoftNetCoreAppRuntimePackDir) + <_CoreCLRRuntimePackDir Condition="'$(_CoreCLRRuntimePackDir)' == ''">%(ResolvedRuntimePack.PackageDirectory) + <_CoreCLRRuntimePackDir Condition="'$(_CoreCLRRuntimePackDir)' != ''">$([MSBuild]::NormalizeDirectory('$(_CoreCLRRuntimePackDir)')) - + <_CoreCLRAppLocalAssembly Include="@(ReferenceCopyLocalPaths)" - Condition="'%(Extension)' == '.dll' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDirNormalized)'))" /> + Condition="'%(Extension)' == '.dll' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)'))" /> <_CoreCLRAppLocalAssemblyNames>;@(_CoreCLRAppLocalAssembly->'%(FileName)%(Extension)'); - + From 58e4d6679f0f2195923eaadc1cfffb5e1d9ee597 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 15 Jun 2026 15:37:20 +0200 Subject: [PATCH 05/13] Run dedupe after ResolveReferences to guarantee populated item list _GatherWasmFilesToBuild has no DependsOnTargets, so a target hooked only via BeforeTargets could run before ResolveReferences populates @(ReferenceCopyLocalPaths), making the dedupe a no-op. Add DependsOnTargets=ResolveReferences so the prune is correct-by-construction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/mono/browser/build/BrowserWasmApp.CoreCLR.targets | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets index 354e89cc9d903c..52dece99895f9c 100644 --- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets +++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets @@ -191,9 +191,13 @@ Resolve it the way a self-contained app loads assemblies (copy-local wins): drop runtime-pack copies shadowed by a same-named app-local copy before the candidates are gathered. Apps without an app-local copy of a framework assembly are unaffected. + + DependsOnTargets=ResolveReferences guarantees @(ReferenceCopyLocalPaths) is populated before we prune; + _GatherWasmFilesToBuild (unlike _ComputeWasmBuildCandidates) does not pull it in on its own. --> + + + <_CoreCLRAppLocalAssembly Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)'))" /> + <_CoreCLRRuntimePackAssembly Include="@(ReferenceCopyLocalPaths)" + Condition="'%(Extension)' == '.dll' and $([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)'))" /> - + <_CoreCLRAppLocalAssemblyNames>;@(_CoreCLRAppLocalAssembly->'%(FileName)%(Extension)'); + <_CoreCLRRuntimePackAssemblyNames>;@(_CoreCLRRuntimePackAssembly->'%(FileName)%(Extension)'); - + + <_CoreCLRConflictCandidate Include="@(_CoreCLRRuntimePackAssembly)" + Condition="$(_CoreCLRAppLocalAssemblyNames.Contains(';%(FileName)%(Extension);'))" /> + <_CoreCLRConflictCandidate Include="@(_CoreCLRAppLocalAssembly)" + Condition="$(_CoreCLRRuntimePackAssemblyNames.Contains(';%(FileName)%(Extension);'))" /> + + + + + + + + <_CoreCLRConflictLoser Include="@(_CoreCLRConflictCandidate)" Exclude="@(_CoreCLRConflictWinners)" /> + + + + + - + + ReferenceCopyLocalPaths="@(_CoreCLRConflictCandidate)" + PreferredPackages="$(PackageConflictPreferredPackages)"> From a94dc001369623f059c7f4f021e95b1e7ffa2dff Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Tue, 23 Jun 2026 13:12:33 +0200 Subject: [PATCH 08/13] Use OrdinalIgnoreCase for runtime-pack path/name comparisons The StartsWith path-prefix checks and the Contains name-membership checks were ordinal case-sensitive. On a case-insensitive filesystem a casing difference between $(MicrosoftNetCoreAppRuntimePackDir) and the enumerated %(FullPath) values (e.g. drive-letter or user-overridden casing) could silently misclassify a runtime-pack copy as app-local and no-op the fix with no error. Compare with System.StringComparison.OrdinalIgnoreCase so classification is robust, matching how the SDK itself does path-prefix checks (e.g. Microsoft.FSharp.Targets). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/build/BrowserWasmApp.CoreCLR.targets | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets index 8ad9a054616ef5..cacc7831d27703 100644 --- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets +++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets @@ -219,7 +219,9 @@ + below reliably match %(FullPath), which uses native separators. The path/name comparisons + use OrdinalIgnoreCase so a casing difference between the property and the enumerated item + paths (common on case-insensitive filesystems) cannot silently misclassify a copy. --> <_CoreCLRRuntimePackDir>$(MicrosoftNetCoreAppRuntimePackDir) <_CoreCLRRuntimePackDir Condition="'$(_CoreCLRRuntimePackDir)' == ''">%(ResolvedRuntimePack.PackageDirectory) <_CoreCLRRuntimePackDir Condition="'$(_CoreCLRRuntimePackDir)' != ''">$([MSBuild]::NormalizeDirectory('$(_CoreCLRRuntimePackDir)')) @@ -227,9 +229,9 @@ <_CoreCLRAppLocalAssembly Include="@(ReferenceCopyLocalPaths)" - Condition="'%(Extension)' == '.dll' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)'))" /> + Condition="'%(Extension)' == '.dll' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)', System.StringComparison.OrdinalIgnoreCase))" /> <_CoreCLRRuntimePackAssembly Include="@(ReferenceCopyLocalPaths)" - Condition="'%(Extension)' == '.dll' and $([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)'))" /> + Condition="'%(Extension)' == '.dll' and $([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)', System.StringComparison.OrdinalIgnoreCase))" /> <_CoreCLRAppLocalAssemblyNames>;@(_CoreCLRAppLocalAssembly->'%(FileName)%(Extension)'); @@ -238,9 +240,9 @@ <_CoreCLRConflictCandidate Include="@(_CoreCLRRuntimePackAssembly)" - Condition="$(_CoreCLRAppLocalAssemblyNames.Contains(';%(FileName)%(Extension);'))" /> + Condition="$(_CoreCLRAppLocalAssemblyNames.Contains(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase))" /> <_CoreCLRConflictCandidate Include="@(_CoreCLRAppLocalAssembly)" - Condition="$(_CoreCLRRuntimePackAssemblyNames.Contains(';%(FileName)%(Extension);'))" /> + Condition="$(_CoreCLRRuntimePackAssemblyNames.Contains(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase))" /> <_CoreCLRConflictCandidate Include="@(_CoreCLRRuntimePackAssembly)" - Condition="$(_CoreCLRAppLocalAssemblyNames.Contains(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase))" /> + Condition="$(_CoreCLRAppLocalAssemblyNames.IndexOf(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase) != -1)" /> <_CoreCLRConflictCandidate Include="@(_CoreCLRAppLocalAssembly)" - Condition="$(_CoreCLRRuntimePackAssemblyNames.Contains(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase))" /> + Condition="$(_CoreCLRRuntimePackAssemblyNames.IndexOf(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase) != -1)" /> - From 06eb820bfc08790486d1483522b6c26acb50f0c0 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Wed, 24 Jun 2026 12:02:10 +0200 Subject: [PATCH 12/13] Fix MSB4184 from comparison operator inside $() in conflict conditions The case-insensitive name membership checks used $(list.IndexOf(';name;', System.StringComparison.OrdinalIgnoreCase) != -1). MSBuild's property-function expander evaluates a method-call chain and returns its value, but it rejects a trailing comparison operator (!= -1, >= 0) placed inside the $(), raising error MSB4184 "cannot be evaluated". This is independent of the StringComparison argument and of the item/metadata context, and it broke the wasm CoreCLR build for every library test project (e.g. System.Runtime.Tests). Contains(string, StringComparison) sidesteps it (returns bool directly) but that overload is missing on .NET Framework MSBuild. Keep IndexOf(string, StringComparison) with OrdinalIgnoreCase (available on .NET Framework 2.0+) and move the "!= -1" test OUTSIDE the $() so it is a top-level Condition comparison on the returned int. This preserves the original case-insensitive intent, stays consistent with the StartsWith(..., OrdinalIgnoreCase) path checks, and needs no lower-casing workaround. Validated: Nrbf wasm CoreCLR suite builds 0 warnings/0 errors and runs 4/4 passing from a freshly pruned bundle; the target's ResolvePackageFileConflicts picks Formatters 11.0.0.0 over the 8.1.0.0 stub by AssemblyVersion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../build/BrowserWasmApp.CoreCLR.targets | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets index 49cecf63434f54..3c768808310df9 100644 --- a/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets +++ b/src/mono/browser/build/BrowserWasmApp.CoreCLR.targets @@ -220,9 +220,14 @@ + below reliably match %(FullPath), which uses native separators. All comparisons use the + OrdinalIgnoreCase StringComparison so a casing difference between the property and the + enumerated item paths (common on case-insensitive filesystems) cannot silently misclassify a + copy. Path membership uses StartsWith(string, StringComparison); name membership uses + IndexOf(string, StringComparison) with the "!= -1" test kept OUTSIDE the $() (MSBuild's + property-function expander returns the int index but rejects a trailing comparison operator + inside the $(), which is the MSB4184 trap). Both overloads exist on .NET Framework MSBuild; + the Contains(string, StringComparison) overload does not, so it is avoided. --> <_CoreCLRRuntimePackDir>$(MicrosoftNetCoreAppRuntimePackDir) <_CoreCLRRuntimePackDir Condition="'$(_CoreCLRRuntimePackDir)' == ''">%(ResolvedRuntimePack.PackageDirectory) <_CoreCLRRuntimePackDir Condition="'$(_CoreCLRRuntimePackDir)' != ''">$([MSBuild]::NormalizeDirectory('$(_CoreCLRRuntimePackDir)')) @@ -235,15 +240,17 @@ Condition="'%(Extension)' == '.dll' and $([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)', System.StringComparison.OrdinalIgnoreCase))" /> + <_CoreCLRAppLocalAssemblyNames>;@(_CoreCLRAppLocalAssembly->'%(FileName)%(Extension)'); <_CoreCLRRuntimePackAssemblyNames>;@(_CoreCLRRuntimePackAssembly->'%(FileName)%(Extension)'); <_CoreCLRConflictCandidate Include="@(_CoreCLRRuntimePackAssembly)" - Condition="$(_CoreCLRAppLocalAssemblyNames.IndexOf(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase) != -1)" /> + Condition="$(_CoreCLRAppLocalAssemblyNames.IndexOf(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase)) != -1" /> <_CoreCLRConflictCandidate Include="@(_CoreCLRAppLocalAssembly)" - Condition="$(_CoreCLRRuntimePackAssemblyNames.IndexOf(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase) != -1)" /> + Condition="$(_CoreCLRRuntimePackAssemblyNames.IndexOf(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase)) != -1" /> @@ -233,11 +236,17 @@ <_CoreCLRRuntimePackDir Condition="'$(_CoreCLRRuntimePackDir)' != ''">$([MSBuild]::NormalizeDirectory('$(_CoreCLRRuntimePackDir)')) - + <_CoreCLRAppLocalAssembly Include="@(ReferenceCopyLocalPaths)" - Condition="'%(Extension)' == '.dll' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)', System.StringComparison.OrdinalIgnoreCase))" /> + Condition="'%(Extension)' == '.dll' and '%(ReferenceCopyLocalPaths.DestinationSubDirectory)' == '' and '%(ReferenceCopyLocalPaths.Culture)' == '' and !$([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)', System.StringComparison.OrdinalIgnoreCase))" /> <_CoreCLRRuntimePackAssembly Include="@(ReferenceCopyLocalPaths)" - Condition="'%(Extension)' == '.dll' and $([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)', System.StringComparison.OrdinalIgnoreCase))" /> + Condition="'%(Extension)' == '.dll' and '%(ReferenceCopyLocalPaths.DestinationSubDirectory)' == '' and '%(ReferenceCopyLocalPaths.Culture)' == '' and $([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)', System.StringComparison.OrdinalIgnoreCase))" /> - <_CoreCLRConflictLoser Include="@(_CoreCLRConflictCandidate)" Exclude="@(_CoreCLRConflictWinners)" /> - + + - +