Skip to content

AAR write/read race under parallel build (XARLP7024) #11514

@xen2

Description

@xen2

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.

  1. Atomic write in _CreateAar -- write to temp, File.Replace to final. Closes the IOException; doesn't close redundant re-fire. ~20 lines.
  2. Content-idempotent _CreateAar -- task hashes inputs, skips write when stamp matches. Closes both the race and the redundant work. ~30 lines + a stamp file.
  3. 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.
  4. 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.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: App+Library BuildIssues when building Library projects or Application projects.need-infoIssues that need more information from the author.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions