Android framework version
net10.0-android
Affected platform version
VS 2026, .NET 10.0
Description
Under parallel build, NuGet Pack's per-TFM inner-target dispatches re-evaluate a producing net10.0-android library on a fresh MSBuild node, causing _CreateAar to re-write bin/.../<assembly>.aar non-atomically while consumers concurrently read it via Files.HashFile:
error XARLP7024: System.IO.IOException: The process cannot access the file
'.../<assembly>.aar' because it is being used by another process.
at Microsoft.Android.Build.Tasks.Files.HashFile(String filename, HashAlgorithm hashAlg)
at Xamarin.Android.Tasks.ResolveLibraryProjectImports.Extract(...)
Invisible at -m:1; fires reliably in graphs with ~10+ parallel android projects.
Environment
- .NET SDK 10.0.201
- Microsoft.Android.Sdk.Linux 36.1.53 (also reproduced with 36.x line generally)
- Linux x64 (WSL2 Ubuntu, 32 logical cores) -- also reproduced on hosted GitHub Actions ubuntu-24.04 (4 cores) intermittently
- MSBuild parallelism default (
-m)
Minimal repro (shows the dispatch pattern)
A single net10.0-android library with GeneratePackageOnBuild=true is enough:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
</Project>
dotnet build -bl shows _CreateAar running in two distinct Project evaluations of the same csproj:
[..47.418] _CreateAar project id=239 parent=_UpdateAndroidResources ← from regular Build
[..49.472] _CreateAar project id=375 parent=_UpdateAndroidResources, entry=_GetFrameworkAssemblyReferences
← Pack-dispatched re-evaluation
Same project, same TargetFramework, but two distinct project instances. The second was MSBuild-task-dispatched from NuGet pack's per-TFM inner-target chain after the regular Build completed. On a single node with cleanly-stamped state, the second _CreateAar no-ops -- the dispatch pattern is established but the race window is zero. Anything that puts the second dispatch on a fresh MSBuild node, or lands it inside the first's write window, flips it to a real concurrent write. The Stride evidence below shows it firing.
Root cause
- Writer (
CreateAar.cs:50): File.Create(OutputFile) -- FileShare.None, held for the whole zip-write.
- Reader (
Files.cs:552): File.OpenRead -- FileShare.Read; fails while writer holds exclusive.
- Producer's
_CreateAar runs from multiple paths: its own Build chain, plus NuGet pack's per-TFM inner-target dispatches (via _UpdateAndroidResourcesDependsOn) on a fresh MSBuild node with no cached project state.
- Consumer's
_ResolveLibraryProjectImports has no MSBuild edge to the second producer evaluation.
NuGet pack only exposes the bug -- any cross-targeted producer with GeneratePackageOnBuild=true (the default in many SDK templates and engines) hits it once parallel build spawns >1 worker node.
Possible fixes (AI investigation)
One-liners; happy to expand on any of these or post draft diffs.
- Atomic write in
_CreateAar -- write to temp, File.Replace to final. Closes the IOException; doesn't close redundant re-fire. ~20 lines.
- Content-idempotent
_CreateAar -- task hashes inputs, skips write when stamp matches. Closes both the race and the redundant work. ~30 lines + a stamp file.
- Reorganize target ordering to avoid the double
_CreateAar invocation -- pack's per-TFM inner-target dispatches transitively re-fire _CreateAar on a fresh MSBuild node; if pack can be wired to not pull _CreateAar into its dependency closure (or to reuse the producer's already-completed Build result), the redundant write goes away. Open as to exact mechanism --likely in BuildOrder.targets or pack's TargetsForTfmSpecificContentInPackage chain.
- Preserve
lp/* mtimes on identical re-extract in _ResolveLibraryProjectImports -- removes the spurious trigger of _CreateAar's incremental check. Closes both the race and redundant work. Medium scope; cold-extract path slightly slower.
- Retry-on-shared-violation in
Files.HashFile -- pure defense-in-depth on the reader. Closes the IOException; doesn't address producer-side waste. ~10 lines.
Likely combinations: #1 alone (smallest correctness fix), #4 alone (cleanest root fix), or #1 + #5 (belt-and-braces).
Happy to share the full binlog privately if helpful for diagnosis, and to open a draft PR for whichever option fits best.
Android framework version
net10.0-android
Affected platform version
VS 2026, .NET 10.0
Description
Under parallel build, NuGet Pack's per-TFM inner-target dispatches re-evaluate a producing net10.0-android library on a fresh MSBuild node, causing
_CreateAarto re-writebin/.../<assembly>.aarnon-atomically while consumers concurrently read it viaFiles.HashFile:Invisible at
-m:1; fires reliably in graphs with ~10+ parallel android projects.Environment
-m)Minimal repro (shows the dispatch pattern)
A single
net10.0-androidlibrary withGeneratePackageOnBuild=trueis enough:dotnet build -blshows_CreateAarrunning in two distinct Project evaluations of the same csproj:Same project, same
TargetFramework, but two distinct project instances. The second was MSBuild-task-dispatched from NuGet pack's per-TFM inner-target chain after the regular Build completed. On a single node with cleanly-stamped state, the second_CreateAarno-ops -- the dispatch pattern is established but the race window is zero. Anything that puts the second dispatch on a fresh MSBuild node, or lands it inside the first's write window, flips it to a real concurrent write. The Stride evidence below shows it firing.Root cause
CreateAar.cs:50):File.Create(OutputFile)--FileShare.None, held for the whole zip-write.Files.cs:552):File.OpenRead--FileShare.Read; fails while writer holds exclusive._CreateAarruns from multiple paths: its own Build chain, plus NuGet pack's per-TFM inner-target dispatches (via_UpdateAndroidResourcesDependsOn) on a fresh MSBuild node with no cached project state._ResolveLibraryProjectImportshas no MSBuild edge to the second producer evaluation.NuGet pack only exposes the bug -- any cross-targeted producer with
GeneratePackageOnBuild=true(the default in many SDK templates and engines) hits it once parallel build spawns >1 worker node.Possible fixes (AI investigation)
One-liners; happy to expand on any of these or post draft diffs.
_CreateAar-- write to temp,File.Replaceto final. Closes the IOException; doesn't close redundant re-fire. ~20 lines._CreateAar-- task hashes inputs, skips write when stamp matches. Closes both the race and the redundant work. ~30 lines + a stamp file._CreateAarinvocation -- pack's per-TFM inner-target dispatches transitively re-fire_CreateAaron a fresh MSBuild node; if pack can be wired to not pull_CreateAarinto its dependency closure (or to reuse the producer's already-completed Build result), the redundant write goes away. Open as to exact mechanism --likely inBuildOrder.targetsor pack'sTargetsForTfmSpecificContentInPackagechain.lp/*mtimes on identical re-extract in_ResolveLibraryProjectImports-- removes the spurious trigger of_CreateAar's incremental check. Closes both the race and redundant work. Medium scope; cold-extract path slightly slower.Files.HashFile-- pure defense-in-depth on the reader. Closes the IOException; doesn't address producer-side waste. ~10 lines.Likely combinations: #1 alone (smallest correctness fix), #4 alone (cleanest root fix), or #1 + #5 (belt-and-braces).
Happy to share the full binlog privately if helpful for diagnosis, and to open a draft PR for whichever option fits best.