Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/libraries/tests.proj
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,8 @@
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Runtime\tests\System.IO.FileSystem.Tests\DisabledFileLockingTests\System.IO.FileSystem.DisabledFileLocking.Tests.csproj" />
</ItemGroup>

<!-- https://github.com/dotnet/runtime/issues/125495 - These tests are disabled on browser/CoreCLR because of crashes and test failures. -->
<!-- https://github.com/dotnet/runtime/issues/125495 - Some test suites are disabled on browser/CoreCLR (e.g., long running suites). -->
<ItemGroup Condition="'$(TargetOS)' == 'browser' and '$(RuntimeFlavor)' == 'CoreCLR' and '$(RunDisabledWasmTests)' != 'true'">
<!-- test failures -->
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Formats.Nrbf\tests\System.Formats.Nrbf.Tests.csproj" />
<!-- long running suites -->
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Linq\tests\System.Linq.Tests.csproj" />
<ProjectExclusions Include="$(MSBuildThisFileDirectory)System.Memory\tests\System.Memory.Tests.csproj" />
Expand Down
116 changes: 116 additions & 0 deletions src/mono/browser/build/BrowserWasmApp.CoreCLR.targets
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,122 @@
'$(WasmBuildingForNestedPublish)' != 'true' and
'$(UsingBrowserRuntimeWorkload)' != 'true'" />

<!-- The SDK declares this in Microsoft.NET.ConflictResolution.targets for every Microsoft.NET.Sdk build
(which every browser-wasm app is). Re-declare it here, guarded, so the task resolves on user systems
regardless of import order; the guard also lets the target degrade gracefully where the SDK build
tasks assembly is unavailable. -->
<UsingTask TaskName="ResolvePackageFileConflicts"
AssemblyFile="$(MicrosoftNETBuildTasksAssembly)"
Condition="'$(MicrosoftNETBuildTasksAssembly)' != '' and Exists('$(MicrosoftNETBuildTasksAssembly)')" />

<!-- ==================== Resolve runtime-pack vs app-local assembly conflicts by version ====================

For a self-contained browser-wasm app, @(ReferenceCopyLocalPaths) can contain two copies of the
same assembly: the runtime-pack copy (under $(MicrosoftNetCoreAppRuntimePackDir)) and an app-local
copy referenced out-of-band at a different version. ComputeWasmBuildAssets dedupes the webcil bundle
candidates by relative path (first-wins, version-blind), so the wrong copy can shadow the other one
in _framework. The canonical case is System.Runtime.Serialization.Formatters: the shared framework
ships a non-functional 8.1.0.0 stub while the app references the functional 11.0.0.0 build, so an
Assembly.Load for 11.0.0.0 then fails with FileNotFoundException.

Resolve the shadowed pairs with the SDK's own conflict resolver (ResolvePackageFileConflicts) — the
same task SDK publish uses (Microsoft.NET.ConflictResolution.targets) — so the higher AssemblyVersion
wins, exactly as a desktop self-contained publish would. This is version-based, not an unconditional
app-local preference: when the runtime-pack copy is the higher version it is the one that is kept. The
wasm build path normally never reaches that resolver for runtime-pack files, which is why the stub
leaks into _framework. We feed the resolver only the same-named runtime-pack/app-local pairs that deploy
to the _framework root (no culture subdirectory) and drop whichever copies it discards; assemblies that
exist in just one place, and culture-specific satellites, are untouched.

Robustness on user systems: ResolvePackageFileConflicts ships in the .NET SDK, so it is present for any
Microsoft.NET.Sdk browser-wasm build (in-repo or on a user machine). If the SDK build tasks assembly is
somehow unavailable, fall back to "app-local wins" (drop the shadowed runtime-pack copies) so the fix
still applies — matching how a self-contained app loads assemblies.

