From 70f7f30284cf6821a0299e046e17688ce6d3bb44 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 30 Mar 2026 16:55:43 -0700 Subject: [PATCH 01/13] [illink] Move FixLegacyResourceDesignerStep to post-trim pipeline Migrate FixLegacyResourceDesignerStep out of the ILLink trimmer process and into PostTrimmingPipeline, continuing the work in #10842 to remove custom ILLink steps. The step now runs after ILLink via a thin wrapper (PostTrimmingFixLegacyResourceDesignerStep) that calls ProcessAssemblyDesigner() directly, matching the former ILLink behavior. - Remove all #if ILLINK conditionals from FixLegacyResourceDesignerStep, LinkDesignerBase, and BaseStep - Remove FixLegacyResourceDesignerStep and LinkDesignerBase from the ILLink project (no longer compiled as a trimmer step) - Add UseDesignerAssembly property to PostTrimmingPipeline task - Wire AndroidUseDesignerAssembly through targets to PostTrimmingPipeline - Remove _TrimmerCustomSteps entry for FixLegacyResourceDesignerStep --- .../Microsoft.Android.Sdk.ILLink.csproj | 2 -- .../Linker/External/Linker.Steps/BaseStep.cs | 4 --- .../FixLegacyResourceDesignerStep.cs | 19 +---------- .../MonoDroid.Tuner/LinkDesignerBase.cs | 20 ++--------- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 9 ++--- .../Tasks/PostTrimmingPipeline.cs | 33 +++++++++++++++++++ .../Xamarin.Android.Build.Tasks.csproj | 2 +- 7 files changed, 39 insertions(+), 50 deletions(-) diff --git a/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj b/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj index 26d2067d1fc..5a8cef363e4 100644 --- a/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj +++ b/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj @@ -17,8 +17,6 @@ - - diff --git a/src/Xamarin.Android.Build.Tasks/Linker/External/Linker.Steps/BaseStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/External/Linker.Steps/BaseStep.cs index c1aa1ded94a..a3a5bb11066 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/External/Linker.Steps/BaseStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/External/Linker.Steps/BaseStep.cs @@ -89,11 +89,7 @@ public virtual void LogMessage (string message) public virtual void LogError (int code, string message) { -#if ILLINK - Context.LogMessage (MessageContainer.CreateCustomErrorMessage (message, code, origin: new MessageOrigin ())); -#else // !ILLINK Context.LogError ($"XA{code}", message); -#endif // !ILLINK } } } diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs index 7606f022e7a..7028aea33e0 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs @@ -14,18 +14,11 @@ using Mono.Linker.Steps; using Mono.Tuner; -#if ILLINK -using Resources = Microsoft.Android.Sdk.ILLink.Properties.Resources; -#else // !ILLINK using Resources = Xamarin.Android.Tasks.Properties.Resources; -#endif // ILLINK namespace MonoDroid.Tuner { - public class FixLegacyResourceDesignerStep : LinkDesignerBase -#if !ILLINK - , Xamarin.Android.Tasks.IAssemblyModifierPipelineStep -#endif // !ILLINK + public class FixLegacyResourceDesignerStep : LinkDesignerBase, Xamarin.Android.Tasks.IAssemblyModifierPipelineStep { internal const string DesignerAssemblyName = "_Microsoft.Android.Resource.Designer"; internal const string DesignerAssemblyNamespace = "_Microsoft.Android.Resource.Designer"; @@ -36,14 +29,6 @@ public class FixLegacyResourceDesignerStep : LinkDesignerBase Dictionary lookup; Dictionary lookupCaseInsensitive; - protected override void EndProcess () - { - if (designerAssembly != null) { - LogMessage ($" Setting Action on {designerAssembly.Name} to Link."); - Annotations.SetAction (designerAssembly, AssemblyAction.Link); - } - } - protected override void LoadDesigner () { if (designerLoaded) @@ -72,7 +57,6 @@ protected override void LoadDesigner () } } -#if !ILLINK public void ProcessAssembly (AssemblyDefinition assembly, Xamarin.Android.Tasks.StepContext context) { // Only run this step on non-main user Android assemblies @@ -81,7 +65,6 @@ public void ProcessAssembly (AssemblyDefinition assembly, Xamarin.Android.Tasks. context.IsAssemblyModified |= ProcessAssemblyDesigner (assembly); } -#endif // !ILLINK internal override bool ProcessAssemblyDesigner (AssemblyDefinition assembly) { diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs index 5f7f7fc1f74..6e50b10d7d7 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs @@ -4,16 +4,12 @@ using Mono.Linker; using Mono.Linker.Steps; using System; -using System.Linq; using Xamarin.Android.Tasks; using System.Collections.Generic; using System.Globalization; using Mono.Cecil.Cil; using System.Text.RegularExpressions; using Mono.Collections.Generic; -#if ILLINK -using Microsoft.Android.Sdk.ILLink; -#endif namespace MonoDroid.Tuner { @@ -21,26 +17,14 @@ public abstract class LinkDesignerBase : BaseStep { protected IMetadataResolver Cache => Context; - public -#if !ILLINK - override -#endif - void LogMessage (string message) + public override void LogMessage (string message) { Context.LogMessage (message); } - public -#if !ILLINK - override -#endif - void LogError (int code, string error) + public override void LogError (int code, string error) { -#if ILLINK - Context.LogMessage (MessageContainer.CreateCustomErrorMessage (error, code, origin: new MessageOrigin ())); -#else // !ILLINK Context.LogError ($"XA{code}", error); -#endif // !ILLINK } public virtual AssemblyDefinition Resolve (AssemblyNameReference name) 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 9b8870a8295..da747461e70 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 @@ -198,12 +198,6 @@ <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.FixAbstractMethodsStep" /> - <_TrimmerCustomSteps - Condition=" '$(AndroidUseDesignerAssembly)' == 'true' " - Include="$(_AndroidLinkerCustomStepAssembly)" - BeforeStep="MarkStep" - Type="MonoDroid.Tuner.FixLegacyResourceDesignerStep" - /> <_TrimmerCustomSteps Condition=" '$(_AndroidTypeMapImplementation)' == 'managed' " Include="$(_AndroidLinkerCustomStepAssembly)" @@ -248,7 +242,8 @@ Assemblies="@(_PostTrimmingAssembly)" AddKeepAlives="$(AndroidAddKeepAlives)" AndroidLinkResources="$(AndroidLinkResources)" - Deterministic="$(Deterministic)" /> + Deterministic="$(Deterministic)" + UseDesignerAssembly="$(AndroidUseDesignerAssembly)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs index 56fb01b6485..645b4fe4489 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs @@ -6,6 +6,7 @@ using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Mono.Cecil; +using Mono.Linker; using MonoDroid.Tuner; namespace Xamarin.Android.Tasks; @@ -34,6 +35,8 @@ public class PostTrimmingPipeline : AndroidTask public bool Deterministic { get; set; } + public bool UseDesignerAssembly { get; set; } + public override bool RunTask () { using var resolver = new DirectoryAssemblyResolver ( @@ -70,6 +73,15 @@ public override bool RunTask () }, (msg) => Log.LogDebugMessage (msg))); } + if (UseDesignerAssembly) { + // Create an MSBuildLinkContext so FixLegacyResourceDesignerStep can resolve assemblies + // and log messages. The resolver is owned by the outer 'using' block, so we intentionally + // do not dispose this context (LinkContext.Dispose would double-dispose the resolver). + var linkContext = new MSBuildLinkContext (resolver, Log); + var fixLegacyStep = new FixLegacyResourceDesignerStep (); + fixLegacyStep.Initialize (linkContext); + steps.Add (new PostTrimmingFixLegacyResourceDesignerStep (fixLegacyStep)); + } if (AndroidLinkResources) { var allAssemblies = new List (Assemblies.Length); foreach (var item in Assemblies) { @@ -96,3 +108,24 @@ public override bool RunTask () return !Log.HasLoggedErrors; } } + +/// +/// Thin wrapper around for the post-trimming pipeline. +/// Calls directly, matching the +/// behavior of the former ILLink path which processed all assemblies without StepContext flag filtering. +/// Assemblies without a resource designer are skipped internally by ProcessAssemblyDesigner. +/// +class PostTrimmingFixLegacyResourceDesignerStep : IAssemblyModifierPipelineStep +{ + readonly FixLegacyResourceDesignerStep _inner; + + public PostTrimmingFixLegacyResourceDesignerStep (FixLegacyResourceDesignerStep inner) + { + _inner = inner; + } + + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) + { + context.IsAssemblyModified |= _inner.ProcessAssemblyDesigner (assembly); + } +} 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..843e4879257 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -45,7 +45,7 @@ - + From 77de75d8945d558a96f0aad77a47244abe3920e1 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Wed, 1 Apr 2026 16:18:54 -0700 Subject: [PATCH 02/13] [targets] Fix NativeAOT designer assembly trimming In NativeAOT builds, _AddResourceDesignerToPublishFiles ran after _ComputeManagedAssemblyToLink, so the designer assembly was not in ManagedAssemblyToLink. ILLink skipped the designer assembly entirely and did not rewrite its netstandard references. Fix by adding _AddResourceDesignerToPublishFiles to _PrepareLinking's DependsOnTargets, so the designer is passed to ILLink. --- .../targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 da747461e70..3d9d9713433 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 @@ -160,7 +160,7 @@ + DependsOnTargets="GetReferenceAssemblyPaths;_CreatePropertiesCache;_AddResourceDesignerToPublishFiles"> true <_ExtraTrimmerArgs Condition=" '$(_EnableSerializationDiscovery)' != 'false' ">--enable-serialization-discovery $(_ExtraTrimmerArgs) From f0059fd75f820eedb792e67e0376b35c90421b46 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 2 Apr 2026 09:48:30 -0700 Subject: [PATCH 03/13] Fix merge conflict: remove FixAbstractMethodsStep from ILLink csproj FixAbstractMethodsStep was moved from ILLink to the post-trim pipeline on main (7ae24aab1). The merge conflict resolution incorrectly kept the Compile include, but the file now uses types not available in the ILLink project (IAssemblyModifierPipelineStep, StepContext, Properties.Resources). --- .../Microsoft.Android.Sdk.ILLink.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj b/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj index 5a8cef363e4..6c4368fd7d2 100644 --- a/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj +++ b/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj @@ -16,7 +16,6 @@ - From 64d4ed1f29be377559c1d4e9c5771e40d7a6a7d2 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 2 Apr 2026 15:21:08 -0700 Subject: [PATCH 04/13] Root designer assembly to prevent ILLink from trimming resource properties FixLegacyResourceDesignerStep now runs in PostTrimmingPipeline (after ILLink) instead of as an ILLink custom step. Previously, the step ran before MarkStep and rewrote library assemblies to reference designer properties, which caused MarkStep to preserve them. Now that the rewriting happens after ILLink, we must root the designer assembly so all resource properties survive trimming. Add TrimmerRootAssembly for the designer in _AddResourceDesignerToPublishFiles. This keeps IsTrimmable=true (action=link) so ILLink still rewrites the netstandard assembly reference, but roots all types so nothing is trimmed. An alternative approach would be to run FixLegacyResourceDesignerStep before ILLink instead of after. That would allow ILLink to trim unused resource properties from the designer (since the rewritten references would already be in place for MarkStep), but it would also process resource references in assemblies that ILLink may later remove entirely. --- .../Android/Xamarin.Android.Resource.Designer.targets | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets index 5c154d9e05c..a8a666ab598 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets @@ -226,6 +226,10 @@ Copyright (C) 2016 Xamarin. All rights reserved. In additon we MUST set the `PostprocessAssembly` metadata to `true` so that the file is processed by the ILLink step. If we do not do this then the reference to `netstandard.dll` is not replaced with `System.Private.CoreLib` and the app crashes. + + We also add a TrimmerRootAssembly entry to prevent ILLink from trimming the designer's + resource properties. FixLegacyResourceDesignerStep now runs after ILLink and needs + all properties to be present when rewriting library assemblies. --> true true + + From 924649e83b7f0759734a8443def7453c065186dc Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 2 Apr 2026 17:07:16 -0700 Subject: [PATCH 05/13] Use TrimmerRootDescriptor instead of TrimmerRootAssembly for designer assembly TrimmerRootAssembly (-a flag) makes the designer an entry point, which causes ILLink to retain netstandard.dll as a dependency, leaking it into the APK (+19KB). TrimmerRootDescriptor (-x flag) preserves all designer types/members without making it an entry point, avoiding the regression. --- .../Xamarin.Android.Resource.Designer.targets | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets index a8a666ab598..d847c57568a 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets @@ -227,14 +227,24 @@ Copyright (C) 2016 Xamarin. All rights reserved. is processed by the ILLink step. If we do not do this then the reference to `netstandard.dll` is not replaced with `System.Private.CoreLib` and the app crashes. - We also add a TrimmerRootAssembly entry to prevent ILLink from trimming the designer's - resource properties. FixLegacyResourceDesignerStep now runs after ILLink and needs - all properties to be present when rewriting library assemblies. + We use a TrimmerRootDescriptor (not TrimmerRootAssembly) to prevent ILLink from + trimming the designer's resource properties. FixLegacyResourceDesignerStep runs + after ILLink and needs all properties present when rewriting library assemblies. + A descriptor (-x) preserves types without making the assembly an entry point, + which avoids pulling netstandard.dll into the output. --> + + <_DesignerLinkerDescriptor>$(IntermediateOutputPath)_Microsoft.Android.Resource.Designer.xml + + $(_DesignerAssemblyName).dll @@ -242,9 +252,11 @@ Copyright (C) 2016 Xamarin. All rights reserved. true true - - + + + From 8be3817205b840b703ff331a70e063bd0d0dc26c Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 3 Apr 2026 09:34:23 -0700 Subject: [PATCH 06/13] Update SkiaSharp test to expect XA8000 instead of IL8000 for release builds FixLegacyResourceDesignerStep now runs in PostTrimmingPipeline (MSBuild task) instead of inside ILLink, so the error code is always XA8000 regardless of build configuration. --- tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 04e52ee9839..3edda4515c9 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -1423,7 +1423,7 @@ private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e) // TODO: fix for NativeAOT if (!addResource && runtime != AndroidRuntime.NativeAOT) { Assert.IsFalse (b.Build (app, doNotCleanupOnUpdate: true), $"Build of {app.ProjectName} should have failed."); - Assert.IsTrue (b.LastBuildOutput.ContainsText (isRelease ? "IL8000" : "XA8000")); + Assert.IsTrue (b.LastBuildOutput.ContainsText ("XA8000")); Assert.IsTrue (b.LastBuildOutput.ContainsText ("@styleable/SKCanvasView"), "Expected '@styleable/SKCanvasView' in build output."); Assert.IsTrue (b.LastBuildOutput.ContainsText ("@styleable/SKCanvasView_ignorePixelScaling"), "Expected '@styleable/SKCanvasView_ignorePixelScaling' in build output."); return; From 83a8f64c91f795a6c385e68c9b48cccb2083ee6a Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 6 Apr 2026 11:23:56 -0700 Subject: [PATCH 07/13] Fix incremental build: use WriteOnlyWhenDifferent for linker descriptor WriteLinesToFile was regenerating the TrimmerRootDescriptor XML on every build, changing its timestamp and causing ILLink to re-run on incremental builds. Adding WriteOnlyWhenDifferent preserves the timestamp when the content hasn't changed. --- .../Xamarin/Android/Xamarin.Android.Resource.Designer.targets | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets index d847c57568a..f9ff732238f 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets @@ -243,6 +243,7 @@ Copyright (C) 2016 Xamarin. All rights reserved. From 6e870ef4cc111ef30b0582e2eb02779e0d87ba49 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 6 Apr 2026 11:24:16 -0700 Subject: [PATCH 08/13] Add TODO to simplify TrimmerRootDescriptor once dotnet/runtime#126518 is fixed --- .../Android/Xamarin.Android.Resource.Designer.targets | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets index f9ff732238f..7105bf0286c 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets @@ -232,6 +232,10 @@ Copyright (C) 2016 Xamarin. All rights reserved. after ILLink and needs all properties present when rewriting library assemblies. A descriptor (-x) preserves types without making the assembly an entry point, which avoids pulling netstandard.dll into the output. + See https://github.com/dotnet/runtime/issues/126518 + + TODO: Once dotnet/runtime#126518 is fixed and flows to dotnet/android, + simplify this to use TrimmerRootAssembly instead of the XML descriptor. --> + assembly an entry point, which would pull netstandard.dll into the output. + See https://github.com/dotnet/runtime/issues/126518 --> From 4c4c091f753555c04e352f4b56399f448fc0cabc Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 7 Apr 2026 10:38:45 -0700 Subject: [PATCH 09/13] Move FixLegacyResourceDesignerStep to run before ILLink trimming Run FixLegacyResourceDesignerStep *before* ILLink instead of after it, so the trimmer sees the rewritten IL (call instructions to designer property getters) and can freely trim unused designer types. This removes the need to root the entire designer assembly via a TrimmerRootDescriptor during trimming, eliminating the APK size regression. --- .../Xamarin.Android.Resource.Designer.targets | 28 ++------- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 22 ++++++- .../Tasks/PostTrimmingPipeline.cs | 33 ---------- .../Tasks/PreTrimmingFixLegacyDesigner.cs | 61 +++++++++++++++++++ 4 files changed, 85 insertions(+), 59 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets index 7105bf0286c..e19d986acda 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets @@ -227,29 +227,15 @@ Copyright (C) 2016 Xamarin. All rights reserved. is processed by the ILLink step. If we do not do this then the reference to `netstandard.dll` is not replaced with `System.Private.CoreLib` and the app crashes. - We use a TrimmerRootDescriptor (not TrimmerRootAssembly) to prevent ILLink from - trimming the designer's resource properties. FixLegacyResourceDesignerStep runs - after ILLink and needs all properties present when rewriting library assemblies. - A descriptor (-x) preserves types without making the assembly an entry point, - which avoids pulling netstandard.dll into the output. - See https://github.com/dotnet/runtime/issues/126518 - - TODO: Once dotnet/runtime#126518 is fixed and flows to dotnet/android, - simplify this to use TrimmerRootAssembly instead of the XML descriptor. + No TrimmerRootDescriptor is needed: PreTrimmingFixLegacyDesigner rewrites library + assemblies *before* ILLink, replacing designer field loads with calls to the designer + assembly's property getters. ILLink then naturally preserves only the referenced + properties through its mark step, allowing everything else to be trimmed. --> - - <_DesignerLinkerDescriptor>$(IntermediateOutputPath)_Microsoft.Android.Resource.Designer.xml - - $(_DesignerAssemblyName).dll @@ -257,12 +243,6 @@ Copyright (C) 2016 Xamarin. All rights reserved. true true - - - 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 0c2a2b21f28..7cbc906441f 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 @@ -8,6 +8,7 @@ + <_RemoveRegisterFlag>$(MonoAndroidIntermediateAssemblyDir)shrunk\shrunk.flag @@ -216,6 +217,24 @@ + + + + <_PreTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + + + + + Deterministic="$(Deterministic)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs index df2f1d48b53..fa3c3385737 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs @@ -6,7 +6,6 @@ using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Mono.Cecil; -using Mono.Linker; using MonoDroid.Tuner; namespace Xamarin.Android.Tasks; @@ -35,8 +34,6 @@ public class PostTrimmingPipeline : AndroidTask public bool Deterministic { get; set; } - public bool UseDesignerAssembly { get; set; } - public override bool RunTask () { using var resolver = new DirectoryAssemblyResolver ( @@ -103,15 +100,6 @@ public override bool RunTask () }, (msg) => Log.LogDebugMessage (msg))); } - if (UseDesignerAssembly) { - // Create an MSBuildLinkContext so FixLegacyResourceDesignerStep can resolve assemblies - // and log messages. The resolver is owned by the outer 'using' block, so we intentionally - // do not dispose this context (LinkContext.Dispose would double-dispose the resolver). - var linkContext = new MSBuildLinkContext (resolver, Log); - var fixLegacyStep = new FixLegacyResourceDesignerStep (); - fixLegacyStep.Initialize (linkContext); - steps.Add (new PostTrimmingFixLegacyResourceDesignerStep (fixLegacyStep)); - } foreach (var (item, assembly) in loadedAssemblies) { var context = new StepContext (item, item); @@ -130,24 +118,3 @@ public override bool RunTask () return !Log.HasLoggedErrors; } } - -/// -/// Thin wrapper around for the post-trimming pipeline. -/// Calls directly, matching the -/// behavior of the former ILLink path which processed all assemblies without StepContext flag filtering. -/// Assemblies without a resource designer are skipped internally by ProcessAssemblyDesigner. -/// -class PostTrimmingFixLegacyResourceDesignerStep : IAssemblyModifierPipelineStep -{ - readonly FixLegacyResourceDesignerStep _inner; - - public PostTrimmingFixLegacyResourceDesignerStep (FixLegacyResourceDesignerStep inner) - { - _inner = inner; - } - - public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) - { - context.IsAssemblyModified |= _inner.ProcessAssemblyDesigner (assembly); - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs new file mode 100644 index 00000000000..a1de7d23132 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs @@ -0,0 +1,61 @@ +#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; + +/// +/// Runs on assemblies that are about to be +/// trimmed by ILLink. This rewrites library assemblies so their resource field accesses +/// (ldsfld) become calls to the designer assembly's property getters. +/// +/// Running this *before* ILLink means the trimmer sees the rewritten IL and can freely +/// trim unused designer types/fields. This avoids the need to root the entire designer +/// assembly during trimming (which causes an APK size regression). +/// +public class PreTrimmingFixLegacyDesigner : AndroidTask +{ + public override string TaskPrefix => "PTD"; + + [Required] + public ITaskItem [] Assemblies { 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 }); + + foreach (var assembly in Assemblies) { + var dir = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec) ?? ""); + if (!resolver.SearchDirectories.Contains (dir)) { + resolver.SearchDirectories.Add (dir); + } + } + + var linkContext = new MSBuildLinkContext (resolver, Log); + var fixLegacyStep = new FixLegacyResourceDesignerStep (); + fixLegacyStep.Initialize (linkContext); + + foreach (var item in Assemblies) { + var assembly = resolver.GetAssembly (item.ItemSpec); + if (fixLegacyStep.ProcessAssemblyDesigner (assembly)) { + Log.LogDebugMessage ($" Writing modified assembly: {item.ItemSpec}"); + assembly.Write (new WriterParameters { + WriteSymbols = assembly.MainModule.HasSymbols, + DeterministicMvid = Deterministic, + }); + } + } + + return !Log.HasLoggedErrors; + } +} From 5e3ba1fca05fa582db9990abac5748ac75f2b50b Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 7 Apr 2026 10:25:09 -0700 Subject: [PATCH 10/13] Fix SkiaSharp NativeAOT test to expect XA8000 build failure FixLegacyResourceDesignerStep now runs in PostTrimmingPipeline for all runtimes including NativeAOT, so the missing @styleable/SKCanvasView resources correctly produce XA8000 errors. Remove the NativeAOT exclusion from the addResource=False path. --- tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 3edda4515c9..8a8194f3cfe 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -1420,8 +1420,7 @@ private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e) using (var b = CreateApkBuilder (Path.Combine ("temp", TestName, app.ProjectName))) { b.BuildLogFile = "build1.log"; b.ThrowOnBuildFailure = false; - // TODO: fix for NativeAOT - if (!addResource && runtime != AndroidRuntime.NativeAOT) { + if (!addResource) { Assert.IsFalse (b.Build (app, doNotCleanupOnUpdate: true), $"Build of {app.ProjectName} should have failed."); Assert.IsTrue (b.LastBuildOutput.ContainsText ("XA8000")); Assert.IsTrue (b.LastBuildOutput.ContainsText ("@styleable/SKCanvasView"), "Expected '@styleable/SKCanvasView' in build output."); From 432b1b2f9526b53dca4bec5ab36ca99cf2b8b88d Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 7 Apr 2026 12:45:17 -0700 Subject: [PATCH 11/13] Filter framework/main assemblies in PreTrimmingFixLegacyDesigner for efficiency --- .../Microsoft.Android.Sdk.TypeMap.LlvmIr.targets | 1 + .../Tasks/PreTrimmingFixLegacyDesigner.cs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) 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 7cbc906441f..85b80ea4061 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 @@ -232,6 +232,7 @@ diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs index a1de7d23132..9c49fc6274b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs @@ -1,6 +1,5 @@ #nullable enable -using System.Collections.Generic; using System.IO; using Java.Interop.Tools.Cecil; using Microsoft.Android.Build.Tasks; @@ -26,6 +25,9 @@ public class PreTrimmingFixLegacyDesigner : AndroidTask [Required] public ITaskItem [] Assemblies { get; set; } = []; + [Required] + public string TargetName { get; set; } = ""; + public bool Deterministic { get; set; } public override bool RunTask () @@ -46,6 +48,15 @@ public override bool RunTask () fixLegacyStep.Initialize (linkContext); foreach (var item in Assemblies) { + // Match the filtering in FixLegacyResourceDesignerStep.ProcessAssembly: + // skip the main assembly and framework/BCL assemblies. + if (Path.GetFileNameWithoutExtension (item.ItemSpec) == TargetName) { + continue; + } + if (MonoAndroidHelper.IsFrameworkAssembly (item)) { + continue; + } + var assembly = resolver.GetAssembly (item.ItemSpec); if (fixLegacyStep.ProcessAssemblyDesigner (assembly)) { Log.LogDebugMessage ($" Writing modified assembly: {item.ItemSpec}"); From 7b7261187275f662e4eeb3db28d22fcd0b551fe6 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 10 Apr 2026 10:18:11 -0700 Subject: [PATCH 12/13] Filter pre/post trimming assemblies by PostprocessAssembly metadata Both _PreTrimmingFixLegacyDesigner and _PostTrimmingPipeline were including all .dll files from ResolvedFileToPublish, which includes satellite resource assemblies (.resources.dll) from the shared NuGet package cache. These tasks open assemblies with ReadWrite access via DirectoryAssemblyResolver, causing IOException when parallel multi-RID builds contend for locks on the same cached files. Filter both targets to only process items where PostprocessAssembly metadata is 'true', which excludes satellite assemblies that ILLink does not process. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1d9bbd590b7..2cce752fd5e 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 @@ -235,7 +235,7 @@ BeforeTargets="ILLink" Condition=" '$(PublishTrimmed)' == 'true' and '$(AndroidUseDesignerAssembly)' == 'True' "> - <_PreTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + <_PreTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " /> Date: Fri, 10 Apr 2026 15:25:59 -0700 Subject: [PATCH 13/13] Open assemblies read-only in PreTrimmingFixLegacyDesigner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resolver was using ReadWrite=true, which acquires exclusive file locks on every assembly it loads — including the shared _Microsoft.Android.Resource.Designer.dll that is loaded implicitly via Resolve() during FixLegacyResourceDesignerStep.LoadDesigner(). The designer DLL is built once in the outer build at a shared obj/ path, but this task runs in each inner per-RID build. Parallel RID builds contend for exclusive locks on the shared file, causing IOException. Since the task only reads the designer assembly (never writes it), drop ReadWrite=true so all assemblies are opened in shared read mode. Library assemblies that get modified are written back using an explicit output path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/PreTrimmingFixLegacyDesigner.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs index 9c49fc6274b..5b0a896dda3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs @@ -33,8 +33,7 @@ public class PreTrimmingFixLegacyDesigner : AndroidTask public override bool RunTask () { using var resolver = new DirectoryAssemblyResolver ( - this.CreateTaskLogger (), loadDebugSymbols: true, - loadReaderParameters: new ReaderParameters { ReadWrite = true }); + this.CreateTaskLogger (), loadDebugSymbols: true); foreach (var assembly in Assemblies) { var dir = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec) ?? ""); @@ -60,7 +59,7 @@ public override bool RunTask () var assembly = resolver.GetAssembly (item.ItemSpec); if (fixLegacyStep.ProcessAssemblyDesigner (assembly)) { Log.LogDebugMessage ($" Writing modified assembly: {item.ItemSpec}"); - assembly.Write (new WriterParameters { + assembly.Write (item.ItemSpec, new WriterParameters { WriteSymbols = assembly.MainModule.HasSymbols, DeterministicMvid = Deterministic, });