From 481bb2466897f1ad7b32e1b64c474b39e7046ced Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 5 Mar 2026 09:19:17 -0800 Subject: [PATCH 01/13] 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 02/13] 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 04/13] 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 06/13] 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 07/13] 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 08/13] 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)" /> From 3974885eb2b84a41db23124cdbbf4d4cc0ea4055 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 24 Mar 2026 14:37:36 -0700 Subject: [PATCH 09/13] Fix PostTrimmingPipeline file-locking on parallel multi-RID builds Filter _PostTrimmingAssembly by PostprocessAssembly=true metadata instead of all .dll files from @(ResolvedFileToPublish). The unfiltered list included NuGet satellite assemblies (e.g. Microsoft.Maui.Controls.resources.dll) that point to shared paths in the NuGet cache. When parallel inner builds for multiple RIDs both opened these shared files with ReadWrite access, it caused an IOException file-locking conflict on Windows (XAPTP7000 wrapping XA0009). PostprocessAssembly=true is the same metadata gate that ILLink uses to select which assemblies to process, so this gives us exactly the trimmed assemblies from $(IntermediateLinkDir) without coupling to ILLink internals. --- .../Microsoft.Android.Sdk.TypeMap.LlvmIr.targets | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index 84e68d8bf6f..43a2f3009df 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -244,22 +244,20 @@ - - <_PostTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + <_PostTrimmingAssembly Include="@(ResolvedFileToPublish)" + Condition=" '%(PostprocessAssembly)' == 'true' " /> + + <_PostTrimmingAssembly Remove="@(_PostTrimmingAssembly)" /> + From 4667c1f38dcdd281468a8b00342e52ff0fe1b60c Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 24 Mar 2026 21:19:38 -0700 Subject: [PATCH 10/13] Fix MSB4096 by using WithMetadataValue instead of batching condition The %(PostprocessAssembly) batching condition on the ItemGroup Include triggers MSB4096 when any item in @(ResolvedFileToPublish) lacks the PostprocessAssembly metadata (e.g. .runtimeconfig.json). Switch to the WithMetadataValue() item function which safely skips items without the metadata. --- .../targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index 43a2f3009df..c272c2e8d6d 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -248,8 +248,7 @@ AfterTargets="ILLink" Condition=" '$(PublishTrimmed)' == 'true' "> - <_PostTrimmingAssembly Include="@(ResolvedFileToPublish)" - Condition=" '%(PostprocessAssembly)' == 'true' " /> + <_PostTrimmingAssembly Include="@(ResolvedFileToPublish->WithMetadataValue('PostprocessAssembly', 'true'))" /> Date: Thu, 26 Mar 2026 15:42:12 -0700 Subject: [PATCH 11/13] Fix target ordering: run _PostTrimmingPipeline before _AfterILLinkAdditionalSteps Both targets previously used AfterTargets="ILLink", making their relative execution order depend on import order. Change _AfterILLinkAdditionalSteps to AfterTargets="_PostTrimmingPipeline" so the post-trimming pipeline (strip embedded resources, add keep-alives) always runs first. This prevents the assembly modifier pipeline from reading stale MVIDs. --- src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets | 2 +- 1 file changed, 1 insertion(+), 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 026140ffafc..c37640340a8 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1487,7 +1487,7 @@ because xbuild doesn't support framework reference assemblies. Microsoft.Android.Sdk.NativeAOT.targets adds it back. --> From 32a9154935da16e131012ced8b52a670c7ac21eb Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 30 Mar 2026 15:09:51 -0700 Subject: [PATCH 12/13] Merge PostTrimmingPipeline into AssemblyModifierPipeline Eliminate the separate PostTrimmingPipeline MSBuild task by moving all its steps (CheckForObsoletePreserveAttribute, StripEmbeddedLibraries, PostTrimmingAddKeepAlives, RemoveResourceDesigner) into AssemblyModifierPipeline. This gives trimmed builds a single unified pipeline after ILLink instead of two sequential ones. Introduce a virtual BuildAssemblyModificationSteps method that separates the variable steps (trimmed vs non-trimmed) from the common steps (FindJavaObjects, SaveChangedAssembly, FindTypeMapObjects). LinkAssembliesNoShrink now overrides BuildAssemblyModificationSteps instead of BuildPipeline. --- .../CheckForObsoletePreserveAttributeStep.cs | 2 +- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 17 ---- .../Tasks/AssemblyModifierPipeline.cs | 66 ++++++++++++- .../Tasks/LinkAssembliesNoShrink.cs | 7 +- .../Tasks/PostTrimmingPipeline.cs | 98 ------------------- .../Xamarin.Android.Build.Tasks.csproj | 4 +- .../Xamarin.Android.Common.targets | 13 ++- 7 files changed, 75 insertions(+), 132 deletions(-) delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/CheckForObsoletePreserveAttributeStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/CheckForObsoletePreserveAttributeStep.cs index fb80578ef9a..9099132cf6c 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/CheckForObsoletePreserveAttributeStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/CheckForObsoletePreserveAttributeStep.cs @@ -9,7 +9,7 @@ namespace MonoDroid.Tuner; /// /// Post-trimming step that warns when an assembly references the obsolete -/// Android.Runtime.PreserveAttribute. Runs as part of PostTrimmingPipeline +/// Android.Runtime.PreserveAttribute. Runs as part of AssemblyModifierPipeline /// so the assemblies are already loaded by Mono.Cecil and the check is free. /// class CheckForObsoletePreserveAttributeStep : IAssemblyModifierPipelineStep diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index 0a8b730a336..8de11e61c0c 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -7,7 +7,6 @@ - <_RemoveRegisterFlag>$(MonoAndroidIntermediateAssemblyDir)shrunk\shrunk.flag @@ -232,22 +231,6 @@ - - - <_PostTrimmingAssembly Include="@(ResolvedFileToPublish->WithMetadataValue('PostprocessAssembly', 'true'))" /> - - - - <_PostTrimmingAssembly Remove="@(_PostTrimmingAssembly)" /> - - - diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs index 08a558de918..14fc35fce0d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs @@ -18,14 +18,24 @@ namespace Xamarin.Android.Tasks; /// -/// This task runs additional "linker steps" that are not part of ILLink. These steps -/// are run *after* the linker has run. Additionally, this task is run by -/// LinkAssembliesNoShrink to modify assemblies when ILLink is not used. +/// This task runs assembly modification steps that are not part of ILLink. +/// +/// For trimmed builds, this runs after ILLink and includes post-trimming steps +/// (CheckForObsoletePreserveAttribute, StripEmbeddedLibraries, AddKeepAlives, +/// RemoveResourceDesigner) followed by common steps (FindJavaObjects, +/// SaveChangedAssembly, FindTypeMapObjects). +/// +/// For non-trimmed builds, LinkAssembliesNoShrink extends this task and overrides +/// BuildAssemblyModificationSteps to add non-trimmed-specific steps instead. /// public class AssemblyModifierPipeline : AndroidTask { public override string TaskPrefix => "AMP"; + public bool AddKeepAlives { get; set; } + + public bool AndroidLinkResources { get; set; } + public string ApplicationJavaClass { get; set; } = ""; public string CodeGenerationTarget { get; set; } = ""; @@ -111,7 +121,6 @@ public override bool RunTask () } } - // Set up the FixAbstractMethodsStep and AddKeepAlivesStep var context = new MSBuildLinkContext (resolver, Log); pipeline = new AssemblyPipeline (resolver); @@ -130,6 +139,8 @@ public override bool RunTask () protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context) { + BuildAssemblyModificationSteps (pipeline, context); + // FindJavaObjectsStep var findJavaObjectsStep = new FindJavaObjectsStep (Log) { ApplicationJavaClass = ApplicationJavaClass, @@ -158,6 +169,53 @@ protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkCont pipeline.Steps.Add (findTypeMapObjectsStep); } + /// + /// Builds the assembly modification steps that run before FindJavaObjects/Save/FindTypeMapObjects. + /// For trimmed builds (default), this adds post-trimming steps. + /// LinkAssembliesNoShrink overrides this for non-trimmed builds. + /// + protected virtual void BuildAssemblyModificationSteps (AssemblyPipeline pipeline, MSBuildLinkContext context) + { + // CheckForObsoletePreserveAttributeStep + pipeline.Steps.Add (new CheckForObsoletePreserveAttributeStep (Log)); + + // StripEmbeddedLibrariesStep + pipeline.Steps.Add (new StripEmbeddedLibrariesStep (Log)); + + // PostTrimmingAddKeepAlivesStep + if (AddKeepAlives) { + var cache = new TypeDefinitionCache (); + + // Memoize the corlib resolution so the attempt (and any error logging) happens at most once, + // regardless of how many assemblies/methods need KeepAlive injection. + AssemblyDefinition? corlibAssembly = null; + bool corlibResolutionAttempted = false; + + pipeline.Steps.Add (new PostTrimmingAddKeepAlivesStep (cache, + () => { + if (!corlibResolutionAttempted) { + corlibResolutionAttempted = true; + try { + corlibAssembly = pipeline.Resolver.Resolve (AssemblyNameReference.Parse ("System.Private.CoreLib")); + } catch (AssemblyResolutionException ex) { + Log.LogErrorFromException (ex, showStackTrace: false); + } + } + return corlibAssembly; + }, + (msg) => Log.LogDebugMessage (msg))); + } + + // RemoveResourceDesignerStep + if (AndroidLinkResources) { + var allAssemblies = new List (SourceFiles.Length); + foreach (var item in SourceFiles) { + allAssemblies.Add (pipeline.Resolver.GetAssembly (item.ItemSpec)); + } + pipeline.Steps.Add (new RemoveResourceDesignerStep (allAssemblies, (msg) => Log.LogDebugMessage (msg))); + } + } + void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination) { var assembly = pipeline.Resolver.GetAssembly (source.ItemSpec); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs index 9dc7e99e635..70deda9c8a5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs @@ -14,11 +14,9 @@ public class LinkAssembliesNoShrink : AssemblyModifierPipeline { public override string TaskPrefix => "LNS"; - public bool AddKeepAlives { get; set; } - public bool UseDesignerAssembly { get; set; } - protected override void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context) + protected override void BuildAssemblyModificationSteps (AssemblyPipeline pipeline, MSBuildLinkContext context) { // FixAbstractMethodsStep var fixAbstractMethodsStep = new FixAbstractMethodsStep (); @@ -38,9 +36,6 @@ protected override void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkCon addKeepAliveStep.Initialize (context); pipeline.Steps.Add (addKeepAliveStep); } - - // Ensure the task's steps are added - base.BuildPipeline (pipeline, context); } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs deleted file mode 100644 index 56fb01b6485..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs +++ /dev/null @@ -1,98 +0,0 @@ -#nullable enable - -using System.Collections.Generic; -using System.IO; -using Java.Interop.Tools.Cecil; -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Framework; -using Mono.Cecil; -using MonoDroid.Tuner; - -namespace Xamarin.Android.Tasks; - -/// -/// An MSBuild task that runs post-trimming assembly modifications in a single pass. -/// -/// This opens each assembly once (via DirectoryAssemblyResolver with ReadWrite) and -/// runs all registered steps on it, then writes modified assemblies in-place. Currently -/// runs CheckForObsoletePreserveAttributeStep, StripEmbeddedLibrariesStep and -/// (optionally) AddKeepAlivesStep. -/// -/// Runs in the inner build after ILLink but before ReadyToRun/crossgen2 compilation, -/// so that R2R images are generated from the already-modified assemblies. -/// -public class PostTrimmingPipeline : AndroidTask -{ - public override string TaskPrefix => "PTP"; - - [Required] - public ITaskItem [] Assemblies { get; set; } = []; - - public bool AddKeepAlives { get; set; } - - public bool AndroidLinkResources { get; set; } - - public bool Deterministic { get; set; } - - public override bool RunTask () - { - using var resolver = new DirectoryAssemblyResolver ( - this.CreateTaskLogger (), loadDebugSymbols: true, - loadReaderParameters: new ReaderParameters { ReadWrite = true }); - var cache = new TypeDefinitionCache (); - - foreach (var assembly in Assemblies) { - var dir = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec) ?? ""); - if (!resolver.SearchDirectories.Contains (dir)) { - resolver.SearchDirectories.Add (dir); - } - } - - var steps = new List (); - steps.Add (new CheckForObsoletePreserveAttributeStep (Log)); - steps.Add (new StripEmbeddedLibrariesStep (Log)); - if (AddKeepAlives) { - // Memoize the corlib resolution so the attempt (and any error logging) happens at most once, - // regardless of how many assemblies/methods need KeepAlive injection. - AssemblyDefinition? corlibAssembly = null; - bool corlibResolutionAttempted = false; - steps.Add (new PostTrimmingAddKeepAlivesStep (cache, - () => { - if (!corlibResolutionAttempted) { - corlibResolutionAttempted = true; - try { - corlibAssembly = resolver.Resolve (AssemblyNameReference.Parse ("System.Private.CoreLib")); - } catch (AssemblyResolutionException ex) { - Log.LogErrorFromException (ex, showStackTrace: false); - } - } - return corlibAssembly; - }, - (msg) => Log.LogDebugMessage (msg))); - } - if (AndroidLinkResources) { - var allAssemblies = new List (Assemblies.Length); - foreach (var item in Assemblies) { - allAssemblies.Add (resolver.GetAssembly (item.ItemSpec)); - } - steps.Add (new RemoveResourceDesignerStep (allAssemblies, (msg) => Log.LogDebugMessage (msg))); - } - - foreach (var item in Assemblies) { - var assembly = resolver.GetAssembly (item.ItemSpec); - var context = new StepContext (item, item); - foreach (var step in steps) { - step.ProcessAssembly (assembly, context); - } - if (context.IsAssemblyModified) { - Log.LogDebugMessage ($" Writing modified assembly: {item.ItemSpec}"); - assembly.Write (new WriterParameters { - WriteSymbols = assembly.MainModule.HasSymbols, - DeterministicMvid = Deterministic, - }); - } - } - - return !Log.HasLoggedErrors; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index a0ed0a47ead..d63b6bfdc4c 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -44,8 +44,8 @@ - - + + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 9280a483262..edd190afa40 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1457,9 +1457,12 @@ because xbuild doesn't support framework reference assemblies. @@ -1555,6 +1558,8 @@ because xbuild doesn't support framework reference assemblies. Date: Tue, 31 Mar 2026 09:50:53 -0700 Subject: [PATCH 13/13] Fix incrementality issues in sidecar XML handling and ILLink guard Address PR review feedback: - Only touch sidecar XML files that don't already exist (both R2R composite and linked/ source sidecars), preserving timestamps for incremental builds - Add Exists guard on $(_LinkSemaphore) to _AfterILLinkAdditionalSteps condition to prevent running when ILLink is disabled or semaphore is absent --- .../Xamarin.Android.Common.targets | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index edd190afa40..6d187855e88 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1491,7 +1491,7 @@ because xbuild doesn't support framework reference assemblies. --> @@ -1623,10 +1623,21 @@ because xbuild doesn't support framework reference assemblies. <_NonCompositeAssemblies Include="@(_ResolvedAssemblies)" Condition=" !$([System.String]::Copy('%(Filename)').EndsWith('.r2r')) " /> - + + + <_R2RCompositeSidecarFiles Include="@(_R2RCompositeAssemblies->'%(RootDir)%(Directory)%(Filename).jlo.xml')" /> + <_R2RCompositeSidecarFiles Include="@(_R2RCompositeAssemblies->'%(RootDir)%(Directory)%(Filename).typemap.xml')" /> + + + + + <_MissingR2RCompositeSidecarFiles Include="@(_R2RCompositeSidecarFiles)" Condition=" !Exists('%(Identity)') " /> + + + + old RID and never created the new RID's linked/ directory. MakeDir ensures it exists. + + Only touch files that don't already exist to preserve timestamps and avoid breaking + incremental builds (Copy SkipUnchangedFiles="true" compares timestamps). --> + + <_MissingSidecarXmlCopySource Include="@(_SidecarXmlCopySource)" Condition=" !Exists('%(Identity)') " /> + + Condition=" '@(_MissingSidecarXmlCopySource)' != '' " /> <_SidecarXmlCopyDestination Include="@(_NonCompositeAssemblies->'%(RootDir)%(Directory)%(Filename).jlo.xml')" /> @@ -1683,9 +1700,12 @@ because xbuild doesn't support framework reference assemblies. <_SidecarXmlCopySource Remove="@(_SidecarXmlCopySource)" /> + <_MissingSidecarXmlCopySource Remove="@(_MissingSidecarXmlCopySource)" /> <_SidecarXmlCopyDestination Remove="@(_SidecarXmlCopyDestination)" /> <_NonCompositeAssemblies Remove="@(_NonCompositeAssemblies)" /> <_R2RCompositeAssemblies Remove="@(_R2RCompositeAssemblies)" /> + <_R2RCompositeSidecarFiles Remove="@(_R2RCompositeSidecarFiles)" /> + <_MissingR2RCompositeSidecarFiles Remove="@(_MissingR2RCompositeSidecarFiles)" />