DependsOnTargets=ResolveReferences guarantees @(ReferenceCopyLocalPaths) is populated before we prune;
_GatherWasmFilesToBuild (unlike _ComputeWasmBuildCandidates) does not pull it in on its own.
-->
<Target Name="_CoreCLRResolveRuntimePackVsAppLocalConflicts"
BeforeTargets="_ComputeWasmBuildCandidates;_GatherWasmFilesToBuild"
DependsOnTargets="ResolveReferences"
Comment thread
radekdoulik marked this conversation as resolved.
Condition="'$(IsBrowserWasmProject)' == 'true'">
<PropertyGroup>
<!-- Resolve the runtime pack dir the same way the WebAssembly SDK does (Browser.targets): prefer
the property, fall back to the ResolvedRuntimePack metadata (WBT/Helix-created projects leave
the property empty). A browser-wasm CoreCLR app resolves exactly one runtime pack
(Microsoft.NETCore.App), so the metadata expansion is unambiguous — the same single-pack
assumption the SDK's own Browser.targets relies on. Normalize once (full path + trailing slash)
so the StartsWith checks 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>
<_CoreCLRRuntimePackDir Condition="'$(_CoreCLRRuntimePackDir)' == ''">%(ResolvedRuntimePack.PackageDirectory)</_CoreCLRRuntimePackDir>
<_CoreCLRRuntimePackDir Condition="'$(_CoreCLRRuntimePackDir)' != ''">$([MSBuild]::NormalizeDirectory('$(_CoreCLRRuntimePackDir)'))</_CoreCLRRuntimePackDir>
</PropertyGroup>
<ItemGroup Condition="'$(_CoreCLRRuntimePackDir)' != ''">
<!-- Split copy-local managed assemblies into app-local vs runtime-pack copies. Restrict both sets to
assemblies that deploy to the _framework root: ComputeWasmBuildAssets keys the bundle on a culture-
qualified RelativePath (_framework/<culture>/<name> for satellites, _framework/<name> otherwise), so
same-named satellites in different cultures never actually collide. Matching the conflict set only by
file name would wrongly pair them, so we exclude anything with a culture/subdirectory (empty
DestinationSubDirectory and Culture == root deployment). Satellite-vs-satellite version skew is out of
scope here and is left to the downstream first-wins dedup. -->
<_CoreCLRAppLocalAssembly Include="@(ReferenceCopyLocalPaths)"
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 '%(ReferenceCopyLocalPaths.DestinationSubDirectory)' == '' and '%(ReferenceCopyLocalPaths.Culture)' == '' and $([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)', System.StringComparison.OrdinalIgnoreCase))" />
</ItemGroup>
<PropertyGroup Condition="'$(_CoreCLRRuntimePackDir)' != ''">
<!-- ;-delimited name lists wrapped in leading/trailing ';' so an IndexOf of ';name;' below is an
exact whole-name membership test (never a substring of a longer name). -->
<_CoreCLRAppLocalAssemblyNames>;@(_CoreCLRAppLocalAssembly->'%(FileName)%(Extension)');</_CoreCLRAppLocalAssemblyNames>
<_CoreCLRRuntimePackAssemblyNames>;@(_CoreCLRRuntimePackAssembly->'%(FileName)%(Extension)');</_CoreCLRRuntimePackAssemblyNames>
</PropertyGroup>
<ItemGroup Condition="'$(_CoreCLRRuntimePackDir)' != ''">
<!-- The conflict set: copies of the same file name present in BOTH the runtime pack and app-local. -->
<_CoreCLRConflictCandidate Include="@(_CoreCLRRuntimePackAssembly)"
Condition="$(_CoreCLRAppLocalAssemblyNames.IndexOf(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase)) != -1" />
<_CoreCLRConflictCandidate Include="@(_CoreCLRAppLocalAssembly)"
Condition="$(_CoreCLRRuntimePackAssemblyNames.IndexOf(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase)) != -1" />
</ItemGroup>

<!-- Version-based resolution (parity with SDK publish/desktop): keep the higher AssemblyVersion.
PreferredPackages mirrors the SDK publish call (_HandlePackageFileConflictsForPublish) so that an
exact AssemblyVersion+FileVersion tie is broken the same way publish breaks it (preferring the
framework package) instead of being left to the downstream first-wins dedup. The property is
populated by ResolveTargetingPackAssets, which runs as part of our ResolveReferences dependency. -->
<ResolvePackageFileConflicts Condition="'@(_CoreCLRConflictCandidate)' != '' and Exists('$(MicrosoftNETBuildTasksAssembly)')"
ReferenceCopyLocalPaths="@(_CoreCLRConflictCandidate)"
PreferredPackages="$(PackageConflictPreferredPackages)">
<Output TaskParameter="ReferenceCopyLocalPathsWithoutConflicts" ItemName="_CoreCLRConflictWinners" />
</ResolvePackageFileConflicts>
<!-- Drop the discarded copies. Mirror the SDK's _HandlePackageFileConflicts (remove every candidate, then
re-add the winners) instead of removing a computed "loser" subset by identity: @(ReferenceCopyLocalPaths)
removal matches on ItemSpec only, so an Exclude-derived loser list is fragile if an ItemSpec ever repeats
with different metadata. ReferenceCopyLocalPathsWithoutConflicts carries the winners' full metadata, so
re-including them is lossless, and removing both candidate copies first guarantees the loser is gone. -->
<ItemGroup Condition="'@(_CoreCLRConflictCandidate)' != '' and Exists('$(MicrosoftNETBuildTasksAssembly)')">
<ReferenceCopyLocalPaths Remove="@(_CoreCLRConflictCandidate)" />
<ReferenceCopyLocalPaths Include="@(_CoreCLRConflictWinners)" />
</ItemGroup>

<!-- Fallback when the SDK conflict-resolution task is unavailable: prefer app-local (copy-local wins). Same
root-only restriction as the split above so a runtime-pack satellite is never removed on a bare file-name
match against an unrelated app-local satellite in a different culture. -->
<ItemGroup Condition="'$(_CoreCLRRuntimePackDir)' != '' and '@(_CoreCLRConflictCandidate)' != '' and !Exists('$(MicrosoftNETBuildTasksAssembly)')">
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
Condition="'%(Extension)' == '.dll'
and '%(ReferenceCopyLocalPaths.DestinationSubDirectory)' == '' and '%(ReferenceCopyLocalPaths.Culture)' == ''
and $([System.String]::Copy('%(FullPath)').StartsWith('$(_CoreCLRRuntimePackDir)', System.StringComparison.OrdinalIgnoreCase))
and $(_CoreCLRAppLocalAssemblyNames.IndexOf(';%(FileName)%(Extension);', System.StringComparison.OrdinalIgnoreCase)) != -1" />
</ItemGroup>
</Target>

<!-- ======================== Core Orchestrator ======================== -->

<PropertyGroup>
Expand Down
Loading