From 837e8ce6501c25a6fa4596daa3c4395845a27730 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 13:54:02 +0200 Subject: [PATCH 1/6] Compare trimmable typemap APK contents Add a Release CoreCLR HelloWorld comparison test that builds llvm-ir and trimmable typemap APKs, prints managed and dex diagnostics, and asserts the trimmable typemap does not retain extra typemap-eligible managed or Java entries. Pass all generated typemap assemblies to ILLink as typemap entry assemblies and mark them trimmable so conditional typemap entries are honored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 9 +- .../TrimmableTypeMapBuildTests.cs | 727 ++++++++++++++++++ 2 files changed, 735 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index d0ca9742e30..c27922166c1 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -12,6 +12,7 @@ %(Filename)%(Extension) + true @@ -20,9 +21,15 @@ + + <_TrimmableTypeMapEntryAssemblies Include="$(_TypeMapOutputDirectory)*.dll" /> + - <_ExtraTrimmerArgs>--typemap-entry-assembly $(_TypeMapAssemblyName) $(_ExtraTrimmerArgs) + <_ExtraTrimmerArgs>@(_TrimmableTypeMapEntryAssemblies->'--typemap-entry-assembly %(Filename)', ' ') $(_ExtraTrimmerArgs) + + <_TrimmableTypeMapEntryAssemblies Remove="@(_TrimmableTypeMapEntryAssemblies)" /> + + + + <_LinkedAssemblyForProguard Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 88e4683d37d..7d03164da34 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -4,6 +4,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 068cea291f5..65a39960a18 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -191,6 +191,7 @@ ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) proj.SetRuntime (AndroidRuntime.CoreCLR); proj.SetProperty ("AndroidSupportedAbis", "arm64-v8a"); proj.SetProperty ("AndroidPackageFormat", "apk"); + proj.SetProperty (KnownProperties.AndroidLinkTool, "r8"); proj.SetProperty ("TrimMode", "full"); proj.SetProperty ("_AndroidTypeMapImplementation", typemapImplementation); From 77ee9e0cb3a2b298839c0daf745b2b56cab7df2e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 14:08:13 +0200 Subject: [PATCH 3/6] Pass per-assembly typemap entries to ILLink Limit --typemap-entry-assembly arguments to generated per-assembly *.TypeMap.dll assemblies that contain TypeMapAttribute entries, instead of treating the root typemap loader assembly as an entry assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 43406ea56be..4d3b1470ee2 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -26,7 +26,7 @@ BeforeTargets="PrepareForILLink;_RunILLink" DependsOnTargets="_GenerateTrimmableTypeMap"> - <_TrimmableTypeMapEntryAssemblies Include="$(_TypeMapOutputDirectory)*.dll" /> + <_TrimmableTypeMapEntryAssemblies Include="$(_TypeMapOutputDirectory)*.TypeMap.dll" /> <_ExtraTrimmerArgs>@(_TrimmableTypeMapEntryAssemblies->'--typemap-entry-assembly %(Filename)', ' ') $(_ExtraTrimmerArgs) From 7a654675370199f9a1445ac8bea9663fb9106e67 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 16:23:53 +0200 Subject: [PATCH 4/6] Refine trimmable typemap framework roots Keep SDK framework ACWs conditional unless they are explicitly rooted, and pass framework assembly names through trimmable typemap generation. This allows Mono.Android implementor entries to be trimmed while preserving app ACWs and scanner-rooted components. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 2 +- .../Scanner/JavaPeerInfo.cs | 7 + .../TrimmableTypeMapGenerator.cs | 10 ++ ...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 + .../Tasks/GenerateTrimmableTypeMap.cs | 22 +++- .../TrimmableTypeMapBuildTests.cs | 122 +++++++++++++++++- .../Generator/TypeMapModelBuilderTests.cs | 26 ++++ 7 files changed, 183 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 25e9db22008..a1805b8253b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -226,7 +226,7 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) // User-defined ACW types (not MCW bindings, not interfaces) are unconditional // because Android can instantiate them from Java at any time. - if (!peer.DoNotGenerateAcw && !peer.IsInterface) { + if (!peer.IsFrameworkAssembly && !peer.DoNotGenerateAcw && !peer.IsInterface) { return true; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 97606a4b5af..6b30ec1d3ef 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -43,6 +43,13 @@ public sealed record JavaPeerInfo /// public required string AssemblyName { get; init; } + /// + /// True when the type belongs to a framework assembly supplied by the Android SDK. + /// Framework ACWs are generated by the SDK and can be trimmed like bindings unless + /// another rule explicitly roots them. + /// + public bool IsFrameworkAssembly { get; set; } + /// /// JNI name of the base Java type, e.g., "android/app/Activity" for a type /// that extends Activity. Null for java/lang/Object or types without a Java base. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 4851fde0857..35c55c6834f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -48,6 +48,7 @@ public TrimmableTypeMapResult Execute ( logger.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); } + MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); PropagateDeferredRegistrationToBaseClasses (allPeers); @@ -188,6 +189,15 @@ List GenerateTypeMapAssemblies (List allPeers, return generatedAssemblies; } + static void MarkFrameworkAssemblyPeers (List allPeers, HashSet frameworkAssemblyNames) + { + foreach (var peer in allPeers) { + if (frameworkAssemblyNames.Contains (peer.AssemblyName)) { + peer.IsFrameworkAssembly = true; + } + } + } + /// /// Groups peers by assembly, merging cross-assembly aliases into a single group. /// When the same JNI name appears in multiple assemblies (e.g. Java.Lang.Object diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 7d03164da34..b95d4212a65 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -79,6 +79,7 @@ @@ -43,6 +49,7 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => [Required] public ITaskItem [] ResolvedAssemblies { get; set; } = []; + public string [] FrameworkAssemblyNames { get; set; } = []; [Required] public string OutputDirectory { get; set; } = ""; [Required] @@ -91,8 +98,13 @@ public override bool RunTask () { var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion); var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); - // TODO(#10792): populate with framework assembly names to skip JCW generation for pre-compiled framework types - var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); + var frameworkAssemblyPaths = new HashSet (ResolvedAssemblies + .Where (i => string.Equals (i.GetMetadata ("FrameworkAssembly"), "true", StringComparison.OrdinalIgnoreCase)) + .Select (i => i.ItemSpec), StringComparer.OrdinalIgnoreCase); + var frameworkAssemblyNames = new HashSet (DefaultFrameworkAssemblyNames, StringComparer.OrdinalIgnoreCase); + foreach (var assemblyName in FrameworkAssemblyNames) { + frameworkAssemblyNames.Add (assemblyName); + } Directory.CreateDirectory (OutputDirectory); Directory.CreateDirectory (JavaSourceOutputDirectory); @@ -105,7 +117,11 @@ public override bool RunTask () var peReader = new PEReader (File.OpenRead (path)); peReaders.Add (peReader); var mdReader = peReader.GetMetadataReader (); - assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader)); + var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); + assemblies.Add ((assemblyName, peReader)); + if (frameworkAssemblyPaths.Contains (path)) { + frameworkAssemblyNames.Add (assemblyName); + } } ManifestConfig? manifestConfig = null; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 65a39960a18..98a7f66c6b0 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -207,8 +207,10 @@ ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) var apkPath = Directory.GetFiles (apkDirectory, "*-Signed.apk", SearchOption.AllDirectories).Single (); var acwMapPath = builder.Output.GetIntermediaryPath ("acw-map.txt"); var javaSourceDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android", "src")); + var typeMapDirectory = builder.Output.GetIntermediaryPath ("typemap"); + var linkedAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked")); - var profile = ReadApkProfile (typemapImplementation, apkPath, acwMapPath, javaSourceDirectory); + var profile = ReadApkProfile (typemapImplementation, apkPath, acwMapPath, javaSourceDirectory, typeMapDirectory, linkedAssemblyDirectory); if (typemapImplementation == "trimmable") { Assert.IsTrue (profile.ManagedAssemblyNames.Contains ("_Microsoft.Android.TypeMaps.dll"), "trimmable build should package the root managed typemap assembly."); } else { @@ -217,7 +219,7 @@ ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) return profile; } - ApkComparisonProfile ReadApkProfile (string name, string apkPath, string acwMapPath, string javaSourceDirectory) + ApkComparisonProfile ReadApkProfile (string name, string apkPath, string acwMapPath, string javaSourceDirectory, string typeMapDirectory, string linkedAssemblyDirectory) { var profile = new ApkComparisonProfile { Name = name, @@ -227,6 +229,8 @@ ApkComparisonProfile ReadApkProfile (string name, string apkPath, string acwMapP LoadAcwMap (acwMapPath, profile); ReadGeneratedJavaProfile (javaSourceDirectory, profile); + ReadTypeMapAssemblyProfile (profile, "generated", typeMapDirectory); + ReadTypeMapAssemblyProfile (profile, "linked", linkedAssemblyDirectory); ReadAssemblyStoreProfile (profile); ReadDexProfile (profile); @@ -328,6 +332,9 @@ void ReadAssemblyStoreProfile (ApkComparisonProfile profile) } using (assembly) { profile.ManagedAssemblyCount++; + if (item.Name.EndsWith (".TypeMap.dll", StringComparison.Ordinal) || item.Name == "_Microsoft.Android.TypeMaps.dll") { + ReadTypeMapAssemblyProfile (profile, "packaged", assembly, item.Name); + } foreach (var type in assembly.Modules.SelectMany (m => m.Types).SelectMany (FlattenType)) { if (IsTypemapHelperManagedType (type.FullName)) { continue; @@ -351,6 +358,66 @@ void ReadAssemblyStoreProfile (ApkComparisonProfile profile) } } + void ReadTypeMapAssemblyProfile (ApkComparisonProfile profile, string stage, string directory) + { + if (!Directory.Exists (directory)) { + return; + } + + foreach (var file in Directory.EnumerateFiles (directory, "*.dll", SearchOption.TopDirectoryOnly).Where (IsTypeMapAssemblyPath)) { + using var assembly = AssemblyDefinition.ReadAssembly (file); + ReadTypeMapAssemblyProfile (profile, stage, assembly, Path.GetFileName (file)); + } + } + + bool IsTypeMapAssemblyPath (string file) + { + var name = Path.GetFileName (file); + return name.EndsWith (".TypeMap.dll", StringComparison.Ordinal) || name == "_Microsoft.Android.TypeMaps.dll"; + } + + void ReadTypeMapAssemblyProfile (ApkComparisonProfile profile, string stage, AssemblyDefinition assembly, string assemblyName) + { + var metrics = new TypeMapAssemblyMetrics { + Stage = stage, + AssemblyName = assemblyName, + }; + + foreach (var attribute in assembly.CustomAttributes) { + var attributeName = attribute.AttributeType.FullName; + if (attributeName.StartsWith ("System.Runtime.InteropServices.TypeMapAttribute`1", StringComparison.Ordinal)) { + ReadTypeMapAttribute (attribute, metrics); + } else if (attributeName.StartsWith ("System.Runtime.InteropServices.TypeMapAssociationAttribute", StringComparison.Ordinal)) { + metrics.AssociationAttributeCount++; + } else if (attributeName.StartsWith ("System.Runtime.InteropServices.TypeMapAssemblyTargetAttribute`1", StringComparison.Ordinal)) { + metrics.AssemblyTargetAttributeCount++; + } + } + + if (metrics.TypeMapAttributeCount != 0 || metrics.AssociationAttributeCount != 0 || metrics.AssemblyTargetAttributeCount != 0) { + profile.TypeMapAssemblies.Add (metrics); + } + } + + void ReadTypeMapAttribute (CustomAttribute attribute, TypeMapAssemblyMetrics metrics) + { + metrics.TypeMapAttributeCount++; + if (attribute.ConstructorArguments.Count == 2) { + metrics.UnconditionalTypeMapAttributeCount++; + } else if (attribute.ConstructorArguments.Count == 3) { + metrics.ConditionalTypeMapAttributeCount++; + } + + var jniName = attribute.ConstructorArguments.Count > 0 ? attribute.ConstructorArguments [0].Value as string : null; + var proxyType = attribute.ConstructorArguments.Count > 1 ? attribute.ConstructorArguments [1].Value as string : null; + var targetType = attribute.ConstructorArguments.Count > 2 ? attribute.ConstructorArguments [2].Value as string : null; + var key = $"{jniName}\t{proxyType}\t{targetType}"; + metrics.TypeMapAttributeKeys.Add (key); + if (jniName != null) { + metrics.IncrementPrefixBucket (jniName); + } + } + bool IsManagedTypemapEligible (TypeDefinition type, ApkComparisonProfile profile) { if (profile.CandidateManagedTypes.Contains (type.FullName)) { @@ -546,7 +613,7 @@ void WriteComparisonTable (ApkComparisonProfile llvmIr, ApkComparisonProfile tri TestContext.Out.WriteLine ($"| APK size | {FormatNumber (llvmIr.ApkSize)} | {FormatNumber (trimmable.ApkSize)} |"); TestContext.Out.WriteLine ($"| Assembly-store payload | {FormatNumber (llvmIr.AssemblyStoreSize)} | {FormatNumber (trimmable.AssemblyStoreSize)} |"); TestContext.Out.WriteLine ($"| classes*.dex | {FormatNumber (llvmIr.DexSize)} | {FormatNumber (trimmable.DexSize)} |"); - TestContext.Out.WriteLine ($"| Filtered managed types / methods | {FormatNumber (llvmIr.FilteredManagedTypeCount)} / {FormatNumber (llvmIr.FilteredManagedMethodCount)} | {FormatNumber (trimmable.FilteredManagedTypeCount)} / {FormatNumber (trimmable.FilteredManagedMethodCount)} |"); + TestContext.Out.WriteLine ($"| Registered managed types / methods | {FormatNumber (llvmIr.FilteredManagedTypeCount)} / {FormatNumber (llvmIr.FilteredManagedMethodCount)} | {FormatNumber (trimmable.FilteredManagedTypeCount)} / {FormatNumber (trimmable.FilteredManagedMethodCount)} |"); TestContext.Out.WriteLine ($"| Managed diff | {FormatNumber (managedDiff.LlvmIrOnly.Length)} llvm-ir-only | {FormatNumber (managedDiff.TrimmableOnly.Length)} trimmable-only |"); TestContext.Out.WriteLine ($"| Java diff | {FormatNumber (javaDiff.LlvmIrOnly.Length)} llvm-ir-only | {FormatNumber (javaDiff.TrimmableOnly.Length)} trimmable-only |"); } @@ -562,6 +629,9 @@ void WriteProfile (ApkComparisonProfile profile) TestContext.Out.WriteLine ($"{profile.Name}: generated Java sources={profile.GeneratedJavaSourceCount}, __md_methods files={profile.GeneratedJavaWithMdMethodsCount}, Runtime.register files={profile.GeneratedJavaWithRuntimeRegisterCount}, Runtime.registerNatives files={profile.GeneratedJavaWithRegisterNativesCount}"); TestContext.Out.WriteLine ($"{profile.Name}: assembly stores: {String.Join ("; ", profile.AssemblyStores)}"); TestContext.Out.WriteLine ($"{profile.Name}: dex files: {String.Join ("; ", profile.DexFiles)}"); + foreach (var metrics in profile.TypeMapAssemblies) { + TestContext.Out.WriteLine ($"{profile.Name}: typemap {metrics.Stage}/{metrics.AssemblyName}: typemap={metrics.TypeMapAttributeCount}, unique={metrics.UniqueTypeMapAttributeCount}, duplicates={metrics.DuplicateTypeMapAttributeCount}, unconditional={metrics.UnconditionalTypeMapAttributeCount}, conditional={metrics.ConditionalTypeMapAttributeCount}, associations={metrics.AssociationAttributeCount}, assembly-targets={metrics.AssemblyTargetAttributeCount}, prefixes={metrics.FormatPrefixBuckets ()}"); + } } void WriteSize (string label, long llvmIr, long trimmable) @@ -636,6 +706,52 @@ class ApkComparisonProfile public readonly HashSet ManagedTypemapEntries = new HashSet (StringComparer.Ordinal); public readonly HashSet JavaTypemapEntries = new HashSet (StringComparer.Ordinal); public readonly HashSet JavaClassNames = new HashSet (StringComparer.Ordinal); + public readonly List TypeMapAssemblies = new List (); + } + + class TypeMapAssemblyMetrics + { + public string Stage; + public string AssemblyName; + public int TypeMapAttributeCount; + public int UnconditionalTypeMapAttributeCount; + public int ConditionalTypeMapAttributeCount; + public int AssociationAttributeCount; + public int AssemblyTargetAttributeCount; + public readonly List TypeMapAttributeKeys = new List (); + readonly SortedDictionary prefixBuckets = new SortedDictionary (StringComparer.Ordinal); + + public int UniqueTypeMapAttributeCount => TypeMapAttributeKeys.Distinct (StringComparer.Ordinal).Count (); + public int DuplicateTypeMapAttributeCount => TypeMapAttributeCount - UniqueTypeMapAttributeCount; + + public void IncrementPrefixBucket (string jniName) + { + var bucket = GetPrefixBucket (jniName); + prefixBuckets.TryGetValue (bucket, out int count); + prefixBuckets [bucket] = count + 1; + } + + public string FormatPrefixBuckets () + { + return String.Join (", ", prefixBuckets.Select (p => $"{p.Key}={p.Value}")); + } + + static string GetPrefixBucket (string jniName) + { + if (jniName.StartsWith ("mono/android/", StringComparison.Ordinal)) { + return "mono/android"; + } + if (jniName.StartsWith ("android/", StringComparison.Ordinal)) { + return "android"; + } + if (jniName.StartsWith ("java/", StringComparison.Ordinal)) { + return "java"; + } + if (jniName.StartsWith ("com/xamarin/", StringComparison.Ordinal)) { + return "app"; + } + return "other"; + } } class EntryDiff diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 7a34fc345fe..9fcf59fe774 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -168,6 +168,32 @@ public void Build_UserAcwType_IsUnconditional () Assert.Null (mainEntry.TargetTypeReference); } + [Fact] + public void Build_FrameworkAcwType_IsTrimmable () + { + var peer = MakeAcwPeer ("mono/android/view/View_OnClickListenerImplementor", "Android.Views.View+IOnClickListenerImplementor", "Mono.Android") with { + IsFrameworkAssembly = true, + }; + var model = BuildModel (new [] { peer }); + + var entry = model.Entries.First (e => e.JniName == "mono/android/view/View_OnClickListenerImplementor"); + Assert.False (entry.IsUnconditional); + Assert.Equal ("Android.Views.View+IOnClickListenerImplementor, Mono.Android", entry.TargetTypeReference); + } + + [Fact] + public void Build_FrameworkAcwType_MarkedUnconditional_IsUnconditional () + { + var peer = MakeAcwPeer ("mono/android/app/ApplicationRegistration", "Android.App.ApplicationRegistration", "Mono.Android") with { + IsFrameworkAssembly = true, + IsUnconditional = true, + }; + var model = BuildModel (new [] { peer }); + + Assert.True (model.Entries [0].IsUnconditional); + Assert.Null (model.Entries [0].TargetTypeReference); + } + [Fact] public void Build_McwBinding_IsTrimmable () { From 44d8c10d429dfc2e691f06bd12f74d4675d9aea5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 20:45:59 +0200 Subject: [PATCH 5/6] Reduce trimmable typemap R8 keep rules Generate R8 keep rules for trimmable typemap builds from linked assemblies instead of the pre-link acw-map, and use a minimal Xamarin runtime keep list for this path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 20 +++++------ src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 34 +++++++++++++++++-- .../Xamarin.Android.D8.targets | 4 +++ 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 4d3b1470ee2..714ff9ae8aa 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -7,6 +7,8 @@ <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink + <_R8GenerateApplicationProguardConfigurationFromAcwMap>false + <_R8UseMinimalXamarinProguardConfiguration>true @@ -36,22 +38,20 @@ - - - <_LinkedAssemblyForProguard Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> - - - + + <_LinkedAssemblyForProguard Include="$(IntermediateOutputPath)*/linked/*.dll" /> + + + <_LinkedAssemblyForProguard Remove="@(_LinkedAssemblyForProguard)" /> + <_R8EnableShrinking Condition=" '$(AndroidLinkTool)' == 'r8' ">True <_R8EnableShrinking Condition=" '$(_R8EnableShrinking)' == '' ">False + <_R8GenerateApplicationProguardConfigurationFromAcwMap Condition=" '$(_R8GenerateApplicationProguardConfigurationFromAcwMap)' == '' ">True + <_R8UseMinimalXamarinProguardConfiguration Condition=" '$(_R8UseMinimalXamarinProguardConfiguration)' == '' ">False Date: Thu, 14 May 2026 21:24:34 +0200 Subject: [PATCH 6/6] Revert "Reduce trimmable typemap R8 keep rules" This reverts commit 44d8c10d429dfc2e691f06bd12f74d4675d9aea5. --- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 20 +++++------ src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 34 ++----------------- .../Xamarin.Android.D8.targets | 4 --- 3 files changed, 12 insertions(+), 46 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 714ff9ae8aa..4d3b1470ee2 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -7,8 +7,6 @@ <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink - <_R8GenerateApplicationProguardConfigurationFromAcwMap>false - <_R8UseMinimalXamarinProguardConfiguration>true @@ -38,20 +36,22 @@ + + + <_LinkedAssemblyForProguard Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + + + - - <_LinkedAssemblyForProguard Include="$(IntermediateOutputPath)*/linked/*.dll" /> - - - <_LinkedAssemblyForProguard Remove="@(_LinkedAssemblyForProguard)" /> - <_R8EnableShrinking Condition=" '$(AndroidLinkTool)' == 'r8' ">True <_R8EnableShrinking Condition=" '$(_R8EnableShrinking)' == '' ">False - <_R8GenerateApplicationProguardConfigurationFromAcwMap Condition=" '$(_R8GenerateApplicationProguardConfigurationFromAcwMap)' == '' ">True - <_R8UseMinimalXamarinProguardConfiguration Condition=" '$(_R8UseMinimalXamarinProguardConfiguration)' == '' ">False