Skip to content

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

@jonathanpeppers

Description

@jonathanpeppers

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

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:

<Target Name="_GenerateTrimmableTypeMap"
    Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable'
                and '@(ReferencePath->Count())' != '0'
                and '$(_OuterIntermediateOutputPath)' == '' "
    AfterTargets="CoreCompile"
    ... />

But the post-trim target _GeneratePostTrimTrimmableTypeMapJavaSources in Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets has no _OuterIntermediateOutputPath == '' guard:

<Target Name="_GeneratePostTrimTrimmableTypeMapJavaSources"
    Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable'
                and '$(PublishTrimmed)' == 'true'
                and Exists('$(IntermediateOutputPath)linked/Link.semaphore') "
    AfterTargets="ILLink"
    BeforeTargets="_GenerateJavaStubs;_CompileJava;_CompileToDalvik"
    Inputs="$(IntermediateOutputPath)linked/Link.semaphore"
    Outputs="$(_PostTrimTrimmableTypeMapJavaStamp)">
  ...
  <GenerateTrimmableTypeMap
      ...
      JavaSourceOutputDirectory="$(_PostTrimTypeMapJavaOutputDirectory)"
      ...
      CleanJavaSourceOutputDirectory="true"
      ... />

So it runs in every parallel inner per-RID build, each one invoking GenerateTrimmableTypeMap with CleanJavaSourceOutputDirectory="true". The task does:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions