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)
-
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).
-
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.
-
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
Summary
When
_AndroidTypeMapImplementation=trimmableis used withPublishTrimmed=trueand the project has multipleRuntimeIdentifiers(the default for CoreCLR Release isandroid-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:Observed failure
Xamarin.Android.Build.Tests.TrimmableTypeMapBuildTests.Build_WithTrimmableTypeMap_Succeeds(True,CoreCLR)Build_WithTrimmableTypeMap_ArrayRankChangeRegeneratesTypeMap.Root cause (from the attached
build.binlog)The CoreCLR Release default RIDs are
android-arm64;android-x64(seeMicrosoft.Android.Sdk.DefaultProperties.targets), and_AndroidBuildRuntimeIdentifiersInParalleldefaults totrue(Microsoft.Android.Sdk.AssemblyResolution.targets). The_ComputeFilesToPublishForRuntimeIdentifierstarget dispatches the two RIDs as parallel MSBuild inner builds.Both inner builds set
_OuterIntermediateOutputPathand inherit a_PostTrimTypeMapJavaOutputDirectorythat is rooted at the outer intermediate path. From the binlog evaluation properties of the inner android-x64 build that failed:RuntimeIdentifierandroid-x64IntermediateOutputPathobj/Release/android-x64/_OuterIntermediateOutputPathobj/Release/_PostTrimTypeMapJavaOutputDirectoryobj/Release/typemap/linked-javaSo both inner builds compute the same
_PostTrimTypeMapJavaOutputDirectory.The pre-trim target
_GenerateTrimmableTypeMapinMicrosoft.Android.Sdk.TypeMap.Trimmable.targetsalready guards against this:But the post-trim target
_GeneratePostTrimTrimmableTypeMapJavaSourcesinMicrosoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targetshas no_OuterIntermediateOutputPath == ''guard:So it runs in every parallel inner per-RID build, each one invoking
GenerateTrimmableTypeMapwithCleanJavaSourceOutputDirectory="true". The task does:Both inner builds race on the shared outer
obj/Release/typemap/linked-java. The binlog shows the target executed twice on the same csproj: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 tormdir mono/android, the directory was no longer empty →IOException.This is not a macOS Spotlight / fsevents
ENOTEMPTYartifact — it's a real concurrent-process race between two MSBuild inner builds.Suggested fixes (pick one — not all)
Restrict the target to the outer build (cleanest, mirrors
_GenerateTrimmableTypeMap). Addand '$(_OuterIntermediateOutputPath)' == ''to theCondition. 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).Make
_PostTrimTypeMapJavaOutputDirectoryper-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.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_GenerateTrimmableTypeMapdoes 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
XamarinAndroidApplicationProjectwith_AndroidTypeMapImplementation=trimmable; failure is non-deterministic but reproduces frequently on macOS CI under the default 2-RID inner-build fan-out.cc @jonathanpeppers