From 17dc49c7c17dfb0acdb8feb13ec385f3a18ac2ec Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 23 Mar 2026 22:37:15 +0100 Subject: [PATCH 1/2] [dotnet-linker] Create an xml descriptor instead of manual marking when applying the [Preserve] attribute. Create an xml descriptor file instead of manually marking when applying the [Preserve] attribute. I first tried using [DynamicDependency] attributes, but it turns out [DynamicDependency] attributes can't accurately express/replace [Preserve] semantics, so I went with an xml descriptor file instead. The [DynamicDependency] attribute code is still present, as an opt-in, because it was already there used for NativeAOT (even though we're always using the xml descriptor logic now, even for NativeAOT). This makes it easier to move this code out of a custom linker step in the future. Also move the removal of any [Preserve] attributes to the RemoveAttributes step. Contributes towards https://github.com/dotnet/macios/issues/17693. --- dotnet/targets/Xamarin.Shared.Sdk.targets | 14 + tools/common/DerivedLinkContext.cs | 2 + tools/dotnet-linker/AppBundleRewriter.cs | 16 +- .../ApplyPreserveAttributeBase.cs | 372 ++++++++---------- .../ApplyPreserveAttributeStep.cs | 307 +++++++++++++++ tools/dotnet-linker/CecilExtensions.cs | 45 +++ .../Steps/AssemblyModifierStep.cs | 14 +- .../Steps/RemoveAttributesStep.cs | 3 + .../Steps/SetBeforeFieldInitStep.cs | 6 - tools/dotnet-linker/dotnet-linker.csproj | 3 - tools/linker/ApplyPreserveAttribute.cs | 67 ---- 11 files changed, 553 insertions(+), 296 deletions(-) create mode 100644 tools/dotnet-linker/ApplyPreserveAttributeStep.cs delete mode 100644 tools/linker/ApplyPreserveAttribute.cs diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets index 9bf01ee471d6..3d51bcbfefc2 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.targets @@ -551,6 +551,8 @@ <_UseDynamicDependenciesForSmartEnumPreservation Condition="'$(_UseDynamicDependenciesForSmartEnumPreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking) <_UseDynamicDependenciesForBlockCodePreservation Condition="'$(_UseDynamicDependenciesForBlockCodePreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking) <_UseDynamicDependenciesForGeneratedCodeOptimizations Condition="'$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' == ''">$(_UseDynamicDependenciesInsteadOfMarking) + <_UseLinkDescriptionForApplyPreserveAttribute Condition="'$(_UseLinkDescriptionForApplyPreserveAttribute)' == '' And '$(_XamarinRuntime)' == 'NativeAOT'">true + <_UseLinkDescriptionForApplyPreserveAttribute Condition="'$(_UseLinkDescriptionForApplyPreserveAttribute)' == ''">$(_UseDynamicDependenciesInsteadOfMarking) @@ -756,6 +758,7 @@ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' == 'true'" /> <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveBlockCodeStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForBlockCodePreservation)' == 'true'" /> <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.OptimizeGeneratedCodeStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' == 'true'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.ApplyPreserveAttributeStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseLinkDescriptionForApplyPreserveAttribute)' == 'true'" /> <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.RegistrarRemovalTrackingStep" /> @@ -1084,6 +1087,7 @@ <_LinkerItemFiles Include="$(_LinkerItemsDirectory)/_AssembliesToAOT.items" /> <_LinkerItemFiles Include="$(_LinkerItemsDirectory)/_FrameworkToPublish.items" /> <_LinkerItemFiles Include="$(_LinkerItemsDirectory)/_DynamicLibraryToPublish.items" /> + <_LinkerItemFiles Include="$(_LinkerItemsDirectory)/TrimmerRootDescriptor.items" /> @@ -1108,7 +1112,17 @@ <_AssembliesToAOT Include="@(_AllLinkerItems)" Condition="'%(_AllLinkerItems.SourceFile)' == '_AssembliesToAOT.items'" /> <_FrameworkToPublish Include="@(_AllLinkerItems)" Condition="'%(_AllLinkerItems.SourceFile)' == '_FrameworkToPublish.items'" /> <_DynamicLibraryToPublish Include="@(_AllLinkerItems)" Condition="'%(_AllLinkerItems.SourceFile)' == '_DynamicLibraryToPublish.items'" /> + <_TrimmerRootDescriptorFromCustomLinkerSteps Include="@(_AllLinkerItems)" Condition="'%(_AllLinkerItems.SourceFile)' == 'TrimmerRootDescriptor.items'" /> + + + + + $(NoWarn);IL2008 + diff --git a/tools/common/DerivedLinkContext.cs b/tools/common/DerivedLinkContext.cs index 35a39f09f125..1cc8f9e0e187 100644 --- a/tools/common/DerivedLinkContext.cs +++ b/tools/common/DerivedLinkContext.cs @@ -48,6 +48,8 @@ public class DerivedLinkContext : LinkContext { // so we need a second dictionary Dictionary LinkedAwayTypeMap = new Dictionary (); + public bool DidRunApplyPreserveAttributeStep { get; set; } + public DerivedLinkContext (LinkerConfiguration configuration, Application app) #if !LEGACY_TOOLS : base (configuration) diff --git a/tools/dotnet-linker/AppBundleRewriter.cs b/tools/dotnet-linker/AppBundleRewriter.cs index e96fc999cfea..a3053029e351 100644 --- a/tools/dotnet-linker/AppBundleRewriter.cs +++ b/tools/dotnet-linker/AppBundleRewriter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text; using Mono.Cecil; using Mono.Cecil.Cil; @@ -1384,7 +1385,7 @@ public bool AddAttributeToStaticConstructor (TypeDefinition onType, CustomAttrib return modified; } - MethodDefinition GetOrCreateStaticConstructor (TypeDefinition type, out bool modified) + public MethodDefinition GetOrCreateStaticConstructor (TypeDefinition type, out bool modified) { modified = false; @@ -1408,7 +1409,7 @@ MethodDefinition GetOrCreateStaticConstructor (TypeDefinition type, out bool mod /// The provider to which the attribute should be added. /// The attribute to add. /// Whether the attribute was added or not. - bool AddAttributeOnlyOnce (ICustomAttributeProvider provider, CustomAttribute attribute) + public bool AddAttributeOnlyOnce (ICustomAttributeProvider provider, CustomAttribute attribute) { if (provider.HasCustomAttributes) { foreach (var ca in provider.CustomAttributes) { @@ -1460,7 +1461,18 @@ bool AddAttributeOnlyOnce (ICustomAttributeProvider provider, CustomAttribute at } } provider.CustomAttributes.Add (attribute); + if (DebugAttributes) + Console.WriteLine ($"Added {attribute.RenderAttribute ()} to {provider}"); return true; } + + static bool? debug_attributes; + static bool DebugAttributes { + get { + if (!debug_attributes.HasValue) + debug_attributes = !string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("PRINT_ATTRIBUTES")); + return debug_attributes.Value; + } + } } } diff --git a/tools/dotnet-linker/ApplyPreserveAttributeBase.cs b/tools/dotnet-linker/ApplyPreserveAttributeBase.cs index 4b67b0195d1a..1d4bc9520977 100644 --- a/tools/dotnet-linker/ApplyPreserveAttributeBase.cs +++ b/tools/dotnet-linker/ApplyPreserveAttributeBase.cs @@ -1,167 +1,229 @@ // This is copied from https://github.com/mono/linker/blob/fa9ccbdaf6907c69ef1bb117906f8f012218d57f/src/tuner/Mono.Tuner/ApplyPreserveAttributeBase.cs // and modified to work without a Profile class. -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; + using System.Linq; -using System.Text; using Mono.Linker; using Mono.Linker.Steps; using Mono.Cecil; -using Mono.Cecil.Cil; - -using Xamarin.Bundler; -using Xamarin.Linker; -using Xamarin.Utils; +using Mono.Tuner; #nullable enable -namespace Mono.Tuner { +namespace Xamarin.Linker.Steps { - public abstract class ApplyPreserveAttributeBase : ConfigurationAwareSubStep { - - AppBundleRewriter? abr; - Queue deferredActions = new (); + public partial class ApplyPreserveAttribute : ConfigurationAwareSubStep, IApplyPreserveAttribute { + ApplyPreserveAttributeImpl impl; protected override string Name { get => "Apply Preserve Attribute"; } protected override int ErrorCode { get => 2450; } - // set 'removeAttribute' to true if you want the preserved attribute to be removed from the final assembly - protected abstract bool IsPreservedAttribute (ICustomAttributeProvider provider, CustomAttribute attribute, out bool removeAttribute); - public override SubStepTargets Targets => SubStepTargets.Assembly; - public override void Initialize (LinkContext context) + public ApplyPreserveAttribute () + { + impl = new ApplyPreserveAttributeImpl (this); + } + + public override bool IsActiveFor (AssemblyDefinition assembly) { - base.Initialize (context); + // It's either this step, or ApplyPreserveAttributeStep. If ApplyPreserveAttributeStep already ran, then we shouldn't run this step. + if (Configuration.DerivedLinkContext.DidRunApplyPreserveAttributeStep) + return false; - if (Configuration.Application.XamarinRuntime == XamarinRuntime.NativeAOT) - abr = Configuration.AppBundleRewriter; + return Annotations.GetAction (assembly) == AssemblyAction.Link; } protected override void Process (AssemblyDefinition assembly) { - BrowseTypes (assembly.MainModule.Types); - ProcessDeferredActions (); + impl.Process (assembly); + } + + bool IApplyPreserveAttribute.PreserveUnconditional (IMetadataTokenProvider provider) + { + if (provider is MethodDefinition method) + Annotations.SetAction (method, MethodAction.Parse); + Annotations.Mark (provider); + return true; + } + + bool IApplyPreserveAttribute.PreserveType (TypeDefinition type, bool allMembers) + { + Annotations.Mark (type); + if (allMembers) + Annotations.SetPreserve (type, TypePreserve.All); + return true; + } + + bool IApplyPreserveAttribute.PreserveConditional (TypeDefinition onType, MethodDefinition forMethod) + { + Annotations.SetAction (forMethod, MethodAction.Parse); + Annotations.AddPreservedMethod (onType, forMethod); + return true; + } + } + + public interface IApplyPreserveAttribute { + bool PreserveType (TypeDefinition type, bool allMembers); + bool PreserveUnconditional (IMetadataTokenProvider provider); + bool PreserveConditional (TypeDefinition onType, MethodDefinition forMethod); + } + + public class ApplyPreserveAttributeImpl { + IApplyPreserveAttribute applyPreserveAttribute; + + public ApplyPreserveAttributeImpl (IApplyPreserveAttribute applyPreserveAttribute) + { + this.applyPreserveAttribute = applyPreserveAttribute; + } + + bool IsPreservedAttribute (ICustomAttributeProvider provider, CustomAttribute attribute) + { + TypeReference type = attribute.Constructor.DeclaringType; + return type.Name == "PreserveAttribute"; + } + + public bool Process (AssemblyDefinition assembly) + { + var modified = false; + modified |= BrowseTypes (assembly.MainModule.Types); + modified |= ProcessAssemblyAttributes (assembly); + return modified; } - void BrowseTypes (IEnumerable types) + bool ProcessAssemblyAttributes (AssemblyDefinition assembly) { - foreach (TypeDefinition type in types) { - ProcessType (type); + if (!assembly.HasCustomAttributes) + return false; + + var modified = false; + foreach (var attribute in assembly.CustomAttributes) { + if (!attribute.Constructor.DeclaringType.Is (Namespaces.Foundation, "PreserveAttribute")) + continue; + + if (!attribute.HasConstructorArguments) + continue; + var tr = (attribute.ConstructorArguments [0].Value as TypeReference); + if (tr is null) + continue; + + // we do not call `this.ProcessType` since + // (a) we're potentially processing a different assembly and `is_active` represent the current one + // (b) it will try to fetch the [Preserve] attribute on the type (and it's not there) as `base` would + var type = tr.Resolve (); + + modified |= PreserveType (type, attribute); + } + return modified; + } + + bool BrowseTypes (IEnumerable types) + { + var modified = false; + foreach (var type in (new List (types))) { + modified |= ProcessType (type); if (type.HasFields) { - foreach (FieldDefinition field in type.Fields) - ProcessField (field); + foreach (var field in type.Fields.ToArray ()) + modified |= ProcessField (field); } if (type.HasMethods) { - foreach (MethodDefinition method in type.Methods) - ProcessMethod (method); + foreach (var method in type.Methods.ToArray ()) + modified |= ProcessMethod (method); } if (type.HasProperties) { - foreach (PropertyDefinition property in type.Properties) - ProcessProperty (property); + foreach (var property in type.Properties.ToArray ()) + modified |= ProcessProperty (property); } if (type.HasEvents) { - foreach (EventDefinition @event in type.Events) - ProcessEvent (@event); + foreach (var @event in type.Events.ToArray ()) + modified |= ProcessEvent (@event); } if (type.HasNestedTypes) { - BrowseTypes (type.NestedTypes); + modified |= BrowseTypes (type.NestedTypes); } } + return modified; } - void ProcessDeferredActions () + bool ProcessType (TypeDefinition type) { - while (deferredActions.Count > 0) { - var action = deferredActions.Dequeue (); - action.Invoke (); - } - } - - public override bool IsActiveFor (AssemblyDefinition assembly) - { - return Annotations.GetAction (assembly) == AssemblyAction.Link; + return TryApplyPreserveAttribute (type); } - protected override void Process (TypeDefinition type) - { - TryApplyPreserveAttribute (type); - } - - protected override void Process (FieldDefinition field) + bool ProcessField (FieldDefinition field) { + var modified = false; foreach (var attribute in GetPreserveAttributes (field)) - Mark (field, attribute); + modified |= Mark (field, attribute); + return modified; } - protected override void Process (MethodDefinition method) + bool ProcessMethod (MethodDefinition method) { - MarkMethodIfPreserved (method); + return MarkMethodIfPreserved (method); } - protected override void Process (PropertyDefinition property) + bool ProcessProperty (PropertyDefinition property) { + var modified = false; foreach (var attribute in GetPreserveAttributes (property)) { - MarkMethod (property.GetMethod, attribute); - MarkMethod (property.SetMethod, attribute); + modified |= MarkMethod (property.GetMethod, attribute); + modified |= MarkMethod (property.SetMethod, attribute); } + return modified; } - protected override void Process (EventDefinition @event) + bool ProcessEvent (EventDefinition @event) { + var modified = false; foreach (var attribute in GetPreserveAttributes (@event)) { - MarkMethod (@event.AddMethod, attribute); - MarkMethod (@event.InvokeMethod, attribute); - MarkMethod (@event.RemoveMethod, attribute); + modified |= MarkMethod (@event.AddMethod, attribute); + modified |= MarkMethod (@event.InvokeMethod, attribute); + modified |= MarkMethod (@event.RemoveMethod, attribute); } + return modified; } - void MarkMethodIfPreserved (MethodDefinition method) + bool MarkMethodIfPreserved (MethodDefinition method) { + var modified = false; foreach (var attribute in GetPreserveAttributes (method)) - MarkMethod (method, attribute); + modified |= MarkMethod (method, attribute); + return modified; } - void MarkMethod (MethodDefinition? method, CustomAttribute? preserve_attribute) + bool MarkMethod (MethodDefinition? method, CustomAttribute? preserve_attribute) { if (method is null) - return; + return false; - Mark (method, preserve_attribute); - Annotations.SetAction (method, MethodAction.Parse); + return Mark (method, preserve_attribute); } - void Mark (IMetadataTokenProvider provider, CustomAttribute? preserve_attribute) + bool Mark (IMetadataTokenProvider provider, CustomAttribute? preserve_attribute) { - if (IsConditionalAttribute (preserve_attribute)) { - PreserveConditional (provider); - return; - } + if (IsConditionalAttribute (preserve_attribute)) + return PreserveConditional (provider); - PreserveUnconditional (provider); + return PreserveUnconditional (provider); } - void PreserveConditional (IMetadataTokenProvider provider) + bool PreserveConditional (IMetadataTokenProvider provider) { var method = provider as MethodDefinition; if (method is null) { // workaround to support (uncommon but valid) conditional fields form [Preserve] - PreserveUnconditional (provider); - return; + return PreserveUnconditional (provider); } - Annotations.AddPreservedMethod (method.DeclaringType, method); - AddConditionalDynamicDependencyAttribute (method.DeclaringType, method); + return applyPreserveAttribute.PreserveConditional (method.DeclaringType, method); } static bool IsConditionalAttribute (CustomAttribute? attribute) @@ -176,55 +238,39 @@ static bool IsConditionalAttribute (CustomAttribute? attribute) return false; } - void PreserveUnconditional (IMetadataTokenProvider provider) + bool PreserveUnconditional (IMetadataTokenProvider provider) { - Annotations.Mark (provider); + var modified = false; - // We want to add a dynamic dependency attribute to preserve methods and fields - // but not to preserve types while we're marking the chain of declaring types. - if (provider is not TypeDefinition) { - AddDynamicDependencyAttribute (provider); - } + modified |= applyPreserveAttribute.PreserveUnconditional (provider); var member = provider as IMemberDefinition; if (member is null || member.DeclaringType is null) - return; + return modified; - Mark (member.DeclaringType, null); + modified |= Mark (member.DeclaringType, null); + + return modified; } - void TryApplyPreserveAttribute (TypeDefinition type) + bool TryApplyPreserveAttribute (TypeDefinition type) { + var modified = false; foreach (var attribute in GetPreserveAttributes (type)) { - PreserveType (type, attribute); + modified |= PreserveType (type, attribute); } + return modified; } List GetPreserveAttributes (ICustomAttributeProvider provider) { - List attrs = new List (); - if (!provider.HasCustomAttributes) - return attrs; - - var attributes = provider.CustomAttributes; - - for (int i = attributes.Count - 1; i >= 0; i--) { - var attribute = attributes [i]; + return new List (); - bool remote_attribute; - if (!IsPreservedAttribute (provider, attribute, out remote_attribute)) - continue; - - attrs.Add (attribute); - if (remote_attribute) - attributes.RemoveAt (i); - } - - return attrs; + return provider.CustomAttributes.Where (a => IsPreservedAttribute (provider, a)).ToList (); } - protected void PreserveType (TypeDefinition type, CustomAttribute preserveAttribute) + protected bool PreserveType (TypeDefinition type, CustomAttribute preserveAttribute) { var allMembers = false; if (preserveAttribute.HasFields) { @@ -233,114 +279,12 @@ protected void PreserveType (TypeDefinition type, CustomAttribute preserveAttrib allMembers = true; } - PreserveType (type, allMembers); - } - - protected void PreserveType (TypeDefinition type, bool allMembers) - { - Annotations.Mark (type); - if (allMembers) - Annotations.SetPreserve (type, TypePreserve.All); - AddDynamicDependencyAttribute (type, allMembers); + return PreserveType (type, allMembers); } - MethodDefinition GetOrCreateModuleConstructor (ModuleDefinition @module) + bool PreserveType (TypeDefinition type, bool allMembers) { - var moduleType = @module.GetModuleType (); - return GetOrCreateStaticConstructor (moduleType); - } - - // We want to avoid `DynamicallyAccessedMemberTypes.All` because the semantics are different - // from `[Preserve (AllMembers = true)]`. Specifically, we don't want to preserve nested types. - // `All` would also keep unused private members of base types which `Preserve` also doesn't cover. - const DynamicallyAccessedMemberTypes allMemberTypes = - DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields - | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties - | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods - | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors - | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents - | DynamicallyAccessedMemberTypes.Interfaces; - - void AddDynamicDependencyAttribute (TypeDefinition type, bool allMembers) - { - if (abr is null) - return; - - abr.ClearCurrentAssembly (); - abr.SetCurrentAssembly (type.Module.Assembly); - - var moduleConstructor = GetOrCreateModuleConstructor (type.GetModule ()); - var members = allMembers - ? allMemberTypes - : DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - - // only preserve fields for enums - if (type.IsEnum) { - members = DynamicallyAccessedMemberTypes.PublicFields; - } - - var attrib = abr.CreateDynamicDependencyAttribute (members, type); - moduleConstructor.CustomAttributes.Add (attrib); - - abr.ClearCurrentAssembly (); - } - - void AddConditionalDynamicDependencyAttribute (TypeDefinition onType, MethodDefinition forMethod) - { - if (abr is null) - return; - - deferredActions.Enqueue (() => AddDynamicDependencyAttributeToStaticConstructor (onType, forMethod)); - } - - void AddDynamicDependencyAttribute (IMetadataTokenProvider provider) - { - if (abr is null) - return; - - var member = provider as IMemberDefinition; - if (member is null) - throw ErrorHelper.CreateError (99, $"Unable to add dynamic dependency attribute to {provider.GetType ().FullName}"); - - var module = member.GetModule (); - abr.ClearCurrentAssembly (); - abr.SetCurrentAssembly (module.Assembly); - - var moduleConstructor = GetOrCreateModuleConstructor (module); - var signature = DocumentationComments.GetSignature (member); - var attrib = abr.CreateDynamicDependencyAttribute (signature, member.DeclaringType); - moduleConstructor.CustomAttributes.Add (attrib); - - abr.ClearCurrentAssembly (); - } - - void AddDynamicDependencyAttributeToStaticConstructor (TypeDefinition onType, MethodDefinition forMethod) - { - if (abr is null) - return; - - abr.ClearCurrentAssembly (); - abr.SetCurrentAssembly (onType.Module.Assembly); - - var cctor = GetOrCreateStaticConstructor (onType); - var signature = DocumentationComments.GetSignature (forMethod); - var attrib = abr.CreateDynamicDependencyAttribute (signature, onType); - cctor.CustomAttributes.Add (attrib); - Annotations.AddPreservedMethod (onType, cctor); - - abr.ClearCurrentAssembly (); - } - - MethodDefinition GetOrCreateStaticConstructor (TypeDefinition type) - { - var staticCtor = type.GetTypeConstructor (); - if (staticCtor is null) { - staticCtor = type.AddMethod (".cctor", MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName | MethodAttributes.Static, abr!.System_Void); - staticCtor.CreateBody (out var il); - il.Emit (OpCodes.Ret); - } - - return staticCtor; + return applyPreserveAttribute.PreserveType (type, allMembers); } } } diff --git a/tools/dotnet-linker/ApplyPreserveAttributeStep.cs b/tools/dotnet-linker/ApplyPreserveAttributeStep.cs new file mode 100644 index 000000000000..d25a335b4d0c --- /dev/null +++ b/tools/dotnet-linker/ApplyPreserveAttributeStep.cs @@ -0,0 +1,307 @@ +using System.IO; +using System.Linq; +using System.Xml.Linq; + +using Mono.Cecil; +using Mono.Linker; +using Mono.Linker.Steps; +using Xamarin.Bundler; +using Xamarin.Tuner; +using Xamarin.Utils; + +#nullable enable + +namespace Xamarin.Linker.Steps { + + public class ApplyPreserveAttributeStep : AssemblyModifierStep, IApplyPreserveAttribute { + sealed class XmlTypeDescription { + public XmlTypeDescription (TypeDefinition type) + { + Type = type; + } + + public TypeDefinition Type { get; } + public bool PreserveAllMembers { get; set; } + public bool PreserveFields { get; set; } + public bool PreserveType { get; set; } + public Dictionary Fields { get; } = new (StringComparer.Ordinal); + public Dictionary Methods { get; } = new (StringComparer.Ordinal); + } + + ApplyPreserveAttributeImpl impl; + readonly Dictionary> xmlDescriptions = new (StringComparer.Ordinal); + protected override string Name { get => "Apply Preserve Attribute"; } + protected override int ErrorCode { get => 2450; } + + bool? create_xml_description_file; + public bool CreateXmlDescriptionFile { + get { + if (create_xml_description_file.HasValue) + return create_xml_description_file.Value; + return Configuration.Application.XamarinRuntime == XamarinRuntime.NativeAOT; + } + set { + create_xml_description_file = value; + } + } + + public bool UseXmlDescriptionFile { get; set; } = true; + public string XmlDescriptionPath { get; set; } = string.Empty; + + public ApplyPreserveAttributeStep () + { + impl = new ApplyPreserveAttributeImpl (this); + } + + protected override void TryProcess () + { + DerivedLinkContext.DidRunApplyPreserveAttributeStep = true; + base.TryProcess (); + } + + protected override bool IsActiveFor (AssemblyDefinition assembly) + { + // We only care about assemblies that are being linked. + if (Annotations.GetAction (assembly) != AssemblyAction.Link) + return false; + + return true; + } + + protected override bool ModifyAssembly (AssemblyDefinition assembly) + { + return impl.Process (assembly); + } + + protected override void TryEndProcess () + { + if (!UseXmlDescriptionFile) + return; + + WriteXmlDescription (); + } + + bool IApplyPreserveAttribute.PreserveUnconditional (IMetadataTokenProvider provider) + { + if (UseXmlDescriptionFile) { + AddUnconditionalXmlDescription (provider); + return false; + } + + // We want to add a dynamic dependency attribute to preserve methods and fields + // but not to preserve types while we're marking the chain of declaring types. + if (provider is not TypeDefinition) { + return AddDynamicDependencyAttribute (provider); + } + return false; + } + + bool IApplyPreserveAttribute.PreserveType (TypeDefinition type, bool allMembers) + { + if (UseXmlDescriptionFile) { + AddXmlDescription (type, allMembers); + return false; + } + + return AddDynamicDependencyAttribute (type, allMembers); + } + + MethodDefinition GetOrCreateModuleConstructor (ModuleDefinition @module, out bool modified) + { + var moduleType = @module.GetModuleType (); + return abr.GetOrCreateStaticConstructor (moduleType, out modified); + } + + bool IApplyPreserveAttribute.PreserveConditional (TypeDefinition onType, MethodDefinition forMethod) + { + if (UseXmlDescriptionFile) { + AddXmlDescription (onType, forMethod, conditional: true); + return false; + } + + return AddConditionalDynamicDependencyAttribute (onType, forMethod); + } + + // We want to avoid `DynamicallyAccessedMemberTypes.All` because the semantics are different + // from `[Preserve (AllMembers = true)]`. Specifically, we don't want to preserve nested types. + // `All` would also keep unused private members of base types which `Preserve` also doesn't cover. + const DynamicallyAccessedMemberTypes allMemberTypes = + DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields + | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties + | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods + | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors + | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents + | DynamicallyAccessedMemberTypes.Interfaces; + + bool AddDynamicDependencyAttribute (TypeDefinition type, bool allMembers) + { + var moduleConstructor = GetOrCreateModuleConstructor (abr.CurrentAssembly.MainModule, out var modified); + var members = allMembers + ? allMemberTypes + : DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + + // only preserve fields for enums + if (type.IsEnum) { + members = DynamicallyAccessedMemberTypes.PublicFields; + } + + var attrib = abr.CreateDynamicDependencyAttribute (members, type); + modified |= abr.AddAttributeOnlyOnce (moduleConstructor, attrib); + return modified; + } + + bool AddConditionalDynamicDependencyAttribute (TypeDefinition onType, MethodDefinition forMethod) + { + return abr.AddDynamicDependencyAttributeToStaticConstructor (onType, forMethod); + } + + bool AddDynamicDependencyAttribute (IMetadataTokenProvider provider) + { + var member = provider as IMemberDefinition; + if (member is null) + throw ErrorHelper.CreateError (99, $"Unable to add dynamic dependency attribute to {provider.GetType ().FullName}"); + + var moduleConstructor = GetOrCreateModuleConstructor (member.GetModule (), out var modified); + var signature = DocumentationComments.GetSignature (member); + var attrib = abr.CreateDynamicDependencyAttribute (signature, member.DeclaringType); + modified |= abr.AddAttributeOnlyOnce (moduleConstructor, attrib); + return modified; + } + + string GetXmlDescriptionFilePath () + { + if (!string.IsNullOrEmpty (XmlDescriptionPath)) + return XmlDescriptionPath; + + return Path.Combine (Configuration.CacheDirectory, "apply-preserve-attribute.xml"); + } + + static string GetXmlSignature (MethodDefinition method) + { + var marker = method.DeclaringType.FullName + "::"; + var index = method.FullName.IndexOf (marker, System.StringComparison.Ordinal); + if (index < 0) + return method.FullName; + + return method.FullName.Substring (0, index) + method.FullName.Substring (index + marker.Length); + } + + XmlTypeDescription GetOrCreateXmlDescription (TypeDefinition type) + { + var assemblyName = type.Module.Assembly.Name.Name; + if (!xmlDescriptions.TryGetValue (assemblyName, out var types)) { + types = new Dictionary (System.StringComparer.Ordinal); + xmlDescriptions.Add (assemblyName, types); + } + + if (!types.TryGetValue (type.FullName, out var description)) { + description = new XmlTypeDescription (type); + types.Add (type.FullName, description); + } + + return description; + } + + void AddXmlDescription (TypeDefinition type, bool allMembers) + { + var description = GetOrCreateXmlDescription (type); + description.PreserveType = true; + if (allMembers) { + description.PreserveAllMembers = true; + return; + } + + if (type.IsEnum) { + description.PreserveFields = true; + return; + } + } + + void AddXmlDescription (TypeDefinition onType, MethodDefinition forMethod, bool conditional) + { + var description = GetOrCreateXmlDescription (onType); + if (!conditional) + description.PreserveType = true; + description.Methods [GetXmlSignature (forMethod)] = conditional; + } + + void AddUnconditionalXmlDescription (IMetadataTokenProvider provider) + { + switch (provider) { + case MethodDefinition method: + AddXmlDescription (method.DeclaringType, method, false); + break; + case FieldDefinition field: + var description = GetOrCreateXmlDescription (field.DeclaringType); + description.Fields [field.Name] = false; + description.PreserveType = true; + break; + } + } + + XElement CreateXmlTypeElement (XmlTypeDescription description) + { + var type = new XElement ("type", new XAttribute ("fullname", description.Type.FullName)); + + if (description.PreserveAllMembers) { + type.SetAttributeValue ("preserve", "all"); + return type; + } + + if (description.PreserveFields && description.Fields.Count == 0 && description.Methods.Count == 0) { + type.SetAttributeValue ("preserve", "fields"); + return type; + } + + if (!description.PreserveType) + type.SetAttributeValue ("required", "false"); + + type.SetAttributeValue ("preserve", "nothing"); + + foreach (var field in description.Fields.OrderBy (v => v.Key, System.StringComparer.Ordinal)) + type.Add (new XElement ("field", new XAttribute ("name", field.Key), new XAttribute ("required", field.Value ? "false" : "true"))); + + foreach (var method in description.Methods.OrderBy (v => v.Key, System.StringComparer.Ordinal)) + type.Add (new XElement ("method", new XAttribute ("signature", method.Key), new XAttribute ("required", method.Value ? "false" : "true"))); + + return type; + } + + void WriteXmlDescription () + { + var xmlPath = GetXmlDescriptionFilePath (); + var directory = Path.GetDirectoryName (xmlPath); + if (!string.IsNullOrEmpty (directory)) + Directory.CreateDirectory (directory); + + var document = new XDocument ( + new XElement ("linker", + xmlDescriptions + .OrderBy (v => v.Key, System.StringComparer.Ordinal) + .Select (assembly => new XElement ("assembly", + new XAttribute ("fullname", assembly.Key), + assembly.Value + .OrderBy (v => v.Key, System.StringComparer.Ordinal) + .Select (v => CreateXmlTypeElement (v.Value)))))); + document.Save (xmlPath); + + if (CreateXmlDescriptionFile) { + var items = new List (); + var item = new MSBuildItem (xmlPath); + items.Add (item); + Configuration.WriteOutputForMSBuild ("TrimmerRootDescriptor", items); + } + + // The current linker run still needs these roots immediately. Writing the TrimmerRootDescriptor item only + // makes the descriptor available to MSBuild after this step has already finished running. + var applyXmlStepType = Context.GetType ().Assembly.GetType ("Mono.Linker.Steps.ResolveFromXmlStep"); + if (applyXmlStepType is not null) { + var documentStream = File.OpenRead (xmlPath); // ResolveFromXmlStep will dispose the stream. + var applyXmlStep = (BaseStep) Activator.CreateInstance (applyXmlStepType, new object [] { documentStream, xmlPath })!; + applyXmlStep.Process (Context); + } else { + throw ErrorHelper.CreateError (99, $"Unable to find Mono.Linker.Steps.ResolveFromXmlStep to apply the generated XML description file {xmlPath}"); + } + } + } +} diff --git a/tools/dotnet-linker/CecilExtensions.cs b/tools/dotnet-linker/CecilExtensions.cs index 810cc0db91a5..705b5bcda241 100644 --- a/tools/dotnet-linker/CecilExtensions.cs +++ b/tools/dotnet-linker/CecilExtensions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text; using Mono.Cecil; using Mono.Cecil.Cil; @@ -161,5 +162,49 @@ public static TypeDefinition GetModuleType (this ModuleDefinition @module) return moduleType; } + public static string RenderAttribute (this CustomAttribute ca) + { + var render = new Func (v => { + if (v is string s) + return $"\"{s}\""; + else if (v is TypeReference tr) + return $"typeof ({tr.FullName})"; + else + return v?.ToString () ?? "null"; + }); + + var sb = new StringBuilder (); + sb.Append ("["); + sb.Append (ca.AttributeType.Name.EndsWith ("Attribute") ? ca.AttributeType.Name.Substring (0, ca.AttributeType.Name.Length - "Attribute".Length) : ca.AttributeType.Name); + if (ca.HasFields || ca.HasConstructorArguments || ca.HasProperties) { + sb.Append ("("); + var first = true; + foreach (var arg in ca.ConstructorArguments) { + if (!first) + sb.Append (", "); + first = false; + sb.Append (render (arg.Value)); + } + foreach (var prop in ca.Properties) { + if (!first) + sb.Append (", "); + first = false; + sb.Append (prop.Name); + sb.Append (" = "); + sb.Append (render (prop.Argument.Value)); + } + foreach (var field in ca.Fields) { + if (!first) + sb.Append (", "); + first = false; + sb.Append (field.Name); + sb.Append (" = "); + sb.Append (render (field.Argument.Value)); + } + sb.Append (")"); + } + sb.Append ("]"); + return sb.ToString (); + } } } diff --git a/tools/dotnet-linker/Steps/AssemblyModifierStep.cs b/tools/dotnet-linker/Steps/AssemblyModifierStep.cs index da3fcb5e5608..2e3e971d4d87 100644 --- a/tools/dotnet-linker/Steps/AssemblyModifierStep.cs +++ b/tools/dotnet-linker/Steps/AssemblyModifierStep.cs @@ -17,19 +17,25 @@ namespace Xamarin.Linker.Steps; public abstract class AssemblyModifierStep : ConfigurationAwareStep { private protected AppBundleRewriter abr => Configuration.AppBundleRewriter; - protected override void TryProcessAssembly (AssemblyDefinition assembly) + protected sealed override void TryProcessAssembly (AssemblyDefinition assembly) { var modified = false; abr.SetCurrentAssembly (assembly); - foreach (var type in assembly.MainModule.Types) - modified |= ProcessTypeImpl (type); - + modified |= ModifyAssembly (assembly); if (modified) abr.SaveCurrentAssembly (); abr.ClearCurrentAssembly (); } + protected virtual bool ModifyAssembly (AssemblyDefinition assembly) + { + var modified = false; + foreach (var type in assembly.MainModule.Types) + modified |= ProcessTypeImpl (type); + return modified; + } + protected virtual bool ProcessType (TypeDefinition type) { return false; diff --git a/tools/dotnet-linker/Steps/RemoveAttributesStep.cs b/tools/dotnet-linker/Steps/RemoveAttributesStep.cs index af9acee94086..7fa016caab70 100644 --- a/tools/dotnet-linker/Steps/RemoveAttributesStep.cs +++ b/tools/dotnet-linker/Steps/RemoveAttributesStep.cs @@ -35,6 +35,9 @@ bool IsRemovedAttribute (CustomAttribute attribute) { // this avoids calling FullName (which allocates a string) var attr_type = attribute.Constructor.DeclaringType; + if (attr_type.Name == "PreserveAttribute") + return true; + switch (attr_type.Namespace) { case Namespaces.ObjCRuntime: switch (attr_type.Name) { diff --git a/tools/dotnet-linker/Steps/SetBeforeFieldInitStep.cs b/tools/dotnet-linker/Steps/SetBeforeFieldInitStep.cs index 1ea1dcb0943b..22f70f051ef0 100644 --- a/tools/dotnet-linker/Steps/SetBeforeFieldInitStep.cs +++ b/tools/dotnet-linker/Steps/SetBeforeFieldInitStep.cs @@ -45,12 +45,6 @@ protected override void Process (TypeDefinition type) if (Configuration.DerivedLinkContext.App.Optimizations.RegisterProtocols != true) return; - if (Configuration.DerivedLinkContext.App.XamarinRuntime == Bundler.XamarinRuntime.NativeAOT) { - // We can't remove the static constructor in the trimmer if we're using NativeAOT, - // because NativeAOT needs it for its own trimming logic. - return; - } - if (!type.IsBeforeFieldInit && type.IsInterface && type.HasMethods) { var cctor = type.GetTypeConstructor (); if (cctor is not null && cctor.IsBindingImplOptimizableCode (LinkContext)) diff --git a/tools/dotnet-linker/dotnet-linker.csproj b/tools/dotnet-linker/dotnet-linker.csproj index 1cd515ca90b8..5b8b4c66d39e 100644 --- a/tools/dotnet-linker/dotnet-linker.csproj +++ b/tools/dotnet-linker/dotnet-linker.csproj @@ -163,9 +163,6 @@ external/src/ObjCRuntime/NativeNameAttribute.cs - - external/tools/linker/ApplyPreserveAttribute.cs - external/tools/linker/ExceptionalSubStep.cs diff --git a/tools/linker/ApplyPreserveAttribute.cs b/tools/linker/ApplyPreserveAttribute.cs deleted file mode 100644 index 9eea0bb1c5ca..000000000000 --- a/tools/linker/ApplyPreserveAttribute.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2011-2013 Xamarin Inc. All rights reserved. - -using System; -using System.Collections.Generic; - -using Mono.Cecil; -using Mono.Linker; -using Mono.Tuner; - -using Xamarin.Tuner; - -namespace Xamarin.Linker.Steps { - - public class ApplyPreserveAttribute : ApplyPreserveAttributeBase { - // We need to run the ApplyPreserveAttribute step even if we're only linking sdk assemblies, because even - // though we know that sdk assemblies will never have Preserve attributes, user assemblies may have - // [assembly: LinkSafe] attributes, which means we treat them as sdk assemblies and those may have - // Preserve attributes. - public override bool IsActiveFor (AssemblyDefinition assembly) - { - return Annotations.GetAction (assembly) == AssemblyAction.Link; - } - - protected override void Process (AssemblyDefinition assembly) - { - base.Process (assembly); - ProcessAssemblyAttributes (assembly); - } - - void ProcessAssemblyAttributes (AssemblyDefinition assembly) - { - if (!assembly.HasCustomAttributes) - return; - - foreach (var attribute in assembly.CustomAttributes) { - if (!attribute.Constructor.DeclaringType.Is (Namespaces.Foundation, "PreserveAttribute")) - continue; - - if (!attribute.HasConstructorArguments) - continue; - var tr = (attribute.ConstructorArguments [0].Value as TypeReference); - if (tr is null) - continue; - - // we do not call `this.ProcessType` since - // (a) we're potentially processing a different assembly and `is_active` represent the current one - // (b) it will try to fetch the [Preserve] attribute on the type (and it's not there) as `base` would - var type = tr.Resolve (); - - PreserveType (type, attribute); - } - } - - protected override bool IsPreservedAttribute (ICustomAttributeProvider provider, CustomAttribute attribute, out bool removeAttribute) - { - removeAttribute = false; - TypeReference type = attribute.Constructor.DeclaringType; - - if (type.Name == "PreserveAttribute") { - // there's no need to keep the [Preserve] attribute in the assembly once it was processed - removeAttribute = true; - return true; - } - return false; - } - } -} From a573a2e3882af03157ad600461e65baea9ba802a Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 14 Apr 2026 10:15:16 +0200 Subject: [PATCH 2/2] Update sizes. --- .../UnitTests/expected/MacCatalyst-NativeAOT-size.txt | 6 +++--- tests/dotnet/UnitTests/expected/MacOSX-NativeAOT-size.txt | 6 +++--- tests/dotnet/UnitTests/expected/TVOS-NativeAOT-size.txt | 6 +++--- tests/dotnet/UnitTests/expected/iOS-NativeAOT-size.txt | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/dotnet/UnitTests/expected/MacCatalyst-NativeAOT-size.txt b/tests/dotnet/UnitTests/expected/MacCatalyst-NativeAOT-size.txt index 6f58a1daf7cb..093edf407164 100644 --- a/tests/dotnet/UnitTests/expected/MacCatalyst-NativeAOT-size.txt +++ b/tests/dotnet/UnitTests/expected/MacCatalyst-NativeAOT-size.txt @@ -1,7 +1,7 @@ -AppBundleSize: 2,450,516 bytes (2,393.1 KB = 2.3 MB) +AppBundleSize: 2,599,110 bytes (2,538.2 KB = 2.5 MB) # The following list of files and their sizes is just informational / for review, and isn't used in the test: Contents/_CodeSignature/CodeResources: 2,358 bytes (2.3 KB = 0.0 MB) -Contents/Info.plist: 1,094 bytes (1.1 KB = 0.0 MB) -Contents/MacOS/SizeTestApp: 2,445,248 bytes (2,387.9 KB = 2.3 MB) +Contents/Info.plist: 1,128 bytes (1.1 KB = 0.0 MB) +Contents/MacOS/SizeTestApp: 2,593,808 bytes (2,533.0 KB = 2.5 MB) Contents/MonoBundle/runtimeconfig.bin: 1,808 bytes (1.8 KB = 0.0 MB) Contents/PkgInfo: 8 bytes (0.0 KB = 0.0 MB) diff --git a/tests/dotnet/UnitTests/expected/MacOSX-NativeAOT-size.txt b/tests/dotnet/UnitTests/expected/MacOSX-NativeAOT-size.txt index 0c2f4851f900..ce539ee72a41 100644 --- a/tests/dotnet/UnitTests/expected/MacOSX-NativeAOT-size.txt +++ b/tests/dotnet/UnitTests/expected/MacOSX-NativeAOT-size.txt @@ -1,8 +1,8 @@ -AppBundleSize: 8,094,981 bytes (7,905.3 KB = 7.7 MB) +AppBundleSize: 8,440,007 bytes (8,242.2 KB = 8.0 MB) # The following list of files and their sizes is just informational / for review, and isn't used in the test: Contents/_CodeSignature/CodeResources: 3,489 bytes (3.4 KB = 0.0 MB) -Contents/Info.plist: 725 bytes (0.7 KB = 0.0 MB) -Contents/MacOS/SizeTestApp: 5,101,408 bytes (4,981.8 KB = 4.9 MB) +Contents/Info.plist: 759 bytes (0.7 KB = 0.0 MB) +Contents/MacOS/SizeTestApp: 5,446,400 bytes (5,318.8 KB = 5.2 MB) Contents/MonoBundle/libSystem.Globalization.Native.dylib: 252,176 bytes (246.3 KB = 0.2 MB) Contents/MonoBundle/libSystem.IO.Compression.Native.dylib: 2,005,440 bytes (1,958.4 KB = 1.9 MB) Contents/MonoBundle/libSystem.Native.dylib: 292,176 bytes (285.3 KB = 0.3 MB) diff --git a/tests/dotnet/UnitTests/expected/TVOS-NativeAOT-size.txt b/tests/dotnet/UnitTests/expected/TVOS-NativeAOT-size.txt index d931959dfc15..a88b9788bf10 100644 --- a/tests/dotnet/UnitTests/expected/TVOS-NativeAOT-size.txt +++ b/tests/dotnet/UnitTests/expected/TVOS-NativeAOT-size.txt @@ -1,8 +1,8 @@ -AppBundleSize: 2,450,669 bytes (2,393.2 KB = 2.3 MB) +AppBundleSize: 2,616,495 bytes (2,555.2 KB = 2.5 MB) # The following list of files and their sizes is just informational / for review, and isn't used in the test: _CodeSignature/CodeResources: 2,589 bytes (2.5 KB = 0.0 MB) archived-expanded-entitlements.xcent: 384 bytes (0.4 KB = 0.0 MB) -Info.plist: 1,112 bytes (1.1 KB = 0.0 MB) +Info.plist: 1,146 bytes (1.1 KB = 0.0 MB) PkgInfo: 8 bytes (0.0 KB = 0.0 MB) runtimeconfig.bin: 1,808 bytes (1.8 KB = 0.0 MB) -SizeTestApp: 2,444,768 bytes (2,387.5 KB = 2.3 MB) +SizeTestApp: 2,610,560 bytes (2,549.4 KB = 2.5 MB) diff --git a/tests/dotnet/UnitTests/expected/iOS-NativeAOT-size.txt b/tests/dotnet/UnitTests/expected/iOS-NativeAOT-size.txt index 2b692c1f301e..238ecfc35830 100644 --- a/tests/dotnet/UnitTests/expected/iOS-NativeAOT-size.txt +++ b/tests/dotnet/UnitTests/expected/iOS-NativeAOT-size.txt @@ -1,8 +1,8 @@ -AppBundleSize: 2,437,189 bytes (2,380.1 KB = 2.3 MB) +AppBundleSize: 2,602,055 bytes (2,541.1 KB = 2.5 MB) # The following list of files and their sizes is just informational / for review, and isn't used in the test: _CodeSignature/CodeResources: 2,589 bytes (2.5 KB = 0.0 MB) archived-expanded-entitlements.xcent: 384 bytes (0.4 KB = 0.0 MB) -Info.plist: 1,136 bytes (1.1 KB = 0.0 MB) +Info.plist: 1,170 bytes (1.1 KB = 0.0 MB) PkgInfo: 8 bytes (0.0 KB = 0.0 MB) runtimeconfig.bin: 1,808 bytes (1.8 KB = 0.0 MB) -SizeTestApp: 2,431,264 bytes (2,374.3 KB = 2.3 MB) +SizeTestApp: 2,596,096 bytes (2,535.2 KB = 2.5 MB)