dotnet / dotnet/android

[Xamarin.Android.Build.Tasks] `_GeneratePostTrimTrimmableTypeMapJavaSources` races between parallel inner per-RID builds

Open
#11,619 0 comments 0 reactions 0 assignees View on GitHub
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.