From 481bb2466897f1ad7b32e1b64c474b39e7046ced Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 5 Mar 2026 09:19:17 -0800 Subject: [PATCH 1/8] Fix AssemblyModifierPipeline ordering: run before R2R/AOT, not after Move _AfterILLinkAdditionalSteps from the outer build into the inner (per-RID) build using AfterTargets="ILLink". This ensures AssemblyModifierPipeline runs on trimmed IL assemblies BEFORE CreateReadyToRunImages/IlcCompile compiles them to native code, preventing assembly modifications from overwriting R2R/AOT native code with pure IL. Add _CopySidecarXmlToAssemblyPaths target to copy .jlo.xml and .typemap.xml sidecar files from linked/ to wherever assemblies end up after R2R/AOT (e.g. R2R/, publish/), so outer-build consumers (_GenerateJavaStubs, GenerateTypeMappings) can find them. Handles: NativeAOT duplicate assemblies (KeepDuplicates="false"), R2R composite assemblies (empty sidecar files), assemblies not in ManagedAssemblyToLink (Touch AlwaysCreate), single-RID vs multi-RID path differences, and framework vs user assembly classification without NuGetPackageId (filter by known framework assembly names). --- .../Xamarin.Android.Common.targets | 159 ++++++++++++++++-- 1 file changed, 149 insertions(+), 10 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 2b0c321c53a..f4aa9718a3c 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1454,33 +1454,172 @@ because xbuild doesn't support framework reference assemblies. - + + AfterTargets="ILLink" + Condition=" '$(PublishTrimmed)' == 'true' and '$(_ComputeFilesToPublishForRuntimeIdentifiers)' == 'true' "> + + + + + + + + + <_AfterILLinkAssemblies + Include="@(ManagedAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" + Condition=" Exists('$(IntermediateLinkDir)%(Filename)%(Extension)') " + KeepDuplicates="false" /> + + + + + <_AfterILLinkAssemblies Update="@(_AfterILLinkAssemblies)" Abi="$(_AfterILLinkAbi)" /> + + + + + <_AfterILLinkUserAssemblies Include="@(_AfterILLinkAssemblies)" + Condition=" '%(Filename)' != 'Mono.Android' and '%(Filename)' != 'Mono.Android.Export' and '%(Filename)' != 'Mono.Android.Runtime' and '%(Filename)' != 'Java.Interop' " /> + + - + + + <_AfterILLinkAssemblies Remove="@(_AfterILLinkAssemblies)" /> + <_AfterILLinkUserAssemblies Remove="@(_AfterILLinkUserAssemblies)" /> + + + + + + + <_R2RCompositeAssemblies Include="@(_ResolvedAssemblies)" Condition=" $([System.String]::Copy('%(Filename)').EndsWith('.r2r')) " /> + <_NonCompositeAssemblies Include="@(_ResolvedAssemblies)" Condition=" !$([System.String]::Copy('%(Filename)').EndsWith('.r2r')) " /> + + + + + + + + <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(IntermediateOutputPath)%(RuntimeIdentifier)\linked\%(Filename).jlo.xml')" /> + <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(IntermediateOutputPath)%(RuntimeIdentifier)\linked\%(Filename).typemap.xml')" /> + + + <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(IntermediateOutputPath)linked\%(Filename).jlo.xml')" /> + <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(IntermediateOutputPath)linked\%(Filename).typemap.xml')" /> + + + + + + + <_SidecarXmlCopyDestination Include="@(_NonCompositeAssemblies->'%(RootDir)%(Directory)%(Filename).jlo.xml')" /> + <_SidecarXmlCopyDestination Include="@(_NonCompositeAssemblies->'%(RootDir)%(Directory)%(Filename).typemap.xml')" /> + + + + + + <_SidecarXmlCopySource Remove="@(_SidecarXmlCopySource)" /> + <_SidecarXmlCopyDestination Remove="@(_SidecarXmlCopyDestination)" /> + <_NonCompositeAssemblies Remove="@(_NonCompositeAssemblies)" /> + <_R2RCompositeAssemblies Remove="@(_R2RCompositeAssemblies)" /> + + + <_GenerateJavaStubsInputs Include="@(_AndroidMSBuildAllProjects)" /> From 689c6c3aa7fc7dd7a74c9f83c3147190bb370b8a Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 5 Mar 2026 15:41:53 -0800 Subject: [PATCH 2/8] Fix root assembly not scanned in NativeAOT builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In NativeAOT builds, the project's own assembly is not in @(ManagedAssemblyToLink) — ILLink passes it as a TrimmerRootAssembly. This caused AssemblyModifierPipeline to skip it, producing no JCW for MainActivity and failing with XA0103. Add the root assembly explicitly to _AfterILLinkAssemblies using Exclude (not KeepDuplicates) to avoid duplicates. KeepDuplicates compares items including metadata, so a bare Include would be considered distinct from an existing item with rich metadata from @(ManagedAssemblyToLink), causing GetPerArchAssemblies() to throw a duplicate key error in CoreCLR builds. Exclude compares by ItemSpec only, correctly deduplicating in both scenarios. Also set HasMonoAndroidReference=true on the root assembly so IsAndroidAssembly() returns true and FindJavaObjectsStep scans it. --- .../Xamarin.Android.Common.targets | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index f4aa9718a3c..103507ad639 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1494,9 +1494,35 @@ because xbuild doesn't support framework reference assemblies. KeepDuplicates="false" /> - + + + <_AfterILLinkAssemblies + Include="$(IntermediateLinkDir)$(TargetName)$(TargetExt)" + Exclude="@(_AfterILLinkAssemblies)" + Condition=" Exists('$(IntermediateLinkDir)$(TargetName)$(TargetExt)') " /> + + + <_AfterILLinkAssemblies Update="@(_AfterILLinkAssemblies)" Abi="$(_AfterILLinkAbi)" /> + <_AfterILLinkAssemblies + Update="$(IntermediateLinkDir)$(TargetName)$(TargetExt)" + HasMonoAndroidReference="true" /> + _AndroidComputeIlcCompileInputs uses. RemoveDuplicates deduplicates by ItemSpec only, + which is needed because NativeAOT builds can have duplicate @(ManagedAssemblyToLink) + entries for the same assembly with different metadata. --> - <_AfterILLinkAssemblies + <_AfterILLinkAssembliesRaw Include="@(ManagedAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" - Condition=" Exists('$(IntermediateLinkDir)%(Filename)%(Extension)') " - KeepDuplicates="false" /> + Condition=" Exists('$(IntermediateLinkDir)%(Filename)%(Extension)') " /> + + + + <_AfterILLinkAssembliesRaw Remove="@(_AfterILLinkAssembliesRaw)" /> <_AfterILLinkAssemblies Remove="@(_AfterILLinkAssemblies)" /> <_AfterILLinkUserAssemblies Remove="@(_AfterILLinkUserAssemblies)" /> From 108841c3f9c5ce8d844193dfee2a143b4b6ec651 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 6 Mar 2026 12:47:38 -0800 Subject: [PATCH 4/8] Fix _CopySidecarXmlToAssemblyPaths path when RuntimeIdentifier is set late When RuntimeIdentifier is set after path evaluation (e.g. via MockPrimaryCpuAbi.targets), IntermediateOutputPath does not contain the RID. The target now detects this and appends the RID explicitly to find sidecar XML files in the correct linked/ directory. --- .../Xamarin.Android.Common.targets | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 1cb5040fd01..33767234af8 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1610,16 +1610,25 @@ because xbuild doesn't support framework reference assemblies. Files="@(_R2RCompositeAssemblies->'%(RootDir)%(Directory)%(Filename).jlo.xml');@(_R2RCompositeAssemblies->'%(RootDir)%(Directory)%(Filename).typemap.xml')" AlwaysCreate="true" /> - + + + <_SidecarLinkedDir Condition=" $(IntermediateOutputPath.Replace('\','/').TrimEnd('/').EndsWith('$(RuntimeIdentifier)')) ">$(IntermediateOutputPath)linked\ + <_SidecarLinkedDir Condition=" '$(_SidecarLinkedDir)' == '' ">$(IntermediateOutputPath)$(RuntimeIdentifier)\linked\ + <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(IntermediateOutputPath)%(RuntimeIdentifier)\linked\%(Filename).jlo.xml')" /> <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(IntermediateOutputPath)%(RuntimeIdentifier)\linked\%(Filename).typemap.xml')" /> - <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(IntermediateOutputPath)linked\%(Filename).jlo.xml')" /> - <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(IntermediateOutputPath)linked\%(Filename).typemap.xml')" /> + <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(_SidecarLinkedDir)%(Filename).jlo.xml')" /> + <_SidecarXmlCopySource Include="@(_NonCompositeAssemblies->'$(_SidecarLinkedDir)%(Filename).typemap.xml')" /> + + + + + + @@ -143,6 +160,8 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. <_AndroidILLinkAssemblies Include="@(ManagedAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" Condition="Exists('$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + + <_AndroidILLinkAssemblies Remove="$(IntermediateLinkDir)$(TargetName)$(TargetExt)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 33767234af8..726f8bd48b5 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1473,10 +1473,24 @@ because xbuild doesn't support framework reference assemblies. are harmlessly satisfied. For non-trimmed builds, _LinkAssembliesNoShrink handles assembly modifications instead. + + Incrementality: Input is $(_LinkSemaphore) (touched by ILLink when it runs). When ILLink is + skipped on a no-change rebuild, the semaphore is unchanged, so this target is also skipped. + This is important because SaveChangedAssemblyStep always updates assembly timestamps (even + when unchanged), which would cascade through _GenerateJavaStubs -> _CompileJava -> + _CompileToDalvik -> _BuildApkEmbed -> _Sign, breaking incremental builds. + + For all runtimes (MonoVM, CoreCLR, NativeAOT), the user assembly is in @(ManagedAssemblyToLink) + which is part of _RunILLink's Inputs, so $(_LinkSemaphore) correctly updates whenever the user + assembly changes. For NativeAOT specifically, the standard SDK targets strip the user assembly + from @(ManagedAssemblyToLink), but _AndroidFixManagedAssemblyToLinkForILLink in + Microsoft.Android.Sdk.NativeAOT.targets adds it back. --> + Condition=" '$(PublishTrimmed)' == 'true' and '$(_ComputeFilesToPublishForRuntimeIdentifiers)' == 'true' " + Inputs="$(_LinkSemaphore)" + Outputs="$(_AdditionalPostLinkerStepsFlag)"> @@ -1496,18 +1510,11 @@ because xbuild doesn't support framework reference assemblies. - + <_AfterILLinkAssemblies Include="$(IntermediateLinkDir)$(TargetName)$(TargetExt)" @@ -1563,6 +1570,8 @@ because xbuild doesn't support framework reference assemblies. TargetName="$(TargetName)"> + + <_AfterILLinkAssembliesRaw Remove="@(_AfterILLinkAssembliesRaw)" /> From 407e356dcf402ddf3c876edf5550addaca843adc Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Sun, 8 Mar 2026 00:02:26 -0800 Subject: [PATCH 6/8] Fix _CopySidecarXmlToAssemblyPaths when linked/ directory does not exist When switching RuntimeIdentifier between builds without cleaning (e.g. ChangeSupportedAbis test switches from android-arm64 to android-x64), the inner build may run for the old RID while the outer build expects the new RID's linked/ directory. The Touch task fails with MSB3371 because it cannot create files in a non-existent directory. Add MakeDir before Touch to ensure the linked/ directory exists. Fixes: ChangeSupportedAbis(NativeAOT) --- .../Xamarin.Android.Common.targets | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 726f8bd48b5..482a8516728 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1643,7 +1643,15 @@ because xbuild doesn't support framework reference assemblies. + them so the Copy below doesn't fail. Zero-length = "not scanned" which is correct. + + The linked/ directory may not exist if the RID changed between builds without a clean + (e.g. switching from android-arm64 to android-x64 via RuntimeIdentifier parameter while + RuntimeIdentifiers still points to the old RID). In that case the inner build ran for the + old RID and never created the new RID's linked/ directory. MakeDir ensures it exists. --> + Date: Mon, 9 Mar 2026 16:26:03 -0700 Subject: [PATCH 7/8] Fix CheckSignApk(NativeAOT) test expecting warnings on incremental build The second build in CheckSignApk only changes Strings.xml (an Android resource), so assemblies are unchanged and ILLink correctly skips. With the _AfterILLinkAdditionalSteps incrementality fix, IL3053 warnings no longer appear on no-code-change rebuilds. Update the test to expect no warnings for all runtimes on the second build. --- .../Tests/Xamarin.Android.Build.Tests/PackagingTest.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index b0dcc115436..dc7591d44e9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -560,11 +560,9 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ item.TextContent = () => proj.StringsXml.Replace ("${PROJECT_NAME}", "Foo"); item.Timestamp = null; Assert.IsTrue (b.Build (proj), "Second build failed"); - if (runtime != AndroidRuntime.NativeAOT) { - b.AssertHasNoWarnings (); - } else { - StringAssertEx.Contains ("2 Warning(s)", b.LastBuildOutput, "NativeAOT should produce two IL3053 warnings"); - } + // Only Strings.xml changed, so assemblies are unchanged and ILLink + // correctly skips — no IL3053 warnings are expected for any runtime. + b.AssertHasNoWarnings (); //Make sure the APKs are signed foreach (var apk in Directory.GetFiles (bin, "*-Signed.apk")) { From 2cdd2d923fa8e990f7f3c59273ffe272c5b7872a Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 12 Mar 2026 10:21:35 -0700 Subject: [PATCH 8/8] Track sidecar XML files in @(FileWrites) to prevent IncrementalClean deletion --- .../Xamarin.Android.Common.targets | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 482a8516728..66056052839 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1572,6 +1572,11 @@ because xbuild doesn't support framework reference assemblies. + + + + + <_AfterILLinkAssembliesRaw Remove="@(_AfterILLinkAssembliesRaw)" /> @@ -1667,6 +1672,10 @@ because xbuild doesn't support framework reference assemblies. DestinationFiles="@(_SidecarXmlCopyDestination)" SkipUnchangedFiles="true" /> + + + + <_SidecarXmlCopySource Remove="@(_SidecarXmlCopySource)" /> <_SidecarXmlCopyDestination Remove="@(_SidecarXmlCopyDestination)" />