[Xamarin.Android.Build.Tasks] `_GeneratePostTrimTrimmableTypeMapJavaSources` races between parallel inner per-RID builds
- Dominant language
- C#
- Stars
- 2.1k
- Forks
- 579
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 257
Description
### Summary
When `_AndroidTypeMapImplementation=trimmable` is used with `PublishTrimmed=true` and the project has multiple `RuntimeIdentifiers` (the default for CoreCLR Release is `android-arm64;android-x64`), the post-trim Java-source generation target runs **once per inner per-RID build, in parallel, against a shared outer folder**, and fails non-deterministically with:
```
error XAGTT7024: System.IO.IOException: Directory not empty :
'.../obj/Release/typemap/linked-java/mono/android'
at System.IO.FileSystem.RemoveDirectoryRecursive(String fullPath)
at Xamarin.Android.Tasks.GenerateTrimmableTypeMap.RunTask()
at Microsoft.Android.Build.Tasks.AndroidTask.Execute()
```
### Observed failure
- Test: `Xamarin.Android.Build.Tests.TrimmableTypeMapBuildTests.Build_WithTrimmableTypeMap_Succeeds(True,CoreCLR)`
- Same run also took down `Build_WithTrimmableTypeMap_ArrayRankChangeRegeneratesTypeMap`.
- Build: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1456555&view=ms.vss-test-web.build-test-results-tab&runId=40303352&resultId=100198
### Root cause (from the attached `build.binlog`)
The CoreCLR Release default RIDs are `android-arm64;android-x64` (see `Microsoft.Android.Sdk.DefaultProperties.targets`), and `_AndroidBuildRuntimeIdentifiersInParallel` defaults to `true` (`Microsoft.Android.Sdk.AssemblyResolution.targets`). The `_ComputeFilesToPublishForRuntimeIdentifiers` target dispatches the two RIDs as parallel MSBuild inner builds.
Both inner builds set `_OuterIntermediateOutputPath` and inherit a `_PostTrimTypeMapJavaOutputDirectory` that is rooted at the **outer** intermediate path. From the binlog evaluation properties of the inner android-x64 build that failed:
| Property | Value |
| --- | --- |
| `RuntimeIdentifier` | `android-x64` |
| `IntermediateOutputPath` | `obj/Release/android-x64/` |
| `_OuterIntermediateOutputPath` | `obj/Release/` |
| `_PostTrimTypeMapJavaOutputDirectory` | `obj/Release/typemap/linked-java` |
So both inner builds compute the same `_PostTrimTypeMapJavaOutputDirectory`.
The pre-trim target `_GenerateTrimmableTypeMap` in `Microsoft.Android.Sdk.TypeMap.Trimmable.targets` already guards against this:
```xml
```
But the post-trim target `_GeneratePostTrimTrimmableTypeMapJavaSources` in `Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets` has **no `_OuterIntermediateOutputPath == ''` guard**:
```xml
...
```
So it runs in every parallel inner per-RID build, each one invoking `GenerateTrimmableTypeMap` with `CleanJavaSourceOutputDirectory="true"`. The task does:
```csharp
if (CleanJavaSourceOutputDirectory && Directory.Exists (JavaSourceOutputDirectory)) {
Directory.Delete (JavaSourceOutputDirectory, recursive: true);
}
Directory.CreateDirectory (JavaSourceOutputDirectory);
```
Both inner builds race on the shared outer `obj/Release/typemap/linked-java`. The binlog shows the target executed twice on the same csproj:
| Project ID | RID | Target outcome | Duration |
| --- | --- | --- | --- |
| 105 | (sibling — android-arm64) | succeeded | 55ms |
| 15 | android-x64 | **failed** | 23ms |
The android-x64 build started its recursive delete, but while .NET was walking back up the tree, the android-arm64 build had already finished its own delete and was rewriting children into `mono/android/`. When the x64 recursion tried to `rmdir mono/android`, the directory was no longer empty → `IOException`.
This is **not** a macOS Spotlight / fsevents `ENOTEMPTY` artifact — it's a real concurrent-process race between two MSBuild inner builds.
### Suggested fixes (pick one — not all)
1. **Restrict the target to the outer build** (cleanest, mirrors `_GenerateTrimmableTypeMap`). Add `and '$(_OuterIntermediateOutputPath)' == ''` to the `Condition`. The outer build would need to be the one consuming linked outputs of a designated inner RID, which may not match the current dataflow (linked assemblies are produced per-RID by ILLink).
2. **Make `_PostTrimTypeMapJavaOutputDirectory` per-RID inside inner builds** (e.g. `$(IntermediateOutputPath)typemap/linked-java`), then have the outer packaging phase pick one canonical RID's output, since the Java sources are RID-invariant.
3. **Serialize the inner runs.** Gate the post-trim target to a single chosen RID (e.g. only when `RuntimeIdentifier == [first RID]`), since the typemap Java sources don't depend on the RID. This matches what `_GenerateTrimmableTypeMap` does conceptually (outer-only).
Option 3 is probably smallest and matches the intent: the Java sources written by this target are RID-invariant (just JCWs + `ApplicationRegistration.java` + `acw-map.txt`), so running it once is sufficient.
### Repro
Default CoreCLR Release `XamarinAndroidApplicationProject` with `_AndroidTypeMapImplementation=trimmable`; failure is non-deterministic but reproduces frequently on macOS CI under the default 2-RID inner-build fan-out.
cc @jonathanpeppers
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.