From c33b39fa06b186277457014b65d6d8fc270a0f65 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 16 Mar 2026 11:10:08 -0700 Subject: [PATCH 1/7] [xabt] Move `AddKeepAlives` trimmer step to standalone MSBuild task Migrate AddKeepAlivesStep out of the ILLink custom step pipeline into a standalone MSBuild task that runs AfterTargets="ILLink", following the same pattern established by StripEmbeddedLibraries in #10894. Core IL-rewriting logic is extracted into AddKeepAlivesHelper, shared by both the new task (trimmed builds) and the existing pipeline step (non-trimmed builds via LinkAssembliesNoShrink). --- .../Microsoft.Android.Sdk.ILLink.csproj | 1 - .../MonoDroid.Tuner/AddKeepAlivesHelper.cs | 124 ++++++++++++++ .../MonoDroid.Tuner/AddKeepAlivesStep.cs | 158 +----------------- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 23 ++- .../Tasks/AddKeepAlives.cs | 91 ++++++++++ .../Xamarin.Android.Build.Tasks.csproj | 1 + 6 files changed, 239 insertions(+), 159 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs 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 3757ed34b68..92c92c0ee94 100644 --- a/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj +++ b/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs new file mode 100644 index 00000000000..5abe21e19ad --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Xamarin.Android.Tasks; + +namespace MonoDroid.Tuner +{ + static class AddKeepAlivesHelper + { + internal static bool AddKeepAlives (AssemblyDefinition assembly, IMetadataResolver resolver, Func getCorlibAssembly, Action logMessage) + { + if (!assembly.MainModule.HasTypeReference ("Java.Lang.Object")) + return false; + + // Anything that was built against .NET for Android will have + // keep-alives already compiled in. + if (MonoAndroidHelper.IsDotNetAndroidAssembly (assembly)) + return false; + + MethodDefinition? methodKeepAlive = null; + bool changed = false; + foreach (TypeDefinition type in assembly.MainModule.Types) + changed |= ProcessType (type, resolver, ref methodKeepAlive, getCorlibAssembly, logMessage); + + return changed; + } + + static bool ProcessType (TypeDefinition type, IMetadataResolver resolver, ref MethodDefinition? methodKeepAlive, Func getCorlibAssembly, Action logMessage) + { + bool changed = false; + if (MightNeedFix (type, resolver)) + changed |= AddKeepAlives (type, ref methodKeepAlive, getCorlibAssembly, logMessage); + + if (type.HasNestedTypes) { + foreach (var t in type.NestedTypes) { + changed |= ProcessType (t, resolver, ref methodKeepAlive, getCorlibAssembly, logMessage); + } + } + + return changed; + } + + static bool MightNeedFix (TypeDefinition type, IMetadataResolver resolver) + { + return !type.IsAbstract && type.IsSubclassOf ("Java.Lang.Object", resolver); + } + + static bool AddKeepAlives (TypeDefinition type, ref MethodDefinition? methodKeepAlive, Func getCorlibAssembly, Action logMessage) + { + bool changed = false; + foreach (MethodDefinition method in type.Methods) { + if (method.Parameters.Count == 0) + continue; + + if (!method.CustomAttributes.Any (a => a.AttributeType.FullName == "Android.Runtime.RegisterAttribute")) + continue; + + var instructions = method.Body.Instructions; + + var found = false; + for (int off = Math.Max (0, instructions.Count - 6); off < instructions.Count; off++) { + var current = instructions [off]; + if (current.OpCode == OpCodes.Call && current.Operand.ToString ().Contains ("System.GC::KeepAlive")) { + found = true; + break; + } + } + + if (found) + continue; + + var processor = method.Body.GetILProcessor (); + var module = method.DeclaringType.Module; + var end = instructions.Last (); + if (end.Previous.OpCode == OpCodes.Endfinally) + end = end.Previous; + + for (int i = 0; i < method.Parameters.Count; i++) { + if (method.Parameters [i].ParameterType.IsValueType || method.Parameters [i].ParameterType.FullName == "System.String") + continue; + + if (methodKeepAlive == null) + methodKeepAlive = GetKeepAliveMethod (getCorlibAssembly, logMessage); + + if (methodKeepAlive == null) { + logMessage ("Unable to add KeepAlive call, did not find System.GC.KeepAlive method."); + break; + } + + processor.InsertBefore (end, GetLoadArgumentInstruction (method.IsStatic ? i : i + 1, method.Parameters [i])); + processor.InsertBefore (end, Instruction.Create (OpCodes.Call, module.ImportReference (methodKeepAlive))); + changed = true; + } + } + return changed; + } + + static MethodDefinition? GetKeepAliveMethod (Func getCorlibAssembly, Action logMessage) + { + var corlibAssembly = getCorlibAssembly (); + if (corlibAssembly == null) + return null; + + var gcType = Extensions.GetType (corlibAssembly, "System.GC"); + if (gcType == null) + return null; + + return Extensions.GetMethod (gcType, "KeepAlive", new string [] { "System.Object" }); + } + + // Adapted from src/Mono.Android.Export/Mono.CodeGeneration/CodeArgumentReference.cs + static Instruction GetLoadArgumentInstruction (int argNum, ParameterDefinition parameter) + { + switch (argNum) { + case 0: return Instruction.Create (OpCodes.Ldarg_0); + case 1: return Instruction.Create (OpCodes.Ldarg_1); + case 2: return Instruction.Create (OpCodes.Ldarg_2); + case 3: return Instruction.Create (OpCodes.Ldarg_3); + default: return Instruction.Create (OpCodes.Ldarg, parameter); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs index 0ca2fa7f597..32f69e5e70f 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs @@ -1,169 +1,23 @@ -using System; -using System.Linq; -using Java.Interop.Tools.Cecil; using Mono.Cecil; -using Mono.Cecil.Cil; -using Mono.Linker; using Mono.Linker.Steps; using Xamarin.Android.Tasks; namespace MonoDroid.Tuner { - public class AddKeepAlivesStep : BaseStep -#if !ILLINK - , IAssemblyModifierPipelineStep -#endif // !ILLINK + public class AddKeepAlivesStep : BaseStep, IAssemblyModifierPipelineStep { - protected override void ProcessAssembly (AssemblyDefinition assembly) - { - var action = Annotations.HasAction (assembly) ? Annotations.GetAction (assembly) : AssemblyAction.Skip; - if (action == AssemblyAction.Delete) - return; - - if (AddKeepAlives (assembly)) { - if (action == AssemblyAction.Skip || action == AssemblyAction.Copy) - Annotations.SetAction (assembly, AssemblyAction.Save); - } - } - -#if !ILLINK public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) { // Only run this step on user Android assemblies if (!context.IsAndroidUserAssembly) return; - context.IsAssemblyModified |= AddKeepAlives (assembly); - } -#endif // !ILLINK - - internal bool AddKeepAlives (AssemblyDefinition assembly) - { - if (!assembly.MainModule.HasTypeReference ("Java.Lang.Object")) - return false; - - // Anything that was built against .NET for Android will have - // keep-alives already compiled in. - if (MonoAndroidHelper.IsDotNetAndroidAssembly (assembly)) - return false; - - bool changed = false; - foreach (TypeDefinition type in assembly.MainModule.Types) - changed |= ProcessType (type); - - return changed; - } - - bool ProcessType (TypeDefinition type) - { - bool changed = false; - if (MightNeedFix (type)) - changed |= AddKeepAlives (type); - - if (type.HasNestedTypes) { - foreach (var t in type.NestedTypes) { - changed |= ProcessType (t); - } - } - - return changed; - } - - bool MightNeedFix (TypeDefinition type) - { - return !type.IsAbstract && type.IsSubclassOf ("Java.Lang.Object", Context); - } - - MethodDefinition? methodKeepAlive = null; - - bool AddKeepAlives (TypeDefinition type) - { - bool changed = false; - foreach (MethodDefinition method in type.Methods) { - if (method.Parameters.Count == 0) - continue; - - if (!method.CustomAttributes.Any (a => a.AttributeType.FullName == "Android.Runtime.RegisterAttribute")) - continue; - - var instructions = method.Body.Instructions; - - var found = false; - for (int off = Math.Max (0, instructions.Count - 6); off < instructions.Count; off++) { - var current = instructions [off]; - if (current.OpCode == OpCodes.Call && current.Operand.ToString ().Contains ("System.GC::KeepAlive")) { - found = true; - break; - } - } - - if (found) - continue; - - var processor = method.Body.GetILProcessor (); - var module = method.DeclaringType.Module; - var end = instructions.Last (); - if (end.Previous.OpCode == OpCodes.Endfinally) - end = end.Previous; - - for (int i = 0; i < method.Parameters.Count; i++) { - if (method.Parameters [i].ParameterType.IsValueType || method.Parameters [i].ParameterType.FullName == "System.String") - continue; - - if (methodKeepAlive == null) - methodKeepAlive = GetKeepAliveMethod (); - - if (methodKeepAlive == null) { - LogMessage ("Unable to add KeepAlive call, did not find System.GC.KeepAlive method."); - break; - } - - processor.InsertBefore (end, GetLoadArgumentInstruction (method.IsStatic ? i : i + 1, method.Parameters [i])); - processor.InsertBefore (end, Instruction.Create (OpCodes.Call, module.ImportReference (methodKeepAlive))); - changed = true; - } - } - return changed; - } - - protected virtual AssemblyDefinition GetCorlibAssembly () - { - return Context.GetAssembly ("System.Private.CoreLib"); - } - - MethodDefinition? GetKeepAliveMethod () - { - var corlibAssembly = GetCorlibAssembly (); - if (corlibAssembly == null) - return null; - - var gcType = Extensions.GetType (corlibAssembly, "System.GC"); - if (gcType == null) - return null; - - return Extensions.GetMethod (gcType, "KeepAlive", new string [] { "System.Object" }); - } - - public -#if !ILLINK - override -#endif - void LogMessage (string message) - { - Context.LogMessage (message); - } - - // Adapted from src/Mono.Android.Export/Mono.CodeGeneration/CodeArgumentReference.cs - static Instruction GetLoadArgumentInstruction (int argNum, ParameterDefinition parameter) - { - switch (argNum) { - case 0: return Instruction.Create (OpCodes.Ldarg_0); - case 1: return Instruction.Create (OpCodes.Ldarg_1); - case 2: return Instruction.Create (OpCodes.Ldarg_2); - case 3: return Instruction.Create (OpCodes.Ldarg_3); - default: return Instruction.Create (OpCodes.Ldarg, parameter); - } + context.IsAssemblyModified |= AddKeepAlivesHelper.AddKeepAlives ( + assembly, + Context, + () => Context.GetAssembly ("System.Private.CoreLib"), + (msg) => LogMessage (msg)); } } } 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 4be55630c91..8a67744e311 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 @@ -197,12 +198,6 @@ <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveJavaInterfaces" /> <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.FixAbstractMethodsStep" /> - <_TrimmerCustomSteps - Condition=" '$(AndroidAddKeepAlives)' == 'true' " - Include="$(_AndroidLinkerCustomStepAssembly)" - AfterStep="CleanStep" - Type="MonoDroid.Tuner.AddKeepAlivesStep" - /> <_TrimmerCustomSteps Condition=" '$(AndroidLinkResources)' == 'true' " @@ -266,6 +261,22 @@ Deterministic="$(Deterministic)" /> + + + + <_AddKeepAlivesAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs new file mode 100644 index 00000000000..3674cff0892 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs @@ -0,0 +1,91 @@ +#nullable enable + +using System; +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 injects GC.KeepAlive() calls into binding methods of trimmed assemblies. +/// +/// This 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 AddKeepAlives : AndroidTask +{ + public override string TaskPrefix => "AKA"; + + [Required] + public ITaskItem [] Assemblies { get; set; } = []; + + public bool Deterministic { get; set; } + + public override bool RunTask () + { + var resolver = new DefaultAssemblyResolver (); + var cache = new TypeDefinitionCache (); + var searchDirectories = new HashSet (StringComparer.OrdinalIgnoreCase); + + foreach (var assembly in Assemblies) { + var dir = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec) ?? ""); + if (searchDirectories.Add (dir)) { + resolver.AddSearchDirectory (dir); + } + } + + try { + foreach (var assembly in Assemblies) { + if (MonoAndroidHelper.IsFrameworkAssembly (assembly)) { + continue; + } + + ProcessAssembly (assembly.ItemSpec, resolver, cache); + } + } finally { + resolver.Dispose (); + } + + return !Log.HasLoggedErrors; + } + + void ProcessAssembly (string assemblyPath, IAssemblyResolver resolver, IMetadataResolver cache) + { + string pdbPath = Path.ChangeExtension (assemblyPath, ".pdb"); + bool havePdb = File.Exists (pdbPath); + + var readerParams = new ReaderParameters { + ReadSymbols = havePdb, + ReadWrite = true, + AssemblyResolver = resolver, + }; + + using (var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams)) { + bool modified = AddKeepAlivesHelper.AddKeepAlives ( + assembly, + cache, + () => GetCorlibAssembly (resolver), + (msg) => Log.LogDebugMessage (msg)); + + if (!modified) { + return; + } + + Log.LogDebugMessage ($" Writing modified assembly: {assemblyPath}"); + assembly.Write (new WriterParameters { + WriteSymbols = havePdb, + DeterministicMvid = Deterministic, + }); + } + } + + static AssemblyDefinition GetCorlibAssembly (IAssemblyResolver resolver) + { + return resolver.Resolve (AssemblyNameReference.Parse ("System.Private.CoreLib")); + } +} 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 79f6e450ae3..e1cedf463b9 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -49,6 +49,7 @@ + From e24ddc37287895f59c8de8d2682106aed4278616 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 16 Mar 2026 13:23:24 -0700 Subject: [PATCH 2/7] Fix missing using directive in AddKeepAlivesHelper.cs --- .../Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs index 5abe21e19ad..c051fe30971 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Java.Interop.Tools.Cecil; using Mono.Cecil; using Mono.Cecil.Cil; using Xamarin.Android.Tasks; From 21ca47518f7c9a11c5b09c637dd14250114880d5 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Wed, 18 Mar 2026 16:19:55 -0700 Subject: [PATCH 3/7] Fix Windows file locking by using DirectoryAssemblyResolver Replace DefaultAssemblyResolver with DirectoryAssemblyResolver (ReadWrite=true) to avoid file handle conflicts on Windows. The directory resolver caches all assemblies (both explicit and dependency-resolved), preventing duplicate file opens that caused IOException when the same assembly was opened as both a dependency and a primary target. --- .../Tasks/AddKeepAlives.cs | 52 +++++-------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs index 3674cff0892..bfed1f4012f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs @@ -1,7 +1,5 @@ #nullable enable -using System; -using System.Collections.Generic; using System.IO; using Java.Interop.Tools.Cecil; using Microsoft.Android.Build.Tasks; @@ -28,64 +26,42 @@ public class AddKeepAlives : AndroidTask public override bool RunTask () { - var resolver = new DefaultAssemblyResolver (); + using var resolver = new DirectoryAssemblyResolver ( + this.CreateTaskLogger (), loadDebugSymbols: true, + loadReaderParameters: new ReaderParameters { ReadWrite = true }); var cache = new TypeDefinitionCache (); - var searchDirectories = new HashSet (StringComparer.OrdinalIgnoreCase); foreach (var assembly in Assemblies) { var dir = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec) ?? ""); - if (searchDirectories.Add (dir)) { - resolver.AddSearchDirectory (dir); + if (!resolver.SearchDirectories.Contains (dir)) { + resolver.SearchDirectories.Add (dir); } } - try { - foreach (var assembly in Assemblies) { - if (MonoAndroidHelper.IsFrameworkAssembly (assembly)) { - continue; - } - - ProcessAssembly (assembly.ItemSpec, resolver, cache); + foreach (var item in Assemblies) { + if (MonoAndroidHelper.IsFrameworkAssembly (item)) { + continue; } - } finally { - resolver.Dispose (); - } - - return !Log.HasLoggedErrors; - } - void ProcessAssembly (string assemblyPath, IAssemblyResolver resolver, IMetadataResolver cache) - { - string pdbPath = Path.ChangeExtension (assemblyPath, ".pdb"); - bool havePdb = File.Exists (pdbPath); - - var readerParams = new ReaderParameters { - ReadSymbols = havePdb, - ReadWrite = true, - AssemblyResolver = resolver, - }; + var assembly = resolver.GetAssembly (item.ItemSpec); - using (var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams)) { bool modified = AddKeepAlivesHelper.AddKeepAlives ( assembly, cache, - () => GetCorlibAssembly (resolver), + () => resolver.Resolve (AssemblyNameReference.Parse ("System.Private.CoreLib")), (msg) => Log.LogDebugMessage (msg)); if (!modified) { - return; + continue; } - Log.LogDebugMessage ($" Writing modified assembly: {assemblyPath}"); + Log.LogDebugMessage ($" Writing modified assembly: {item.ItemSpec}"); assembly.Write (new WriterParameters { - WriteSymbols = havePdb, + WriteSymbols = assembly.MainModule.HasSymbols, DeterministicMvid = Deterministic, }); } - } - static AssemblyDefinition GetCorlibAssembly (IAssemblyResolver resolver) - { - return resolver.Resolve (AssemblyNameReference.Parse ("System.Private.CoreLib")); + return !Log.HasLoggedErrors; } } From d74ce3abb56dd67793b7fe7cdeed1e162befd4ab Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Wed, 18 Mar 2026 21:28:52 -0700 Subject: [PATCH 4/7] [xabt] Combine post-trimming steps into single PostTrimmingPipeline task Replace standalone AddKeepAlives and StripEmbeddedLibraries MSBuild tasks with a single PostTrimmingPipeline task that opens assemblies once (via DirectoryAssemblyResolver with ReadWrite) and runs both modifications in a single pass. Extract StripEmbeddedLibrariesStep as an IAssemblyModifierPipelineStep for reuse. --- .../StripEmbeddedLibrariesStep.cs | 56 +++++++++ ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 33 ++--- .../Tasks/AddKeepAlives.cs | 67 ---------- .../Tasks/PostTrimmingPipeline.cs | 84 +++++++++++++ .../Tasks/StripEmbeddedLibraries.cs | 116 ------------------ .../Xamarin.Android.Build.Tasks.csproj | 1 + 6 files changed, 150 insertions(+), 207 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/StripEmbeddedLibraries.cs diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs new file mode 100644 index 00000000000..bdd031f9420 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs @@ -0,0 +1,56 @@ +#nullable enable + +using System; +using System.Linq; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Xamarin.Android.Tasks; + +namespace MonoDroid.Tuner; + +class StripEmbeddedLibrariesStep : IAssemblyModifierPipelineStep +{ + readonly TaskLoggingHelper log; + + public StripEmbeddedLibrariesStep (TaskLoggingHelper log) + { + this.log = log; + } + + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) + { + foreach (var module in assembly.Modules) { + foreach (var resource in module.Resources.ToArray ()) { + if (ShouldStripResource (resource)) { + log.LogDebugMessage ($" Stripped {resource.Name} from {assembly.Name.Name}.dll"); + module.Resources.Remove (resource); + context.IsAssemblyModified = true; + } + } + } + } + + /// + /// Determines whether a resource should be stripped from the assembly. + /// Matches the same criteria as the old ILLink StripEmbeddedLibraries step. + /// + internal static bool ShouldStripResource (Resource resource) + { + if (!(resource is EmbeddedResource)) + return false; + // Embedded jars + if (resource.Name.EndsWith (".jar", StringComparison.InvariantCultureIgnoreCase)) + return true; + // Embedded AndroidNativeLibrary archive + if (resource.Name == "__AndroidNativeLibraries__.zip") + return true; + // Embedded AndroidResourceLibrary archive + if (resource.Name == "__AndroidLibraryProjects__.zip") + return true; + // Embedded AndroidEnvironment items + if (resource.Name.StartsWith ("__AndroidEnvironment__", StringComparison.Ordinal)) + return true; + return false; + } +} 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 8a67744e311..84e68d8bf6f 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,8 +7,7 @@ - - + <_RemoveRegisterFlag>$(MonoAndroidIntermediateAssemblyDir)shrunk\shrunk.flag @@ -246,34 +245,20 @@ - - - <_StripEmbeddedLibrariesAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> - - - - - - + Condition=" '$(PublishTrimmed)' == 'true' "> - <_AddKeepAlivesAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + <_PostTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> - diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs deleted file mode 100644 index bfed1f4012f..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs +++ /dev/null @@ -1,67 +0,0 @@ -#nullable enable - -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 injects GC.KeepAlive() calls into binding methods of trimmed assemblies. -/// -/// This 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 AddKeepAlives : AndroidTask -{ - public override string TaskPrefix => "AKA"; - - [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 }); - 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); - } - } - - foreach (var item in Assemblies) { - if (MonoAndroidHelper.IsFrameworkAssembly (item)) { - continue; - } - - var assembly = resolver.GetAssembly (item.ItemSpec); - - bool modified = AddKeepAlivesHelper.AddKeepAlives ( - assembly, - cache, - () => resolver.Resolve (AssemblyNameReference.Parse ("System.Private.CoreLib")), - (msg) => Log.LogDebugMessage (msg)); - - if (!modified) { - continue; - } - - 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/Tasks/PostTrimmingPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs new file mode 100644 index 00000000000..ec1ea789a01 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs @@ -0,0 +1,84 @@ +#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 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 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 steps = new List (); + + steps.Add (new StripEmbeddedLibrariesStep (Log)); + + if (AddKeepAlives) { + var linkContext = new MSBuildLinkContext (resolver, Log); + var addKeepAlivesStep = new AddKeepAlivesStep (); + addKeepAlivesStep.Initialize (linkContext); + steps.Add (addKeepAlivesStep); + } + + foreach (var item in Assemblies) { + if (MonoAndroidHelper.IsFrameworkAssembly (item)) { + continue; + } + + var assembly = resolver.GetAssembly (item.ItemSpec); + var context = new StepContext (item, item) { + IsAndroidAssembly = MonoAndroidHelper.IsAndroidAssembly (item), + IsUserAssembly = true, + }; + + 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/Tasks/StripEmbeddedLibraries.cs b/src/Xamarin.Android.Build.Tasks/Tasks/StripEmbeddedLibraries.cs deleted file mode 100644 index 7a65c4a8145..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/StripEmbeddedLibraries.cs +++ /dev/null @@ -1,116 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Mono.Cecil; - -namespace Xamarin.Android.Tasks; - -/// -/// An MSBuild task that strips embedded Android resources (.jar, __AndroidNativeLibraries__.zip, -/// __AndroidLibraryProjects__.zip, __AndroidEnvironment__) from trimmed assemblies. -/// -/// This runs in the inner build after ILLink but before ReadyToRun/crossgen2 compilation, -/// so that R2R images are generated from the already-stripped assemblies. -/// -public class StripEmbeddedLibraries : AndroidTask -{ - public override string TaskPrefix => "SEL"; - - [Required] - public ITaskItem [] Assemblies { get; set; } = []; - - public bool Deterministic { get; set; } - - public override bool RunTask () - { - var resolver = new DefaultAssemblyResolver (); - var searchDirectories = new HashSet (StringComparer.OrdinalIgnoreCase); - - foreach (var assembly in Assemblies) { - var dir = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec) ?? ""); - if (searchDirectories.Add (dir)) { - resolver.AddSearchDirectory (dir); - } - } - - try { - foreach (var assembly in Assemblies) { - if (MonoAndroidHelper.IsFrameworkAssembly (assembly)) { - continue; - } - - StripAssembly (assembly.ItemSpec, resolver); - } - } finally { - resolver.Dispose (); - } - - return !Log.HasLoggedErrors; - } - - void StripAssembly (string assemblyPath, IAssemblyResolver resolver) - { - string pdbPath = Path.ChangeExtension (assemblyPath, ".pdb"); - bool havePdb = File.Exists (pdbPath); - - var readerParams = new ReaderParameters { - ReadSymbols = havePdb, - ReadWrite = true, - AssemblyResolver = resolver, - }; - - bool assembly_modified = false; - - using (var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams)) { - foreach (var module in assembly.Modules) { - foreach (var resource in module.Resources.ToArray ()) { - if (ShouldStripResource (resource)) { - Log.LogDebugMessage ($" Stripped {resource.Name} from {assembly.Name.Name}.dll"); - module.Resources.Remove (resource); - assembly_modified = true; - } - } - } - - if (!assembly_modified) { - return; - } - - Log.LogDebugMessage ($" Writing stripped assembly: {assemblyPath}"); - assembly.Write (new WriterParameters { - WriteSymbols = havePdb, - DeterministicMvid = Deterministic, - }); - } - } - - /// - /// Determines whether a resource should be stripped from the assembly. - /// Matches the same criteria as the old ILLink StripEmbeddedLibraries step. - /// - internal static bool ShouldStripResource (Resource resource) - { - if (!(resource is EmbeddedResource)) - return false; - // Embedded jars - if (resource.Name.EndsWith (".jar", StringComparison.InvariantCultureIgnoreCase)) - return true; - // Embedded AndroidNativeLibrary archive - if (resource.Name == "__AndroidNativeLibraries__.zip") - return true; - // Embedded AndroidResourceLibrary archive - if (resource.Name == "__AndroidLibraryProjects__.zip") - return true; - // Embedded AndroidEnvironment items - if (resource.Name.StartsWith ("__AndroidEnvironment__", StringComparison.Ordinal)) - return true; - return false; - } - -} 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 e1cedf463b9..d309996f45a 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -50,6 +50,7 @@ + From 59e7c899f4013cf17363bbc96dc33ebaa7dd591d Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 19 Mar 2026 10:56:05 -0700 Subject: [PATCH 5/7] [xabt] Refactor PostTrimmingPipeline to use IAssemblyModifierPipelineStep pattern Use List with StripEmbeddedLibrariesStep and PostTrimmingAddKeepAlivesStep instead of calling helpers directly. Move the IsFrameworkAssembly check into StripEmbeddedLibrariesStep.ProcessAssembly and remove all outer-loop filtering so each step handles its own guards internally. --- .../PostTrimmingAddKeepAlivesStep.cs | 30 +++++++++++++++++++ .../StripEmbeddedLibrariesStep.cs | 11 ++++++- .../Tasks/PostTrimmingPipeline.cs | 21 ++++--------- .../Xamarin.Android.Build.Tasks.csproj | 1 + 4 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingAddKeepAlivesStep.cs diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingAddKeepAlivesStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingAddKeepAlivesStep.cs new file mode 100644 index 00000000000..0a7392789cf --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingAddKeepAlivesStep.cs @@ -0,0 +1,30 @@ +using System; +using Java.Interop.Tools.Cecil; +using Mono.Cecil; +using Xamarin.Android.Tasks; + +namespace MonoDroid.Tuner; + +/// +/// Post-trimming version of AddKeepAlives that calls AddKeepAlivesHelper directly, +/// matching the original ILLink behavior (no IsAndroidUserAssembly pre-filter). +/// The helper has its own assembly-level guards (HasTypeReference, IsDotNetAndroidAssembly). +/// +class PostTrimmingAddKeepAlivesStep : IAssemblyModifierPipelineStep +{ + readonly IMetadataResolver cache; + readonly Func getCorlibAssembly; + readonly Action logMessage; + + public PostTrimmingAddKeepAlivesStep (IMetadataResolver cache, Func getCorlibAssembly, Action logMessage) + { + this.cache = cache; + this.getCorlibAssembly = getCorlibAssembly; + this.logMessage = logMessage; + } + + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) + { + context.IsAssemblyModified |= AddKeepAlivesHelper.AddKeepAlives (assembly, cache, getCorlibAssembly, logMessage); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs index bdd031f9420..78f202b23c1 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/StripEmbeddedLibrariesStep.cs @@ -20,15 +20,24 @@ public StripEmbeddedLibrariesStep (TaskLoggingHelper log) public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) { + if (MonoAndroidHelper.IsFrameworkAssembly (assembly)) + return; + context.IsAssemblyModified |= StripEmbeddedLibraries (assembly, log); + } + + internal static bool StripEmbeddedLibraries (AssemblyDefinition assembly, TaskLoggingHelper log) + { + bool modified = false; foreach (var module in assembly.Modules) { foreach (var resource in module.Resources.ToArray ()) { if (ShouldStripResource (resource)) { log.LogDebugMessage ($" Stripped {resource.Name} from {assembly.Name.Name}.dll"); module.Resources.Remove (resource); - context.IsAssemblyModified = true; + modified = true; } } } + return modified; } /// diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs index ec1ea789a01..4084e48cd77 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs @@ -36,6 +36,7 @@ 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) ?? ""); @@ -45,31 +46,19 @@ public override bool RunTask () } var steps = new List (); - steps.Add (new StripEmbeddedLibrariesStep (Log)); - if (AddKeepAlives) { - var linkContext = new MSBuildLinkContext (resolver, Log); - var addKeepAlivesStep = new AddKeepAlivesStep (); - addKeepAlivesStep.Initialize (linkContext); - steps.Add (addKeepAlivesStep); + steps.Add (new PostTrimmingAddKeepAlivesStep (cache, + () => resolver.Resolve (AssemblyNameReference.Parse ("System.Private.CoreLib")), + (msg) => Log.LogDebugMessage (msg))); } foreach (var item in Assemblies) { - if (MonoAndroidHelper.IsFrameworkAssembly (item)) { - continue; - } - var assembly = resolver.GetAssembly (item.ItemSpec); - var context = new StepContext (item, item) { - IsAndroidAssembly = MonoAndroidHelper.IsAndroidAssembly (item), - IsUserAssembly = true, - }; - + 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 { 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 d309996f45a..c03261cb316 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -51,6 +51,7 @@ + From cda293a2b729ba1782934a87664fc5653b058c24 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 19 Mar 2026 11:32:11 -0700 Subject: [PATCH 6/7] [xabt] Move `RemoveResourceDesigner` trimmer step to PostTrimmingPipeline Migrate RemoveResourceDesignerStep from an ILLink custom step to a post-trimming IAssemblyModifierPipelineStep, following the same pattern used for StripEmbeddedLibrariesStep and PostTrimmingAddKeepAlivesStep. - Add PostTrimmingRemoveResourceDesignerStep with self-contained logic from LinkDesignerBase/RemoveResourceDesignerStep, using Action for logging instead of ILLink's Context.LogMessage - Add AndroidLinkResources property to PostTrimmingPipeline task and pre-load all assemblies when enabled (the step needs a two-phase scan) - Remove RemoveResourceDesignerStep and GetAssembliesStep from ILLink custom steps in targets - Remove AndroidLinkConfiguration.cs and RemoveResourceDesignerStep.cs from ILLink csproj (no longer needed there) - Delete GetAssembliesStep.cs (dead code, only served RemoveResourceDesignerStep) --- .../GetAssembliesStep.cs | 28 -- .../Microsoft.Android.Sdk.ILLink.csproj | 2 - .../PostTrimmingRemoveResourceDesignerStep.cs | 324 ++++++++++++++++++ ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 13 +- .../Tasks/PostTrimmingPipeline.cs | 9 + .../Xamarin.Android.Build.Tasks.csproj | 1 + 6 files changed, 335 insertions(+), 42 deletions(-) delete mode 100644 src/Microsoft.Android.Sdk.ILLink/GetAssembliesStep.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingRemoveResourceDesignerStep.cs diff --git a/src/Microsoft.Android.Sdk.ILLink/GetAssembliesStep.cs b/src/Microsoft.Android.Sdk.ILLink/GetAssembliesStep.cs deleted file mode 100644 index 7721dc05151..00000000000 --- a/src/Microsoft.Android.Sdk.ILLink/GetAssembliesStep.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Mono.Cecil; -using Mono.Linker; -using Mono.Linker.Steps; -using System; -using System.Linq; -using Xamarin.Android.Tasks; -using System.Collections.Generic; -using Mono.Cecil.Cil; - -namespace MonoDroid.Tuner -{ - public class GetAssembliesStep : BaseStep - { - AndroidLinkConfiguration config = null; - - protected override void Process () - { - config = AndroidLinkConfiguration.GetInstance (Context); - } - - protected override void ProcessAssembly (AssemblyDefinition assembly) - { - if (config == null) - return; - config.Assemblies.Add (assembly); - } - } -} 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 92c92c0ee94..b79d1ff2487 100644 --- a/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj +++ b/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj @@ -14,12 +14,10 @@ - - diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingRemoveResourceDesignerStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingRemoveResourceDesignerStep.cs new file mode 100644 index 00000000000..4eb6f2a018d --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingRemoveResourceDesignerStep.cs @@ -0,0 +1,324 @@ +#nullable disable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Collections.Generic; +using Xamarin.Android.Tasks; + +namespace MonoDroid.Tuner; + +/// +/// Post-trimming version of RemoveResourceDesignerStep that inlines resource designer +/// constants and removes designer classes, matching the original ILLink behavior. +/// The core logic is identical to the ILLink RemoveResourceDesignerStep / LinkDesignerBase. +/// +class PostTrimmingRemoveResourceDesignerStep : IAssemblyModifierPipelineStep +{ + readonly IList allAssemblies; + readonly Action logMessage; + readonly Regex opCodeRegex = new Regex (@"([\w]+): ([\w]+) ([\w.]+) ([\w:./]+)"); + + TypeDefinition mainDesigner = null; + AssemblyDefinition mainAssembly = null; + CustomAttribute mainDesignerAttribute; + Dictionary designerConstants; + bool designerLoaded; + + public PostTrimmingRemoveResourceDesignerStep (IList allAssemblies, Action logMessage) + { + this.allAssemblies = allAssemblies; + this.logMessage = logMessage; + } + + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) + { + LoadDesigner (); + context.IsAssemblyModified |= ProcessAssemblyDesigner (assembly); + } + + void LoadDesigner () + { + if (designerLoaded) + return; + designerLoaded = true; + + foreach (var asm in allAssemblies) { + if (FindResourceDesigner (asm, mainApplication: true, designer: out mainDesigner, designerAttribute: out mainDesignerAttribute)) { + mainAssembly = asm; + break; + } + } + if (mainDesigner == null) { + logMessage (" Main Designer not found."); + return; + } + logMessage ($" Main Designer found {mainDesigner.FullName}."); + designerConstants = BuildResourceDesignerFieldLookup (mainDesigner); + } + + bool ProcessAssemblyDesigner (AssemblyDefinition assembly) + { + if (mainDesigner == null) + return false; + if (MonoAndroidHelper.IsFrameworkAssembly (assembly)) + return false; + + logMessage ($" Fixing up {assembly.Name.Name}"); + TypeDefinition localDesigner = null; + CustomAttribute designerAttribute; + if (assembly != mainAssembly) { + logMessage ($" {assembly.Name.Name} is not the main assembly. "); + if (!FindResourceDesigner (assembly, mainApplication: false, designer: out localDesigner, designerAttribute: out designerAttribute)) { + logMessage ($" {assembly.Name.Name} does not have a designer file."); + return false; + } + } else { + logMessage ($" {assembly.Name.Name} is the main assembly. "); + localDesigner = mainDesigner; + designerAttribute = mainDesignerAttribute; + } + + logMessage ($" {assembly.Name.Name} has designer {localDesigner.FullName}."); + + FixupAssemblyTypes (assembly, localDesigner); + + ClearDesignerClass (localDesigner); + if (designerAttribute != null) { + assembly.CustomAttributes.Remove (designerAttribute); + } + return true; + } + + // ---- Methods below are from LinkDesignerBase / RemoveResourceDesignerStep, adapted for non-ILLink use ---- + + bool FindResourceDesigner (AssemblyDefinition assembly, bool mainApplication, out TypeDefinition designer, out CustomAttribute designerAttribute) + { + string designerFullName = null; + designer = null; + designerAttribute = null; + foreach (CustomAttribute attribute in assembly.CustomAttributes) + { + if (attribute.AttributeType.FullName == "Android.Runtime.ResourceDesignerAttribute") + { + designerAttribute = attribute; + if (attribute.HasProperties) + { + foreach (var p in attribute.Properties) + { + if (p.Name == "IsApplication" && (bool)p.Argument.Value == (mainApplication ? mainApplication : (bool)p.Argument.Value)) + { + designerFullName = attribute.ConstructorArguments[0].Value.ToString (); + break; + } + } + } + break; + + } + } + + if (string.IsNullOrEmpty(designerFullName)) { + logMessage ($"Inspecting member references for assembly: {assembly.FullName};"); + var memberRefs = assembly.MainModule.GetMemberReferences (); + foreach (var memberRef in memberRefs) { + string declaringType = memberRef.DeclaringType?.ToString () ?? string.Empty; + if (!declaringType.Contains (".Resource/")) { + continue; + } + if (declaringType.Contains ("_Microsoft.Android.Resource.Designer")) { + continue; + } + var resolved = false; + try { + var def = memberRef.Resolve (); + if (resolved = def != null) { + logMessage ($"Resolved member `{memberRef?.Name}`"); + } + } catch (Exception ex) { + logMessage ($"Exception resolving member `{memberRef?.Name}`: {ex}"); + resolved = false; + } + if (!resolved) { + logMessage ($"Adding _Linker.Generated.Resource to {assembly.Name.Name}. Could not resolve {memberRef?.Name} : {declaringType}"); + designer = new TypeDefinition ("_Linker.Generated", "Resource", TypeAttributes.Public | TypeAttributes.AnsiClass); + designer.BaseType = new TypeDefinition ("System", "Object", TypeAttributes.Public | TypeAttributes.AnsiClass); + return true; + } + } + } + + if (string.IsNullOrEmpty(designerFullName)) + return false; + + foreach (ModuleDefinition module in assembly.Modules) + { + foreach (TypeDefinition type in module.Types) + { + if (type.FullName == designerFullName) + { + designer = type; + return true; + } + } + } + return false; + } + + void FixBody (MethodBody body, TypeDefinition designer) + { + Dictionary instructions = new Dictionary(); + var processor = body.GetILProcessor (); + string designerFullName = $"{designer.FullName}/"; + bool isDesignerMethod = designerFullName.Contains (body.Method.DeclaringType.FullName); + string declaringTypeName = body.Method.DeclaringType.Name; + foreach (var i in body.Instructions) + { + string line = i.ToString (); + if ((line.Contains (designerFullName) || (isDesignerMethod && i.OpCode == OpCodes.Stsfld)) && !instructions.ContainsKey (i)) + { + var match = opCodeRegex.Match (line); + if (match.Success && match.Groups.Count == 5) { + string key = match.Groups[4].Value.Replace (designerFullName, string.Empty); + if (isDesignerMethod) { + key = declaringTypeName +"::" + key; + } + if (designerConstants.ContainsKey (key) && !instructions.ContainsKey (i)) + instructions.Add(i, designerConstants [key]); + } + } + } + if (instructions.Count > 0) + logMessage ($" Fixing up {body.Method.FullName}"); + foreach (var i in instructions) + { + var newCode = Extensions.CreateLoadArraySizeOrOffsetInstruction (i.Value); + logMessage ($" Replacing {i.Key}"); + logMessage ($" With {newCode}"); + processor.Replace(i.Key, newCode); + } + } + + Dictionary BuildResourceDesignerFieldLookup (TypeDefinition type) + { + var output = new Dictionary (); + foreach (TypeDefinition definition in type.NestedTypes) + { + foreach (FieldDefinition field in definition.Fields) + { + string key = $"{definition.Name}::{field.Name}"; + if (!output.ContainsKey (key)) + output.Add(key, int.Parse (field.Constant?.ToString () ?? "0", CultureInfo.InvariantCulture)); + } + } + return output; + } + + void ClearDesignerClass (TypeDefinition designer, bool completely = false) + { + logMessage ($" TryRemoving {designer.FullName}"); + // for each of the nested types clear all but the + // int[] fields. + if (!completely) { + for (int i = designer.NestedTypes.Count -1; i >= 0; i--) { + var nestedType = designer.NestedTypes [i]; + RemoveFieldsFromType (nestedType, designer.Module); + if (nestedType.Fields.Count == 0) { + // no fields we do not need this class at all. + designer.NestedTypes.RemoveAt (i); + } + } + RemoveUpdateIdValues (designer); + } else { + designer.NestedTypes.Clear (); + } + designer.Fields.Clear (); + designer.Properties.Clear (); + designer.CustomAttributes.Clear (); + designer.Interfaces.Clear (); + designer.Events.Clear (); + } + + void FixType (TypeDefinition type, TypeDefinition localDesigner) + { + foreach (MethodDefinition method in type.Methods) + { + if (!method.HasBody) + continue; + FixBody (method.Body, localDesigner); + } + foreach (PropertyDefinition property in type.Properties) + { + if (property.GetMethod != null && property.GetMethod.HasBody) + { + FixBody (property.GetMethod.Body, localDesigner); + } + if (property.SetMethod != null && property.SetMethod.HasBody) + { + FixBody (property.SetMethod.Body, localDesigner); + } + } + foreach (TypeDefinition nestedType in type.NestedTypes) + { + FixType (nestedType, localDesigner); + } + } + + void FixupAssemblyTypes (AssemblyDefinition assembly, TypeDefinition designer) + { + foreach (ModuleDefinition module in assembly.Modules) + { + foreach (TypeDefinition type in module.Types) + { + if (type.FullName == designer.FullName) + continue; + FixType (type, designer); + } + } + } + + void RemoveFieldsFromType (TypeDefinition type, ModuleDefinition module) + { + for (int i = type.Fields.Count - 1; i >= 0; i--) { + var field = type.Fields [i]; + if (field.FieldType.IsArray) { + continue; + } + logMessage ($"Removing {type.Name}::{field.Name}"); + type.Fields.RemoveAt (i); + } + } + + void RemoveUpdateIdValues (TypeDefinition type) + { + foreach (var method in type.Methods) { + if (method.Name.Contains ("UpdateIdValues")) { + FixUpdateIdValuesBody (method); + } else { + FixBody (method.Body, type); + } + } + + foreach (var nestedType in type.NestedTypes) { + RemoveUpdateIdValues (nestedType); + } + } + + void FixUpdateIdValuesBody (MethodDefinition method) + { + List finalInstructions = new List (); + Collection instructions = method.Body.Instructions; + for (int i = 0; i < method.Body.Instructions.Count-1; i++) { + Instruction instruction = instructions[i]; + string line = instruction.ToString (); + bool found = line.Contains ("Int32[]") || instruction.OpCode == OpCodes.Ret; + if (!found) { + method.Body.Instructions.Remove (instruction); + i--; + } + } + } +} 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..9b8870a8295 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,18 +198,6 @@ <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.FixAbstractMethodsStep" /> - <_TrimmerCustomSteps - Condition=" '$(AndroidLinkResources)' == 'true' " - Include="$(_AndroidLinkerCustomStepAssembly)" - AfterStep="CleanStep" - Type="MonoDroid.Tuner.RemoveResourceDesignerStep" - /> - <_TrimmerCustomSteps - Condition=" '$(AndroidLinkResources)' == 'true' " - Include="$(_AndroidLinkerCustomStepAssembly)" - AfterStep="CleanStep" - Type="MonoDroid.Tuner.GetAssembliesStep" - /> <_TrimmerCustomSteps Condition=" '$(AndroidUseDesignerAssembly)' == 'true' " Include="$(_AndroidLinkerCustomStepAssembly)" @@ -259,6 +247,7 @@ diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs index 4084e48cd77..3103146d503 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs @@ -29,6 +29,8 @@ public class PostTrimmingPipeline : AndroidTask public bool AddKeepAlives { get; set; } + public bool AndroidLinkResources { get; set; } + public bool Deterministic { get; set; } public override bool RunTask () @@ -52,6 +54,13 @@ public override bool RunTask () () => resolver.Resolve (AssemblyNameReference.Parse ("System.Private.CoreLib")), (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 PostTrimmingRemoveResourceDesignerStep (allAssemblies, (msg) => Log.LogDebugMessage (msg))); + } foreach (var item in Assemblies) { var assembly = resolver.GetAssembly (item.ItemSpec); 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 c03261cb316..ba91e4c25dc 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -52,6 +52,7 @@ + From f9752e5d2669e82a9ca1f1b400e8ab8fd601aeb8 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 19 Mar 2026 12:58:30 -0700 Subject: [PATCH 7/7] [xabt] Rename PostTrimmingRemoveResourceDesignerStep to RemoveResourceDesignerStep The old ILLink RemoveResourceDesignerStep is no longer compiled into Xamarin.Android.Build.Tasks, so the PostTrimming prefix is unnecessary. Also remove AndroidLinkConfiguration.cs from compilation as it was only used by the old ILLink step. --- .../PostTrimmingRemoveResourceDesignerStep.cs | 324 --------------- .../RemoveResourceDesignerStep.cs | 370 ++++++++++++++---- .../Tasks/PostTrimmingPipeline.cs | 2 +- .../Xamarin.Android.Build.Tasks.csproj | 4 +- 4 files changed, 287 insertions(+), 413 deletions(-) delete mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingRemoveResourceDesignerStep.cs diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingRemoveResourceDesignerStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingRemoveResourceDesignerStep.cs deleted file mode 100644 index 4eb6f2a018d..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingRemoveResourceDesignerStep.cs +++ /dev/null @@ -1,324 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text.RegularExpressions; -using Mono.Cecil; -using Mono.Cecil.Cil; -using Mono.Collections.Generic; -using Xamarin.Android.Tasks; - -namespace MonoDroid.Tuner; - -/// -/// Post-trimming version of RemoveResourceDesignerStep that inlines resource designer -/// constants and removes designer classes, matching the original ILLink behavior. -/// The core logic is identical to the ILLink RemoveResourceDesignerStep / LinkDesignerBase. -/// -class PostTrimmingRemoveResourceDesignerStep : IAssemblyModifierPipelineStep -{ - readonly IList allAssemblies; - readonly Action logMessage; - readonly Regex opCodeRegex = new Regex (@"([\w]+): ([\w]+) ([\w.]+) ([\w:./]+)"); - - TypeDefinition mainDesigner = null; - AssemblyDefinition mainAssembly = null; - CustomAttribute mainDesignerAttribute; - Dictionary designerConstants; - bool designerLoaded; - - public PostTrimmingRemoveResourceDesignerStep (IList allAssemblies, Action logMessage) - { - this.allAssemblies = allAssemblies; - this.logMessage = logMessage; - } - - public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) - { - LoadDesigner (); - context.IsAssemblyModified |= ProcessAssemblyDesigner (assembly); - } - - void LoadDesigner () - { - if (designerLoaded) - return; - designerLoaded = true; - - foreach (var asm in allAssemblies) { - if (FindResourceDesigner (asm, mainApplication: true, designer: out mainDesigner, designerAttribute: out mainDesignerAttribute)) { - mainAssembly = asm; - break; - } - } - if (mainDesigner == null) { - logMessage (" Main Designer not found."); - return; - } - logMessage ($" Main Designer found {mainDesigner.FullName}."); - designerConstants = BuildResourceDesignerFieldLookup (mainDesigner); - } - - bool ProcessAssemblyDesigner (AssemblyDefinition assembly) - { - if (mainDesigner == null) - return false; - if (MonoAndroidHelper.IsFrameworkAssembly (assembly)) - return false; - - logMessage ($" Fixing up {assembly.Name.Name}"); - TypeDefinition localDesigner = null; - CustomAttribute designerAttribute; - if (assembly != mainAssembly) { - logMessage ($" {assembly.Name.Name} is not the main assembly. "); - if (!FindResourceDesigner (assembly, mainApplication: false, designer: out localDesigner, designerAttribute: out designerAttribute)) { - logMessage ($" {assembly.Name.Name} does not have a designer file."); - return false; - } - } else { - logMessage ($" {assembly.Name.Name} is the main assembly. "); - localDesigner = mainDesigner; - designerAttribute = mainDesignerAttribute; - } - - logMessage ($" {assembly.Name.Name} has designer {localDesigner.FullName}."); - - FixupAssemblyTypes (assembly, localDesigner); - - ClearDesignerClass (localDesigner); - if (designerAttribute != null) { - assembly.CustomAttributes.Remove (designerAttribute); - } - return true; - } - - // ---- Methods below are from LinkDesignerBase / RemoveResourceDesignerStep, adapted for non-ILLink use ---- - - bool FindResourceDesigner (AssemblyDefinition assembly, bool mainApplication, out TypeDefinition designer, out CustomAttribute designerAttribute) - { - string designerFullName = null; - designer = null; - designerAttribute = null; - foreach (CustomAttribute attribute in assembly.CustomAttributes) - { - if (attribute.AttributeType.FullName == "Android.Runtime.ResourceDesignerAttribute") - { - designerAttribute = attribute; - if (attribute.HasProperties) - { - foreach (var p in attribute.Properties) - { - if (p.Name == "IsApplication" && (bool)p.Argument.Value == (mainApplication ? mainApplication : (bool)p.Argument.Value)) - { - designerFullName = attribute.ConstructorArguments[0].Value.ToString (); - break; - } - } - } - break; - - } - } - - if (string.IsNullOrEmpty(designerFullName)) { - logMessage ($"Inspecting member references for assembly: {assembly.FullName};"); - var memberRefs = assembly.MainModule.GetMemberReferences (); - foreach (var memberRef in memberRefs) { - string declaringType = memberRef.DeclaringType?.ToString () ?? string.Empty; - if (!declaringType.Contains (".Resource/")) { - continue; - } - if (declaringType.Contains ("_Microsoft.Android.Resource.Designer")) { - continue; - } - var resolved = false; - try { - var def = memberRef.Resolve (); - if (resolved = def != null) { - logMessage ($"Resolved member `{memberRef?.Name}`"); - } - } catch (Exception ex) { - logMessage ($"Exception resolving member `{memberRef?.Name}`: {ex}"); - resolved = false; - } - if (!resolved) { - logMessage ($"Adding _Linker.Generated.Resource to {assembly.Name.Name}. Could not resolve {memberRef?.Name} : {declaringType}"); - designer = new TypeDefinition ("_Linker.Generated", "Resource", TypeAttributes.Public | TypeAttributes.AnsiClass); - designer.BaseType = new TypeDefinition ("System", "Object", TypeAttributes.Public | TypeAttributes.AnsiClass); - return true; - } - } - } - - if (string.IsNullOrEmpty(designerFullName)) - return false; - - foreach (ModuleDefinition module in assembly.Modules) - { - foreach (TypeDefinition type in module.Types) - { - if (type.FullName == designerFullName) - { - designer = type; - return true; - } - } - } - return false; - } - - void FixBody (MethodBody body, TypeDefinition designer) - { - Dictionary instructions = new Dictionary(); - var processor = body.GetILProcessor (); - string designerFullName = $"{designer.FullName}/"; - bool isDesignerMethod = designerFullName.Contains (body.Method.DeclaringType.FullName); - string declaringTypeName = body.Method.DeclaringType.Name; - foreach (var i in body.Instructions) - { - string line = i.ToString (); - if ((line.Contains (designerFullName) || (isDesignerMethod && i.OpCode == OpCodes.Stsfld)) && !instructions.ContainsKey (i)) - { - var match = opCodeRegex.Match (line); - if (match.Success && match.Groups.Count == 5) { - string key = match.Groups[4].Value.Replace (designerFullName, string.Empty); - if (isDesignerMethod) { - key = declaringTypeName +"::" + key; - } - if (designerConstants.ContainsKey (key) && !instructions.ContainsKey (i)) - instructions.Add(i, designerConstants [key]); - } - } - } - if (instructions.Count > 0) - logMessage ($" Fixing up {body.Method.FullName}"); - foreach (var i in instructions) - { - var newCode = Extensions.CreateLoadArraySizeOrOffsetInstruction (i.Value); - logMessage ($" Replacing {i.Key}"); - logMessage ($" With {newCode}"); - processor.Replace(i.Key, newCode); - } - } - - Dictionary BuildResourceDesignerFieldLookup (TypeDefinition type) - { - var output = new Dictionary (); - foreach (TypeDefinition definition in type.NestedTypes) - { - foreach (FieldDefinition field in definition.Fields) - { - string key = $"{definition.Name}::{field.Name}"; - if (!output.ContainsKey (key)) - output.Add(key, int.Parse (field.Constant?.ToString () ?? "0", CultureInfo.InvariantCulture)); - } - } - return output; - } - - void ClearDesignerClass (TypeDefinition designer, bool completely = false) - { - logMessage ($" TryRemoving {designer.FullName}"); - // for each of the nested types clear all but the - // int[] fields. - if (!completely) { - for (int i = designer.NestedTypes.Count -1; i >= 0; i--) { - var nestedType = designer.NestedTypes [i]; - RemoveFieldsFromType (nestedType, designer.Module); - if (nestedType.Fields.Count == 0) { - // no fields we do not need this class at all. - designer.NestedTypes.RemoveAt (i); - } - } - RemoveUpdateIdValues (designer); - } else { - designer.NestedTypes.Clear (); - } - designer.Fields.Clear (); - designer.Properties.Clear (); - designer.CustomAttributes.Clear (); - designer.Interfaces.Clear (); - designer.Events.Clear (); - } - - void FixType (TypeDefinition type, TypeDefinition localDesigner) - { - foreach (MethodDefinition method in type.Methods) - { - if (!method.HasBody) - continue; - FixBody (method.Body, localDesigner); - } - foreach (PropertyDefinition property in type.Properties) - { - if (property.GetMethod != null && property.GetMethod.HasBody) - { - FixBody (property.GetMethod.Body, localDesigner); - } - if (property.SetMethod != null && property.SetMethod.HasBody) - { - FixBody (property.SetMethod.Body, localDesigner); - } - } - foreach (TypeDefinition nestedType in type.NestedTypes) - { - FixType (nestedType, localDesigner); - } - } - - void FixupAssemblyTypes (AssemblyDefinition assembly, TypeDefinition designer) - { - foreach (ModuleDefinition module in assembly.Modules) - { - foreach (TypeDefinition type in module.Types) - { - if (type.FullName == designer.FullName) - continue; - FixType (type, designer); - } - } - } - - void RemoveFieldsFromType (TypeDefinition type, ModuleDefinition module) - { - for (int i = type.Fields.Count - 1; i >= 0; i--) { - var field = type.Fields [i]; - if (field.FieldType.IsArray) { - continue; - } - logMessage ($"Removing {type.Name}::{field.Name}"); - type.Fields.RemoveAt (i); - } - } - - void RemoveUpdateIdValues (TypeDefinition type) - { - foreach (var method in type.Methods) { - if (method.Name.Contains ("UpdateIdValues")) { - FixUpdateIdValuesBody (method); - } else { - FixBody (method.Body, type); - } - } - - foreach (var nestedType in type.NestedTypes) { - RemoveUpdateIdValues (nestedType); - } - } - - void FixUpdateIdValuesBody (MethodDefinition method) - { - List finalInstructions = new List (); - Collection instructions = method.Body.Instructions; - for (int i = 0; i < method.Body.Instructions.Count-1; i++) { - Instruction instruction = instructions[i]; - string line = instruction.ToString (); - bool found = line.Contains ("Int32[]") || instruction.OpCode == OpCodes.Ret; - if (!found) { - method.Body.Instructions.Remove (instruction); - i--; - } - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/RemoveResourceDesignerStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/RemoveResourceDesignerStep.cs index c5a34887a0f..51e25383a0d 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/RemoveResourceDesignerStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/RemoveResourceDesignerStep.cs @@ -1,123 +1,323 @@ #nullable disable -using Mono.Cecil; -using Mono.Linker; -using Mono.Linker.Steps; using System; -using System.Linq; -using Xamarin.Android.Tasks; using System.Collections.Generic; -using Mono.Cecil.Cil; +using System.Globalization; using System.Text.RegularExpressions; -#if ILLINK -using Microsoft.Android.Sdk.ILLink; -#endif +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Collections.Generic; +using Xamarin.Android.Tasks; + +namespace MonoDroid.Tuner; -namespace MonoDroid.Tuner +/// +/// Inlines resource designer constants and removes designer classes. +/// The core logic is adapted from the original ILLink RemoveResourceDesignerStep / LinkDesignerBase. +/// +class RemoveResourceDesignerStep : IAssemblyModifierPipelineStep { - public class RemoveResourceDesignerStep : LinkDesignerBase + readonly IList allAssemblies; + readonly Action logMessage; + readonly Regex opCodeRegex = new Regex (@"([\w]+): ([\w]+) ([\w.]+) ([\w:./]+)"); + + TypeDefinition mainDesigner = null; + AssemblyDefinition mainAssembly = null; + CustomAttribute mainDesignerAttribute; + Dictionary designerConstants; + bool designerLoaded; + + public RemoveResourceDesignerStep (IList allAssemblies, Action logMessage) + { + this.allAssemblies = allAssemblies; + this.logMessage = logMessage; + } + + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) { - TypeDefinition mainDesigner = null; - AssemblyDefinition mainAssembly = null; - CustomAttribute mainDesignerAttribute; - Dictionary designerConstants; - Regex opCodeRegex = new Regex (@"([\w]+): ([\w]+) ([\w.]+) ([\w:./]+)"); + LoadDesigner (); + context.IsAssemblyModified |= ProcessAssemblyDesigner (assembly); + } + + void LoadDesigner () + { + if (designerLoaded) + return; + designerLoaded = true; + + foreach (var asm in allAssemblies) { + if (FindResourceDesigner (asm, mainApplication: true, designer: out mainDesigner, designerAttribute: out mainDesignerAttribute)) { + mainAssembly = asm; + break; + } + } + if (mainDesigner == null) { + logMessage (" Main Designer not found."); + return; + } + logMessage ($" Main Designer found {mainDesigner.FullName}."); + designerConstants = BuildResourceDesignerFieldLookup (mainDesigner); + } + + bool ProcessAssemblyDesigner (AssemblyDefinition assembly) + { + if (mainDesigner == null) + return false; + if (MonoAndroidHelper.IsFrameworkAssembly (assembly)) + return false; + + logMessage ($" Fixing up {assembly.Name.Name}"); + TypeDefinition localDesigner = null; + CustomAttribute designerAttribute; + if (assembly != mainAssembly) { + logMessage ($" {assembly.Name.Name} is not the main assembly. "); + if (!FindResourceDesigner (assembly, mainApplication: false, designer: out localDesigner, designerAttribute: out designerAttribute)) { + logMessage ($" {assembly.Name.Name} does not have a designer file."); + return false; + } + } else { + logMessage ($" {assembly.Name.Name} is the main assembly. "); + localDesigner = mainDesigner; + designerAttribute = mainDesignerAttribute; + } + + logMessage ($" {assembly.Name.Name} has designer {localDesigner.FullName}."); + + FixupAssemblyTypes (assembly, localDesigner); + + ClearDesignerClass (localDesigner); + if (designerAttribute != null) { + assembly.CustomAttributes.Remove (designerAttribute); + } + return true; + } + + // ---- Methods below are from LinkDesignerBase, adapted for non-ILLink use ---- - protected override void LoadDesigner () + bool FindResourceDesigner (AssemblyDefinition assembly, bool mainApplication, out TypeDefinition designer, out CustomAttribute designerAttribute) + { + string designerFullName = null; + designer = null; + designerAttribute = null; + foreach (CustomAttribute attribute in assembly.CustomAttributes) { - if (mainAssembly != null) - return; - // resolve the MainAssembly Resource designer TypeDefinition - AndroidLinkConfiguration config = AndroidLinkConfiguration.GetInstance (Context); - if (config == null) - return; - foreach(var asm in config.Assemblies) { - if (FindResourceDesigner (asm, mainApplication: true, designer: out mainDesigner, designerAttribute: out mainDesignerAttribute)) { - mainAssembly = asm; - break; + if (attribute.AttributeType.FullName == "Android.Runtime.ResourceDesignerAttribute") + { + designerAttribute = attribute; + if (attribute.HasProperties) + { + foreach (var p in attribute.Properties) + { + if (p.Name == "IsApplication" && (bool)p.Argument.Value == (mainApplication ? mainApplication : (bool)p.Argument.Value)) + { + designerFullName = attribute.ConstructorArguments[0].Value.ToString (); + break; + } + } } + break; + } - if (mainDesigner == null) { - LogMessage ($" Main Designer not found."); - return; + } + + if (string.IsNullOrEmpty(designerFullName)) { + logMessage ($"Inspecting member references for assembly: {assembly.FullName};"); + var memberRefs = assembly.MainModule.GetMemberReferences (); + foreach (var memberRef in memberRefs) { + string declaringType = memberRef.DeclaringType?.ToString () ?? string.Empty; + if (!declaringType.Contains (".Resource/")) { + continue; + } + if (declaringType.Contains ("_Microsoft.Android.Resource.Designer")) { + continue; + } + var resolved = false; + try { + var def = memberRef.Resolve (); + if (resolved = def != null) { + logMessage ($"Resolved member `{memberRef?.Name}`"); + } + } catch (Exception ex) { + logMessage ($"Exception resolving member `{memberRef?.Name}`: {ex}"); + resolved = false; + } + if (!resolved) { + logMessage ($"Adding _Linker.Generated.Resource to {assembly.Name.Name}. Could not resolve {memberRef?.Name} : {declaringType}"); + designer = new TypeDefinition ("_Linker.Generated", "Resource", TypeAttributes.Public | TypeAttributes.AnsiClass); + designer.BaseType = new TypeDefinition ("System", "Object", TypeAttributes.Public | TypeAttributes.AnsiClass); + return true; + } } - LogMessage ($" Main Designer found {mainDesigner.FullName}."); - designerConstants = BuildResourceDesignerFieldLookup (mainDesigner); } - protected override void EndProcess () + if (string.IsNullOrEmpty(designerFullName)) + return false; + + foreach (ModuleDefinition module in assembly.Modules) { - if (mainDesigner != null) { - LogMessage ($" Setting Action on {mainAssembly.Name} to Save."); - Annotations.SetAction (mainAssembly, AssemblyAction.Save); + foreach (TypeDefinition type in module.Types) + { + if (type.FullName == designerFullName) + { + designer = type; + return true; + } } } + return false; + } - protected override void FixBody (MethodBody body, TypeDefinition designer) + void FixBody (MethodBody body, TypeDefinition designer) + { + Dictionary instructions = new Dictionary(); + var processor = body.GetILProcessor (); + string designerFullName = $"{designer.FullName}/"; + bool isDesignerMethod = designerFullName.Contains (body.Method.DeclaringType.FullName); + string declaringTypeName = body.Method.DeclaringType.Name; + foreach (var i in body.Instructions) { - Dictionary instructions = new Dictionary(); - var processor = body.GetILProcessor (); - string designerFullName = $"{designer.FullName}/"; - bool isDesignerMethod = designerFullName.Contains (body.Method.DeclaringType.FullName); - string declaringTypeName = body.Method.DeclaringType.Name; - foreach (var i in body.Instructions) + string line = i.ToString (); + if ((line.Contains (designerFullName) || (isDesignerMethod && i.OpCode == OpCodes.Stsfld)) && !instructions.ContainsKey (i)) { - string line = i.ToString (); - if ((line.Contains (designerFullName) || (isDesignerMethod && i.OpCode == OpCodes.Stsfld)) && !instructions.ContainsKey (i)) - { - var match = opCodeRegex.Match (line); - if (match.Success && match.Groups.Count == 5) { - string key = match.Groups[4].Value.Replace (designerFullName, string.Empty); - if (isDesignerMethod) { - key = declaringTypeName +"::" + key; - } - if (designerConstants.ContainsKey (key) && !instructions.ContainsKey (i)) - instructions.Add(i, designerConstants [key]); + var match = opCodeRegex.Match (line); + if (match.Success && match.Groups.Count == 5) { + string key = match.Groups[4].Value.Replace (designerFullName, string.Empty); + if (isDesignerMethod) { + key = declaringTypeName +"::" + key; } + if (designerConstants.ContainsKey (key) && !instructions.ContainsKey (i)) + instructions.Add(i, designerConstants [key]); } } - if (instructions.Count > 0) - LogMessage ($" Fixing up {body.Method.FullName}"); - foreach (var i in instructions) + } + if (instructions.Count > 0) + logMessage ($" Fixing up {body.Method.FullName}"); + foreach (var i in instructions) + { + var newCode = Extensions.CreateLoadArraySizeOrOffsetInstruction (i.Value); + logMessage ($" Replacing {i.Key}"); + logMessage ($" With {newCode}"); + processor.Replace(i.Key, newCode); + } + } + + Dictionary BuildResourceDesignerFieldLookup (TypeDefinition type) + { + var output = new Dictionary (); + foreach (TypeDefinition definition in type.NestedTypes) + { + foreach (FieldDefinition field in definition.Fields) { - var newCode = Extensions.CreateLoadArraySizeOrOffsetInstruction (i.Value); - LogMessage ($" Replacing {i.Key}"); - LogMessage ($" With {newCode}"); - processor.Replace(i.Key, newCode); + string key = $"{definition.Name}::{field.Name}"; + if (!output.ContainsKey (key)) + output.Add(key, int.Parse (field.Constant?.ToString () ?? "0", CultureInfo.InvariantCulture)); } } + return output; + } - internal override bool ProcessAssemblyDesigner (AssemblyDefinition assembly) + void ClearDesignerClass (TypeDefinition designer, bool completely = false) + { + logMessage ($" TryRemoving {designer.FullName}"); + // for each of the nested types clear all but the + // int[] fields. + if (!completely) { + for (int i = designer.NestedTypes.Count -1; i >= 0; i--) { + var nestedType = designer.NestedTypes [i]; + RemoveFieldsFromType (nestedType, designer.Module); + if (nestedType.Fields.Count == 0) { + // no fields we do not need this class at all. + designer.NestedTypes.RemoveAt (i); + } + } + RemoveUpdateIdValues (designer); + } else { + designer.NestedTypes.Clear (); + } + designer.Fields.Clear (); + designer.Properties.Clear (); + designer.CustomAttributes.Clear (); + designer.Interfaces.Clear (); + designer.Events.Clear (); + } + + void FixType (TypeDefinition type, TypeDefinition localDesigner) + { + foreach (MethodDefinition method in type.Methods) { - if (mainDesigner == null) - return false; - if (MonoAndroidHelper.IsFrameworkAssembly (assembly)) - return false; + if (!method.HasBody) + continue; + FixBody (method.Body, localDesigner); + } + foreach (PropertyDefinition property in type.Properties) + { + if (property.GetMethod != null && property.GetMethod.HasBody) + { + FixBody (property.GetMethod.Body, localDesigner); + } + if (property.SetMethod != null && property.SetMethod.HasBody) + { + FixBody (property.SetMethod.Body, localDesigner); + } + } + foreach (TypeDefinition nestedType in type.NestedTypes) + { + FixType (nestedType, localDesigner); + } + } - LogMessage ($" Fixing up {assembly.Name.Name}"); - TypeDefinition localDesigner = null; - CustomAttribute designerAttribute; - if (assembly != mainAssembly) { - LogMessage ($" {assembly.Name.Name} is not the main assembly. "); - if (!FindResourceDesigner (assembly, mainApplication: false, designer: out localDesigner, designerAttribute: out designerAttribute)) { - Context.LogMessage ($" {assembly.Name.Name} does not have a designer file."); - return false; - } - } else { - LogMessage ($" {assembly.Name.Name} is the main assembly. "); - localDesigner = mainDesigner; - designerAttribute = mainDesignerAttribute; + void FixupAssemblyTypes (AssemblyDefinition assembly, TypeDefinition designer) + { + foreach (ModuleDefinition module in assembly.Modules) + { + foreach (TypeDefinition type in module.Types) + { + if (type.FullName == designer.FullName) + continue; + FixType (type, designer); + } + } + } + + void RemoveFieldsFromType (TypeDefinition type, ModuleDefinition module) + { + for (int i = type.Fields.Count - 1; i >= 0; i--) { + var field = type.Fields [i]; + if (field.FieldType.IsArray) { + continue; } + logMessage ($"Removing {type.Name}::{field.Name}"); + type.Fields.RemoveAt (i); + } + } - LogMessage ($" {assembly.Name.Name} has designer {localDesigner.FullName}."); + void RemoveUpdateIdValues (TypeDefinition type) + { + foreach (var method in type.Methods) { + if (method.Name.Contains ("UpdateIdValues")) { + FixUpdateIdValuesBody (method); + } else { + FixBody (method.Body, type); + } + } - FixupAssemblyTypes (assembly, localDesigner); + foreach (var nestedType in type.NestedTypes) { + RemoveUpdateIdValues (nestedType); + } + } - ClearDesignerClass (localDesigner); - if (designerAttribute != null) { - assembly.CustomAttributes.Remove (designerAttribute); + void FixUpdateIdValuesBody (MethodDefinition method) + { + List finalInstructions = new List (); + Collection instructions = method.Body.Instructions; + for (int i = 0; i < method.Body.Instructions.Count-1; i++) { + Instruction instruction = instructions[i]; + string line = instruction.ToString (); + bool found = line.Contains ("Int32[]") || instruction.OpCode == OpCodes.Ret; + if (!found) { + method.Body.Instructions.Remove (instruction); + i--; } - return true; } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs index 3103146d503..27fed7ff0aa 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs @@ -59,7 +59,7 @@ public override bool RunTask () foreach (var item in Assemblies) { allAssemblies.Add (resolver.GetAssembly (item.ItemSpec)); } - steps.Add (new PostTrimmingRemoveResourceDesignerStep (allAssemblies, (msg) => Log.LogDebugMessage (msg))); + steps.Add (new RemoveResourceDesignerStep (allAssemblies, (msg) => Log.LogDebugMessage (msg))); } foreach (var item in Assemblies) { 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 ba91e4c25dc..8fea32fb444 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -52,13 +52,11 @@ - - + -