From 9686fb6b5d0b569bb99d67b15be59a4768830162 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 13:54:02 +0200 Subject: [PATCH 1/9] 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, 734 insertions(+), 2 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 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 b040052577f..ea8b052f828 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 2a1fb3f718c..446eb045193 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 @@ -433,6 +433,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 345390f1b95e2937e4050f2d916dd5c176aecf1d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 14:08:13 +0200 Subject: [PATCH 3/9] 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 c442af3fcf8168e3954de868ada34a721e127e6b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 16:23:53 +0200 Subject: [PATCH 4/9] 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> --- .../Scanner/JavaPeerInfo.cs | 6 +- .../TrimmableTypeMapGenerator.cs | 10 ++ ...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 + .../Tasks/GenerateTrimmableTypeMap.cs | 12 +- .../TrimmableTypeMapBuildTests.cs | 122 +++++++++++++++++- .../Generator/TypeMapModelBuilderTests.cs | 26 ++++ 6 files changed, 171 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 2005799d1c3..62a9f55128a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -44,9 +44,11 @@ public sealed record JavaPeerInfo public required string AssemblyName { get; init; } /// - /// True when the type belongs to a framework assembly. + /// 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; init; } + public bool IsFrameworkAssembly { get; set; } /// /// True when per-rank array typemap entries should be generated for this peer. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 5a391f520b6..652c4fb3a5b 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); @@ -212,6 +213,15 @@ List GenerateTypeMapAssemblies ( 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 ea8b052f828..69da896b24f 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 @@ -85,6 +85,7 @@ @@ -46,6 +52,7 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN [Required] public ITaskItem [] ResolvedAssemblies { get; set; } = []; public ITaskItem [] ResolvedFrameworkAssemblies { get; set; } = []; + public string [] FrameworkAssemblyNames { get; set; } = []; [Required] public string OutputDirectory { get; set; } = ""; [Required] @@ -105,7 +112,10 @@ public override bool RunTask () Path: g.Key, IsFrameworkAssembly: frameworkAssemblyPaths.Contains (g.Key) || g.Any (IsFrameworkAssemblyItem))) .ToList (); - var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); + var frameworkAssemblyNames = new HashSet (DefaultFrameworkAssemblyNames, StringComparer.OrdinalIgnoreCase); + foreach (var assemblyName in FrameworkAssemblyNames) { + frameworkAssemblyNames.Add (assemblyName); + } Directory.CreateDirectory (OutputDirectory); Directory.CreateDirectory (JavaSourceOutputDirectory); 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 446eb045193..4e99e9932be 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 @@ -449,8 +449,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 { @@ -459,7 +461,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, @@ -469,6 +471,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); @@ -570,6 +574,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; @@ -593,6 +600,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)) { @@ -788,7 +855,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 |"); } @@ -804,6 +871,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) @@ -878,6 +948,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 06498daa478..33cdea9a84b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -216,6 +216,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 a8af5b16859bb0cfe0d8de325da4afbaf5a3a39a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 20:45:59 +0200 Subject: [PATCH 5/9] 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/9] 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 Date: Wed, 20 May 2026 12:40:46 +0200 Subject: [PATCH 7/9] Fix post-trim trimmable typemap R8 inputs Scan linked assemblies after ILLink to filter generated Java wrappers and acw-map inputs before R8, while packaging only linked typemap assemblies for single-RID builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 11 +- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 45 +++++++- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 22 +++- .../Tasks/GenerateTrimmableTypeMap.cs | 51 ++++++++- .../TrimmableTypeMapBuildTests.cs | 107 ++++++++++++++++++ 5 files changed, 220 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 652c4fb3a5b..95d0446b205 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -34,7 +34,8 @@ public TrimmableTypeMapResult Execute ( ManifestConfig? manifestConfig = null, XDocument? manifestTemplate = null, string? packageNamingPolicy = null, - int maxArrayRank = 0) + int maxArrayRank = 0, + bool generateTypeMapAssemblies = true) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -54,11 +55,9 @@ public TrimmableTypeMapResult Execute ( PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies ( - allPeers, - systemRuntimeVersion, - useSharedTypemapUniverse, - maxArrayRank); + var generatedAssemblies = generateTypeMapAssemblies + ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank) + : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); 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..e3a43a9d87f 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 @@ -54,6 +54,46 @@ OutputFile="$(_ProguardProjectConfiguration)" /> + + + <_PostTrimTrimmableTypeMapInputAssemblies Include="@(ResolvedFileToPublish)" + Condition=" '%(Extension)' == '.dll' " /> + + + + + + + + + + + + + + + <_PostTrimTrimmableTypeMapInputAssemblies Remove="@(_PostTrimTrimmableTypeMapInputAssemblies)" /> + <_PostTrimGeneratedJavaFiles Remove="@(_PostTrimGeneratedJavaFiles)" /> + + + @@ -76,9 +116,10 @@ - + - <_LinkedTypeMapDlls Include="$(IntermediateOutputPath)%(_TrimmableTypeMapAbi.RuntimeIdentifier)/linked/_*.TypeMap.dll;$(IntermediateOutputPath)%(_TrimmableTypeMapAbi.RuntimeIdentifier)/linked/_Microsoft.Android.TypeMap*.dll"> + <_LinkedTypeMapDlls Include="$(IntermediateOutputPath)linked/_*.TypeMap.dll;$(IntermediateOutputPath)linked/_Microsoft.Android.TypeMap*.dll;$(IntermediateOutputPath)%(_TrimmableTypeMapAbi.RuntimeIdentifier)/linked/_*.TypeMap.dll;$(IntermediateOutputPath)%(_TrimmableTypeMapAbi.RuntimeIdentifier)/linked/_Microsoft.Android.TypeMap*.dll"> %(_TrimmableTypeMapAbi.Identity) %(_TrimmableTypeMapAbi.RuntimeIdentifier) %(_TrimmableTypeMapAbi.Identity)/%(_LinkedTypeMapDlls.Filename)%(_LinkedTypeMapDlls.Extension) 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 69da896b24f..1955c4c2580 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 @@ -24,6 +24,13 @@ <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java <_TypeMapAssembliesListFile>$(_TypeMapOutputDirectory)typemap-assemblies.txt + <_PostTrimTypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/linked-java + <_TypeMapJavaStubsSourceDirectory Condition=" '$(_TypeMapJavaStubsSourceDirectory)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTypeMapJavaOutputDirectory) + <_TypeMapJavaStubsSourceDirectory Condition=" '$(_TypeMapJavaStubsSourceDirectory)' == '' ">$(_TypeMapJavaOutputDirectory) + <_PostTrimTrimmableTypeMapJavaStamp>$(_TypeMapBaseOutputDir)stamp/_GeneratePostTrimTrimmableTypeMapJavaSources.stamp + <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTrimmableTypeMapJavaStamp) + <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' ">$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll + @@ -207,16 +214,23 @@ --> - <_TypeMapJavaFiles Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> + <_TypeMapJavaFiles Include="$(_TypeMapJavaStubsSourceDirectory)/**/*.java" /> + <_TypeMapJavaDestinationFiles Include="@(_TypeMapJavaFiles->'$(IntermediateOutputPath)android/src/%(RecursiveDir)%(Filename)%(Extension)')" /> + <_ExistingTypeMapJavaDestinationFiles Include="$(IntermediateOutputPath)android/src/mono/**/*.java;$(IntermediateOutputPath)android/src/android/runtime/**/*.java" /> + <_StaleTypeMapJavaDestinationFiles Include="@(_ExistingTypeMapJavaDestinationFiles)" Exclude="@(_TypeMapJavaDestinationFiles)" /> - + + - + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 6184f36b467..2dd34547a16 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -57,6 +57,7 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN public string OutputDirectory { get; set; } = ""; [Required] public string JavaSourceOutputDirectory { get; set; } = ""; + public string? JavaSourceInputDirectory { get; set; } [Required] public string TargetFrameworkVersion { get; set; } = ""; @@ -92,6 +93,8 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN public string? ManifestPlaceholders { get; set; } public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } + public bool GenerateTypeMapAssemblies { get; set; } = true; + public bool CleanJavaSourceOutputDirectory { get; set; } [Output] public ITaskItem [] GeneratedAssemblies { get; set; } = []; @@ -117,7 +120,19 @@ public override bool RunTask () frameworkAssemblyNames.Add (assemblyName); } + if (CleanJavaSourceOutputDirectory && !JavaSourceInputDirectory.IsNullOrEmpty ()) { + var inputDirectory = Path.GetFullPath (JavaSourceInputDirectory); + var outputDirectory = Path.GetFullPath (JavaSourceOutputDirectory); + if (string.Equals (inputDirectory, outputDirectory, StringComparison.OrdinalIgnoreCase)) { + Log.LogError ($"{nameof (JavaSourceInputDirectory)} and {nameof (JavaSourceOutputDirectory)} must be different when {nameof (CleanJavaSourceOutputDirectory)} is true."); + return false; + } + } + Directory.CreateDirectory (OutputDirectory); + if (CleanJavaSourceOutputDirectory && Directory.Exists (JavaSourceOutputDirectory)) { + Directory.Delete (JavaSourceOutputDirectory, recursive: true); + } Directory.CreateDirectory (JavaSourceOutputDirectory); var peReaders = new List (); @@ -168,11 +183,16 @@ public override bool RunTask () manifestConfig: manifestConfig, manifestTemplate: manifestTemplate, packageNamingPolicy: PackageNamingPolicy, - maxArrayRank: MaxArrayRank); + maxArrayRank: MaxArrayRank, + generateTypeMapAssemblies: GenerateTypeMapAssemblies); - GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); - WriteGeneratedAssembliesListFile (GeneratedAssemblies); - GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); + if (GenerateTypeMapAssemblies) { + GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); + WriteGeneratedAssembliesListFile (GeneratedAssemblies); + } + GeneratedJavaFiles = JavaSourceInputDirectory.IsNullOrEmpty () + ? WriteJavaSourcesToDisk (result.GeneratedJavaSources) + : CopyJavaSourcesFromInputDirectory (result.GeneratedJavaSources); // Write manifest to disk if generated if (result.Manifest is not null && !MergedAndroidManifestOutput.IsNullOrEmpty ()) { @@ -247,6 +267,29 @@ void WriteGeneratedAssembliesListFile (IReadOnlyList assemblies) Files.CopyIfStringChanged (text, GeneratedAssembliesListFile); } + ITaskItem [] CopyJavaSourcesFromInputDirectory (IReadOnlyList javaSources) + { + var items = new List (); + foreach (var source in javaSources) { + string inputPath = Path.Combine (JavaSourceInputDirectory ?? "", source.RelativePath); + if (!File.Exists (inputPath)) { + Log.LogError ($"Generated Java source '{inputPath}' was not found."); + continue; + } + + string outputPath = Path.Combine (JavaSourceOutputDirectory, source.RelativePath); + string? dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + using (var stream = File.OpenRead (inputPath)) { + Files.CopyIfStreamChanged (stream, outputPath); + } + items.Add (new TaskItem (outputPath)); + } + return items.ToArray (); + } + ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyPaths) { // Build a map from assembly name -> source path for timestamp comparison 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 4e99e9932be..0ec9a91085d 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 @@ -205,6 +205,55 @@ public void CoreClrTrimmableTypeMap_PackagesJavaProxyThrowable () $"`{dexFile}` should include `android.runtime.JavaProxyThrowable`."); } + [Test] + public void ReleaseCoreClrTrimmableTypeMap_SingleRuntimeIdentifier_DoesNotPackageUnlinkedTypeMapAssemblies () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + PackageName = "com.xamarin.typemapcomparison", + ProjectName = "TypemapComparison", + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty (KnownProperties.RuntimeIdentifier, "android-arm64"); + proj.SetProperty ("AndroidPackageFormat", "apk"); + proj.SetProperty (KnownProperties.AndroidLinkTool, "r8"); + proj.SetProperty ("TrimMode", "full"); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (Path.Combine ("temp", $"TypemapComparison_trimmable_single_rid_{Guid.NewGuid ():N}")); + Assert.IsTrue (builder.Build (proj), "trimmable single-RID build should have succeeded."); + + var apkDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath); + var apkPath = Directory.GetFiles (apkDirectory, "*-Signed.apk", SearchOption.AllDirectories).Single (); + var typeMapDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "typemap")); + var linkedAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked")); + var javaSourceDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "android", "src")); + var acwMapPath = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "acw-map.txt")); + var proguardPrimaryPath = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "proguard", "proguard_project_primary.cfg")); + + DirectoryAssert.Exists (typeMapDirectory, "trimmable build should generate typemap assemblies."); + DirectoryAssert.Exists (linkedAssemblyDirectory, "Release trimmable build should run ILLink."); + + var unlinkedTypeMapAssemblies = Directory.GetFiles (typeMapDirectory, "*.dll") + .Where (file => !File.Exists (Path.Combine (linkedAssemblyDirectory, Path.GetFileName (file)))) + .Select (Path.GetFileName) + .OrderBy (name => name, StringComparer.Ordinal) + .ToArray (); + Assert.IsNotEmpty (unlinkedTypeMapAssemblies, "Test setup should include typemap assemblies that ILLink removed."); + + var packagedAssemblyNames = ReadPackagedManagedAssemblyNames (apkPath, AndroidTargetArch.Arm64); + var packagedUnlinkedTypeMapAssemblies = packagedAssemblyNames.Intersect (unlinkedTypeMapAssemblies, StringComparer.Ordinal).OrderBy (name => name, StringComparer.Ordinal).ToArray (); + Assert.IsEmpty ( + packagedUnlinkedTypeMapAssemblies, + $"{apkPath} should package linked typemap assemblies, not untrimmed typemap assemblies removed by ILLink."); + + AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (apkPath, javaSourceDirectory, acwMapPath, proguardPrimaryPath); + } + [Test] public void ReleaseCoreClrTrimmableTypeMap_DoesNotKeepMoreTypemapPeersThanLlvmIr () { @@ -461,6 +510,64 @@ ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) return profile; } + ISet ReadPackagedManagedAssemblyNames (string apkPath, AndroidTargetArch targetArch) + { + (var explorers, var errorMessage) = AssemblyStoreExplorer.Open (apkPath); + Assert.IsNull (errorMessage, $"{apkPath} should contain readable assembly stores."); + Assert.IsNotNull (explorers, $"{apkPath} should contain assembly stores."); + + var explorer = explorers.FirstOrDefault (e => e.TargetArch == targetArch); + Assert.IsNotNull (explorer, $"{apkPath} should contain an {targetArch} assembly store."); + + return explorer.Assemblies + .Where (a => !a.Ignore && a.Name.EndsWith (".dll", StringComparison.OrdinalIgnoreCase) && !a.Name.EndsWith (".ni.dll", StringComparison.OrdinalIgnoreCase)) + .Select (a => a.Name) + .ToHashSet (StringComparer.Ordinal); + } + + void AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (string apkPath, string javaSourceDirectory, string acwMapPath, string proguardPrimaryPath) + { + const string deadManagedType = "Android.Speech.IRecognitionListenerImplementor"; + const string deadJavaName = "mono/android/speech/RecognitionListenerImplementor"; + const string deadJavaDotName = "mono.android.speech.RecognitionListenerImplementor"; + + Assert.IsTrue ( + Directory.EnumerateFiles (javaSourceDirectory, "MainActivity.java", SearchOption.AllDirectories).Any (), + "Post-trim Java source generation should keep the app activity JCW."); + FileAssert.DoesNotExist ( + Path.Combine (javaSourceDirectory, "mono", "android", "speech", "RecognitionListenerImplementor.java"), + "Post-trim Java source generation should not copy framework listener implementors removed by ILLink."); + + FileAssert.Exists (acwMapPath, "Post-trim scan should rewrite acw-map.txt for R8."); + var acwMap = File.ReadAllText (acwMapPath); + Assert.IsFalse (acwMap.Contains (deadManagedType, StringComparison.Ordinal), $"{acwMapPath} should be based on linked assemblies."); + Assert.IsFalse (acwMap.Contains (deadJavaDotName, StringComparison.Ordinal), $"{acwMapPath} should not keep removed framework listener implementors."); + + FileAssert.Exists (proguardPrimaryPath, "R8 should generate a primary proguard configuration from the post-trim acw-map."); + Assert.IsFalse ( + File.ReadAllText (proguardPrimaryPath).Contains (deadJavaDotName, StringComparison.Ordinal), + $"{proguardPrimaryPath} should not keep removed framework listener implementors."); + + Assert.IsFalse ( + ReadDexClassNames (apkPath).Contains (deadJavaName), + $"{apkPath} should not contain the removed framework listener implementor in classes.dex."); + } + + ISet ReadDexClassNames (string apkPath) + { + var classNames = new HashSet (StringComparer.Ordinal); + using var zip = ZipFile.OpenRead (apkPath); + foreach (var entry in zip.Entries.Where (e => Regex.IsMatch (e.FullName, @"^classes(\d*)\.dex$", RegexOptions.CultureInvariant))) { + using var stream = entry.Open (); + using var memory = new MemoryStream (); + stream.CopyTo (memory); + foreach (var javaClass in DexProfileReader.Read (memory.ToArray ()).Classes) { + classNames.Add (javaClass.Name); + } + } + return classNames; + } + ApkComparisonProfile ReadApkProfile (string name, string apkPath, string acwMapPath, string javaSourceDirectory, string typeMapDirectory, string linkedAssemblyDirectory) { var profile = new ApkComparisonProfile { From 9d74d353709624e92cc628e4cbfd7ed11478572e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 20 May 2026 17:26:07 +0200 Subject: [PATCH 8/9] Support DynamicCodeSupport=false with trimmable typemaps Emit array typemap sentinels when dynamic code support is explicitly disabled so CoreCLR trimmable typemap builds can use the no-dynamic-code array path. Keep DynamicCodeSupport enabled by default and cover the explicit opt-out path in tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 6 +- .../TrimmableTypeMapBuildTests.cs | 87 ++++++++++++++++++- 2 files changed, 86 insertions(+), 7 deletions(-) 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 1955c4c2580..bae58e324e5 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 @@ -31,10 +31,10 @@ <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTrimmableTypeMapJavaStamp) <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' ">$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll - - <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and '$(PublishAot)' == 'true' ">3 + <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and ('$(PublishAot)' == 'true' or '$(DynamicCodeSupport)' == 'false') ">3 <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' ">0 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 0ec9a91085d..87b560096b6 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 @@ -205,6 +205,24 @@ public void CoreClrTrimmableTypeMap_PackagesJavaProxyThrowable () $"`{dexFile}` should include `android.runtime.JavaProxyThrowable`."); } + [Test] + public void ReleaseCoreClrTrimmableTypeMap_SupportsExplicitDynamicCodeSupportOff () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + + var dynamicCodeDisabledTrimmable = BuildDynamicCodeSupportProfile ("trimmable", dynamicCodeSupport: false); + + const string dynamicCodeSupportFalse = "\"System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported\": false"; + Assert.IsTrue ( + dynamicCodeDisabledTrimmable.RuntimeConfig.Contains (dynamicCodeSupportFalse, StringComparison.Ordinal), + "trimmable typemap builds should honor explicit DynamicCodeSupport=false."); + Assert.IsTrue ( + dynamicCodeDisabledTrimmable.LinkedTypeMapAssembliesContainArrayRankSentinels, + "trimmable typemap builds should emit array typemap sentinels when dynamic code is disabled."); + } + [Test] public void ReleaseCoreClrTrimmableTypeMap_SingleRuntimeIdentifier_DoesNotPackageUnlinkedTypeMapAssemblies () { @@ -238,17 +256,17 @@ public void ReleaseCoreClrTrimmableTypeMap_SingleRuntimeIdentifier_DoesNotPackag DirectoryAssert.Exists (typeMapDirectory, "trimmable build should generate typemap assemblies."); DirectoryAssert.Exists (linkedAssemblyDirectory, "Release trimmable build should run ILLink."); - var unlinkedTypeMapAssemblies = Directory.GetFiles (typeMapDirectory, "*.dll") + var removedTypeMapAssemblies = Directory.GetFiles (typeMapDirectory, "*.dll") + .Where (IsTypeMapAssemblyPath) .Where (file => !File.Exists (Path.Combine (linkedAssemblyDirectory, Path.GetFileName (file)))) .Select (Path.GetFileName) .OrderBy (name => name, StringComparer.Ordinal) .ToArray (); - Assert.IsNotEmpty (unlinkedTypeMapAssemblies, "Test setup should include typemap assemblies that ILLink removed."); var packagedAssemblyNames = ReadPackagedManagedAssemblyNames (apkPath, AndroidTargetArch.Arm64); - var packagedUnlinkedTypeMapAssemblies = packagedAssemblyNames.Intersect (unlinkedTypeMapAssemblies, StringComparer.Ordinal).OrderBy (name => name, StringComparer.Ordinal).ToArray (); + var packagedRemovedTypeMapAssemblies = packagedAssemblyNames.Intersect (removedTypeMapAssemblies, StringComparer.Ordinal).OrderBy (name => name, StringComparer.Ordinal).ToArray (); Assert.IsEmpty ( - packagedUnlinkedTypeMapAssemblies, + packagedRemovedTypeMapAssemblies, $"{apkPath} should package linked typemap assemblies, not untrimmed typemap assemblies removed by ILLink."); AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (apkPath, javaSourceDirectory, acwMapPath, proguardPrimaryPath); @@ -510,6 +528,61 @@ ApkComparisonProfile BuildTypemapComparisonApk (string typemapImplementation) return profile; } + DynamicCodeSupportProfile BuildDynamicCodeSupportProfile (string typemapImplementation, bool? dynamicCodeSupport) + { + var dynamicCodeSuffix = dynamicCodeSupport.HasValue ? $"_{dynamicCodeSupport.Value.ToString ().ToLowerInvariant ()}" : ""; + var projectName = $"DynamicCodeSupport_{typemapImplementation.Replace ("-", "_")}{dynamicCodeSuffix}"; + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + PackageName = "com.xamarin.dynamiccodesupport", + ProjectName = projectName, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty (KnownProperties.RuntimeIdentifier, "android-arm64"); + proj.SetProperty ("AndroidPackageFormat", "apk"); + proj.SetProperty (KnownProperties.AndroidLinkTool, "r8"); + proj.SetProperty ("TrimMode", "full"); + proj.SetProperty ("PublishReadyToRun", "false"); + proj.SetProperty ("_AndroidTypeMapImplementation", typemapImplementation); + if (dynamicCodeSupport.HasValue) { + proj.SetProperty ("DynamicCodeSupport", dynamicCodeSupport.Value.ToString ().ToLowerInvariant ()); + } + + using var builder = CreateApkBuilder (Path.Combine ("temp", $"{projectName}_{Guid.NewGuid ():N}")); + Assert.IsTrue (builder.Build (proj), $"{typemapImplementation} build should have succeeded."); + + var runtimeConfigPath = FindOutputFile (builder, proj, $"{proj.ProjectName}.runtimeconfig.json"); + var linkedAssemblyDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android-arm64", "linked")); + return new DynamicCodeSupportProfile { + RuntimeConfig = File.ReadAllText (runtimeConfigPath), + LinkedTypeMapAssembliesContainArrayRankSentinels = TypeMapAssembliesContainType (linkedAssemblyDirectory, "__ArrayMapRank1"), + }; + } + + string FindOutputFile (ProjectBuilder builder, XamarinAndroidApplicationProject proj, string fileName) + { + var outputDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath); + var files = Directory.GetFiles (outputDirectory, fileName, SearchOption.AllDirectories); + Assert.AreEqual (1, files.Length, $"{outputDirectory} should contain one {fileName}."); + return files [0]; + } + + bool TypeMapAssembliesContainType (string directory, string typeName) + { + if (!Directory.Exists (directory)) { + return false; + } + + foreach (var file in Directory.EnumerateFiles (directory, "*.dll", SearchOption.TopDirectoryOnly).Where (IsTypeMapAssemblyPath)) { + using var assembly = AssemblyDefinition.ReadAssembly (file); + if (assembly.Modules.SelectMany (m => m.Types).Any (type => type.Name == typeName)) { + return true; + } + } + + return false; + } + ISet ReadPackagedManagedAssemblyNames (string apkPath, AndroidTargetArch targetArch) { (var explorers, var errorMessage) = AssemblyStoreExplorer.Open (apkPath); @@ -1058,6 +1131,12 @@ class ApkComparisonProfile public readonly List TypeMapAssemblies = new List (); } + class DynamicCodeSupportProfile + { + public string RuntimeConfig; + public bool LinkedTypeMapAssembliesContainArrayRankSentinels; + } + class TypeMapAssemblyMetrics { public string Stage; From 6daf3c7f9bcfb4a0caaa6e35b3dfac28abdd6e57 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 20 May 2026 18:04:49 +0200 Subject: [PATCH 9/9] Use coded errors for trimmable typemap task Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/index.md | 2 ++ Documentation/docs-mobile/messages/xa4253.md | 2 +- Documentation/docs-mobile/messages/xa4254.md | 25 +++++++++++++++++ Documentation/docs-mobile/messages/xa4255.md | 27 +++++++++++++++++++ .../Properties/Resources.Designer.cs | 18 +++++++++++++ .../Properties/Resources.resx | 11 ++++++++ .../Tasks/GenerateTrimmableTypeMap.cs | 4 +-- 7 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 Documentation/docs-mobile/messages/xa4254.md create mode 100644 Documentation/docs-mobile/messages/xa4255.md diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index eba78e5367c..cb6423a2a25 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -214,6 +214,8 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4250](xa4250.md): Manifest-referenced type '{type}' was not found in any scanned assembly. It may be a framework type. + [XA4252](xa4252.md): Insecure HTTP Maven repository URL '{url}' is not allowed. Use an HTTPS URL, or set AllowInsecureHttp="true" metadata on the item to override this check. + [XA4253](xa4253.md): Generated Java callable wrapper code changed: '{path}' ++ [XA4254](xa4254.md): Trimmable type map Java source input directory '{input}' and output directory '{output}' must be different. ++ [XA4255](xa4255.md): Generated trimmable type map Java source '{path}' was not found. + XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI. + [XA4301](xa4301.md): Apk already contains the item `xxx`. + [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex} diff --git a/Documentation/docs-mobile/messages/xa4253.md b/Documentation/docs-mobile/messages/xa4253.md index 74632be962c..8c27b5fb802 100644 --- a/Documentation/docs-mobile/messages/xa4253.md +++ b/Documentation/docs-mobile/messages/xa4253.md @@ -10,7 +10,7 @@ f1_keywords: ## Example messages -``` +```text error XA4253: Generated Java callable wrapper code changed: 'obj/Release/android/src/mono/MonoRuntimeProvider.java' ``` diff --git a/Documentation/docs-mobile/messages/xa4254.md b/Documentation/docs-mobile/messages/xa4254.md new file mode 100644 index 00000000000..6997cf235b1 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4254.md @@ -0,0 +1,25 @@ +--- +title: .NET for Android error XA4254 +description: XA4254 error code +ms.date: 05/20/2026 +f1_keywords: + - "XA4254" +--- + +# .NET for Android error XA4254 + +## Example message + +```text +error XA4254: Trimmable type map Java source input directory 'obj/Release/net11.0-android/typemap/java' and output directory 'obj/Release/net11.0-android/typemap/java' must be different. +``` + +## Issue + +The trimmable type map build tried to clean the Java source output directory, but the configured input and output directories resolved to the same path. + +Cleaning the output directory in this configuration would delete the input Java sources before they can be copied. + +## Solution + +This error indicates an internal build configuration problem. File an issue at and include the full build log. diff --git a/Documentation/docs-mobile/messages/xa4255.md b/Documentation/docs-mobile/messages/xa4255.md new file mode 100644 index 00000000000..633df761ecb --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4255.md @@ -0,0 +1,27 @@ +--- +title: .NET for Android error XA4255 +description: XA4255 error code +ms.date: 05/20/2026 +f1_keywords: + - "XA4255" +--- + +# .NET for Android error XA4255 + +## Example message + +```text +error XA4255: Generated trimmable type map Java source 'obj/Release/net11.0-android/typemap/java/my/app/MainActivity.java' was not found. +``` + +## Issue + +The post-trim trimmable type map scan expected to copy a generated Java source file from the pre-trim Java source directory, but the file was missing. + +This can happen if intermediate build outputs are stale or if the generated Java source list no longer matches the files on disk. + +## Solution + +Delete the project's `obj` and `bin` directories, then rebuild. + +If the error persists after a clean rebuild, file an issue at and include the full build log. diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 7d2fe163ff9..6cfdd56fa24 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1506,6 +1506,24 @@ public static string XA4251 { } } + /// + /// Looks up a localized string similar to Trimmable type map Java source input directory '{0}' and output directory '{1}' must be different.. + /// + public static string XA4254 { + get { + return ResourceManager.GetString("XA4254", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generated trimmable type map Java source '{0}' was not found.. + /// + public static string XA4255 { + get { + return ResourceManager.GetString("XA4255", resourceCulture); + } + } + /// /// Looks up a localized string similar to Native library '{0}' will not be bundled because it has an unsupported ABI. Move this file to a directory with a valid Android ABI name such as 'libs/armeabi-v7a/'.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index 25fb32571a5..bc949a02578 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1103,6 +1103,17 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS Generated Java callable wrapper code changed: '{0}' {0} - The path to the generated Java callable wrapper file + + Trimmable type map Java source input directory '{0}' and output directory '{1}' must be different. + The following are literal names and should not be translated: Trimmable type map, Java. +{0} - Full path to the Java source input directory +{1} - Full path to the Java source output directory + + + Generated trimmable type map Java source '{0}' was not found. + The following are literal names and should not be translated: trimmable type map, Java. +{0} - Full path to the generated Java source file + Command '{0}' failed.\n{1} '{0}' is a failed command name (potentially with path) followed by all the arguments passed to it. {1} is the combined output on the standard error and standard output streams. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 2dd34547a16..2b3d67477f2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -124,7 +124,7 @@ public override bool RunTask () var inputDirectory = Path.GetFullPath (JavaSourceInputDirectory); var outputDirectory = Path.GetFullPath (JavaSourceOutputDirectory); if (string.Equals (inputDirectory, outputDirectory, StringComparison.OrdinalIgnoreCase)) { - Log.LogError ($"{nameof (JavaSourceInputDirectory)} and {nameof (JavaSourceOutputDirectory)} must be different when {nameof (CleanJavaSourceOutputDirectory)} is true."); + Log.LogCodedError ("XA4254", Properties.Resources.XA4254, inputDirectory, outputDirectory); return false; } } @@ -273,7 +273,7 @@ ITaskItem [] CopyJavaSourcesFromInputDirectory (IReadOnlyList