From 0133a7dd7c4613199312d08aa4e672551b1d93c3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 3 Jul 2026 16:58:44 +0200 Subject: [PATCH 01/14] [TrimmableTypeMap][NativeAOT] Fix XA4321: collect ILC DGML from per-RID inner build path _ResolveAssemblies always dispatches a per-RID inner build with AppendRuntimeIdentifierToOutputPath=true, so ILC writes each *.scan.dgml.xml under the RID-nested obj///native/ path -- for both a single explicit RuntimeIdentifier and a RuntimeIdentifiers list. _CollectTrimmableNativeAotDgmlFiles runs in the outer build, whose NativeIntermediateOutputPath has no RID segment. The single-RuntimeIdentifier branch collected from that flat path (obj//native/), which never exists, so _GenerateTrimmableTypeMapProguardConfiguration failed with XA4321 across every single-RID NativeAOT build (e.g. IncrementalBuildDifferentDevice and the other MockPrimaryCpuAbi tests). Reconstruct the RID-nested path for both the single-RuntimeIdentifier and the RuntimeIdentifiers cases (they now share one item expression); the flat NativeIntermediateOutputPath remains only as the defensive no-RID fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 15124493662d8f4b4be98cf4499778cd4e3c2d73) --- ...osoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index fb655661a24..80ee5ca5b6f 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -73,10 +73,13 @@ <_TrimmableNativeAotDgmlFiles Remove="@(_TrimmableNativeAotDgmlFiles)" /> <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifier)" Condition=" '$(RuntimeIdentifier)' != '' " /> <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifiers)" Condition=" '$(RuntimeIdentifier)' == '' and '$(RuntimeIdentifiers)' != '' " /> - + <_TrimmableNativeAotDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' == '' " /> - <_TrimmableNativeAotDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" Condition=" '$(RuntimeIdentifier)' != '' " /> - <_TrimmableNativeAotDgmlFiles Include="$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).scan.dgml.xml" Condition=" '$(RuntimeIdentifier)' == '' and '@(_TrimmableNativeAotRuntimeIdentifiers)' != '' " /> + <_TrimmableNativeAotDgmlFiles Include="$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).scan.dgml.xml" Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' != '' " /> From f0724bfb1316acfd16653a5a5ce773fd0243c5b8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 3 Jul 2026 20:42:40 +0200 Subject: [PATCH 02/14] [TrimmableTypeMap][NativeAOT] Fix single-RID DGML path (both output-path shapes) The previous XA4321 fix assumed ILC's *.scan.dgml.xml always lives at the RID-nested $(IntermediateOutputPath)/native/. That is only true when the outer $(IntermediateOutputPath) has no RID segment (a $(RuntimeIdentifiers) list, or a RID assigned late by _GetPrimaryCpuAbi). For a single explicit $(RuntimeIdentifier) set early, the SDK already appended the RID to the outer path, so reconstructing /native/ produced a doubled RID (obj/Release/android-arm64/android-arm64/native/) and XA4321. Emit both candidate paths ($(NativeIntermediateOutputPath) and the RID-nested form), Exists()-filtered, and consume whichever ILC actually produced. This is correct for single-RID (either output-path shape), multi-RID, and no-RID. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 6ea9395feaa0c1be9420a5a1affeb50c59d7c3d5) --- ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index 80ee5ca5b6f..50d59b808cc 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -73,13 +73,20 @@ <_TrimmableNativeAotDgmlFiles Remove="@(_TrimmableNativeAotDgmlFiles)" /> <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifier)" Condition=" '$(RuntimeIdentifier)' != '' " /> <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifiers)" Condition=" '$(RuntimeIdentifier)' == '' and '$(RuntimeIdentifiers)' != '' " /> - - <_TrimmableNativeAotDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' == '' " /> - <_TrimmableNativeAotDgmlFiles Include="$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).scan.dgml.xml" Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' != '' " /> + + <_TrimmableNativeAotDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" + Condition=" Exists('$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml') " /> + <_TrimmableNativeAotDgmlFiles Include="$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).scan.dgml.xml" + Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' != '' and Exists('$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).scan.dgml.xml') " /> From 0f33c69c11882974a032a1d9a92cc9e387488586 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 3 Jul 2026 22:43:01 +0200 Subject: [PATCH 03/14] [TrimmableTypeMap][NativeAOT] Fall back to codegen DGML when scan DGML is absent ILC only emits the scan graph (*.scan.dgml.xml) when its scanner phase runs (optimized/Release builds). Unoptimized NativeAOT builds - e.g. a Debug configuration, as produced when a solution is built without an explicit Release configuration (AllProjectsHaveSameOutputDirectory) - emit only the codegen graph (*.codegen.dgml.xml). The trimmable proguard-keep generator then failed with XA4319 "No NativeAOT DGML files were provided". The codegen graph carries the same "Type metadata: [...]" nodes the generator reads, so collect it at the same candidate locations and use it only when no scan graph was found (the scan graph is smaller, hence preferred). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit e58cb04e191007c9950135db2c92ab3c4de7f5d6) --- ...t.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index 50d59b808cc..3e118f5833c 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -71,6 +71,7 @@ <_TrimmableNativeAotRuntimeIdentifiers Remove="@(_TrimmableNativeAotRuntimeIdentifiers)" /> <_TrimmableNativeAotDgmlFiles Remove="@(_TrimmableNativeAotDgmlFiles)" /> + <_TrimmableNativeAotCodegenDgmlFiles Remove="@(_TrimmableNativeAotCodegenDgmlFiles)" /> <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifier)" Condition=" '$(RuntimeIdentifier)' != '' " /> <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifiers)" Condition=" '$(RuntimeIdentifier)' == '' and '$(RuntimeIdentifiers)' != '' " /> + <_TrimmableNativeAotCodegenDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).codegen.dgml.xml" + Condition=" Exists('$(NativeIntermediateOutputPath)$(TargetName).codegen.dgml.xml') " /> + <_TrimmableNativeAotCodegenDgmlFiles Include="$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).codegen.dgml.xml" + Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' != '' and Exists('$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).codegen.dgml.xml') " /> + <_TrimmableNativeAotDgmlFiles Include="@(_TrimmableNativeAotCodegenDgmlFiles)" + Condition=" '@(_TrimmableNativeAotDgmlFiles->Count())' == '0' " /> From 9e41e031c52eb665495bb2835f79a43ad486a37c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 3 Jul 2026 22:43:01 +0200 Subject: [PATCH 04/14] [TrimmableTypeMap] Fix ManifestPlaceholders on the trimmable NativeAOT path Two regressions surfaced by ManifestPlaceholders once the trimmable typemap became the NativeAOT default: * Placeholder values kept literal backslashes (e.g. "a=b\c"). The legacy pipeline re-encodes the substituted manifest through aapt2, which rewrites '\' to '/' on Unix; the trimmable generator writes the merged manifest directly, so normalize placeholder values to Path.DirectorySeparatorChar in ManifestGenerator.ApplyPlaceholders to keep the output identical. * The legacy manifest merger has no _ManifestMerger step (that target only runs for manifestmerger.jar), so with AndroidManifestMerger=legacy nothing copied the already-merged trimmable manifest to obj//android/AndroidManifest.xml and _ReadAndroidManifest failed with a FileNotFoundException. Copy it into place on the trimmable path when the merger is not manifestmerger.jar. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 8af5b62039b67bd46b5911cf6bd65d147df2b125) --- .../Generator/ManifestGenerator.cs | 6 +++++- .../Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 1a4fb259ded..f88b9481382 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -518,7 +518,11 @@ internal static void ApplyPlaceholders (XDocument doc, string? placeholders, str var eqIndex = entry.IndexOf ('='); if (eqIndex > 0) { var key = entry.Substring (0, eqIndex).Trim (); - var value = entry.Substring (eqIndex + 1).Trim (); + // Normalize '\' to the platform directory separator to match the legacy pipeline, + // where the substituted manifest is re-encoded by aapt2 (which rewrites backslashes + // to '/' on Unix). The trimmable generator writes the merged manifest directly, so we + // apply the same normalization here to keep placeholder values byte-for-byte identical. + var value = entry.Substring (eqIndex + 1).Trim ().Replace ('\\', Path.DirectorySeparatorChar); replacements ["${" + key + "}"] = value; } else if (eqIndex < 0) { // An entry without '=' is not a valid key=value pair. Mirror the legacy 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 5090ba7fa6a..2b1af9cd713 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 @@ -383,6 +383,15 @@ SkipUnchangedFiles="true" Condition="Exists('$(_TypeMapBaseOutputDir)AndroidManifest.xml')" /> + + + @@ -392,6 +401,8 @@ + From fd76e45d3c63eac2c30a185f51d90746c995765b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 3 Jul 2026 23:29:51 +0200 Subject: [PATCH 05/14] [TrimmableTypeMap][NativeAOT] Add _AndroidTrimmableTypemapTrimJavaCode toggle (default off) The ILC DGML files used to drive Java Callable Wrapper trimming are very large (hundreds of MB) and dominate NativeAOT build time. Until ILC/illink expose a leaner typemap dump, gate that work behind a new property: * _AndroidTrimmableTypemapTrimJavaCode (default false): skip DGML generation (IlcGenerateDgmlFile is no longer forced) and skip DGML collection, and have GenerateNativeAotProguardConfiguration emit a -keep rule for every ACW in the ACW map so R8 keeps them all. This costs a few hundred kB of extra dex but removes the huge DGML and its parsing/scan from every build. * Set to true, the previous behavior is restored: ILC emits the DGML and the keep rules are computed from the DGML-retained subset. GenerateNativeAotProguardConfiguration gains a TrimJavaCallableWrappers switch; when false it no longer requires a DGML (NativeAotDgmlFiles is now optional). Unit tests cover both the DGML-trimmed path and the keep-all path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit f157df92e627717618a16319d0652ecfbe21a27c) --- ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 13 +++++- .../GenerateNativeAotProguardConfiguration.cs | 40 +++++++++++++------ .../Tasks/GenerateTrimmableTypeMapTests.cs | 32 +++++++++++++++ 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index 3e118f5833c..35ede0cd9ef 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -5,12 +5,20 @@ + + <_AndroidTrimmableTypemapTrimJavaCode Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == '' ">false <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider r8 d8 True True - true + + true <_UseTrimmableNativeAotProguardConfiguration Condition=" '$(_UseTrimmableNativeAotProguardConfiguration)' == '' ">true <_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_GenerateTrimmableTypeMapProguardConfiguration @@ -67,7 +75,7 @@ DependsOnTargets="_GenerateTrimmableTypeMap" /> + Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == 'true' and '$(PublishTrimmed)' == 'true' and '$(_ProguardProjectConfiguration)' != '' "> <_TrimmableNativeAotRuntimeIdentifiers Remove="@(_TrimmableNativeAotRuntimeIdentifiers)" /> <_TrimmableNativeAotDgmlFiles Remove="@(_TrimmableNativeAotDgmlFiles)" /> @@ -110,6 +118,7 @@ diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs index 44ae925d4f4..08e768d315f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs @@ -15,7 +15,6 @@ public class GenerateNativeAotProguardConfiguration : AndroidTask public override string TaskPrefix => "GNAPC"; - [Required] public ITaskItem [] NativeAotDgmlFiles { get; set; } = []; [Required] @@ -24,6 +23,12 @@ public class GenerateNativeAotProguardConfiguration : AndroidTask [Required] public string OutputFile { get; set; } = ""; + // When false, the ILC DGML is not consulted (it may not have been generated at all) and a + // -keep rule is emitted for every Java Callable Wrapper in the ACW map, so R8 keeps them all + // instead of shrinking the unused ones. This trades a small amount of dex size for skipping the + // very large DGML files and the DGML parsing/scan, which dominate NativeAOT build time. + public bool TrimJavaCallableWrappers { get; set; } = true; + public override bool RunTask () { var dir = Path.GetDirectoryName (OutputFile); @@ -31,22 +36,27 @@ public override bool RunTask () Directory.CreateDirectory (dir); } - if (NativeAotDgmlFiles.Length == 0) { - Log.LogCodedError ("XA4319", Properties.Resources.XA4319); - return !Log.HasLoggedErrors; - } if (!File.Exists (AcwMapFile)) { Log.LogCodedError ("XA4320", Properties.Resources.XA4320, AcwMapFile); return !Log.HasLoggedErrors; } - foreach (var dgmlFile in NativeAotDgmlFiles) { - if (!File.Exists (dgmlFile.ItemSpec)) { - Log.LogCodedError ("XA4321", Properties.Resources.XA4321, dgmlFile.ItemSpec); + + HashSet? retainedTypeKeys = null; + if (TrimJavaCallableWrappers) { + if (NativeAotDgmlFiles.Length == 0) { + Log.LogCodedError ("XA4319", Properties.Resources.XA4319); return !Log.HasLoggedErrors; } + foreach (var dgmlFile in NativeAotDgmlFiles) { + if (!File.Exists (dgmlFile.ItemSpec)) { + Log.LogCodedError ("XA4321", Properties.Resources.XA4321, dgmlFile.ItemSpec); + return !Log.HasLoggedErrors; + } + } + retainedTypeKeys = LoadRetainedTypeKeysFromDgml (); } - var retainedTypeKeys = LoadRetainedTypeKeysFromDgml (); + // A null retainedTypeKeys means "keep every ACW" (Java trimming disabled). var javaTypes = LoadJavaTypesFromAcwMap (retainedTypeKeys); using var writer = new StringWriter (); @@ -56,13 +66,17 @@ public override bool RunTask () } Files.CopyIfStringChanged (writer.ToString (), OutputFile); - Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT trimmable typemap ProGuard rules from {1} DGML file(s).", javaTypes.Count, NativeAotDgmlFiles.Length); + if (TrimJavaCallableWrappers) { + Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT trimmable typemap ProGuard rules from {1} DGML file(s).", javaTypes.Count, NativeAotDgmlFiles.Length); + } else { + Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT ProGuard rules keeping all ACWs (Java Callable Wrapper trimming is disabled).", javaTypes.Count); + } return !Log.HasLoggedErrors; } - List LoadJavaTypesFromAcwMap (HashSet retainedTypeKeys) + List LoadJavaTypesFromAcwMap (HashSet? retainedTypeKeys) { - var javaTypes = new List (retainedTypeKeys.Count); + var javaTypes = new List (retainedTypeKeys?.Count ?? 0); var seenJavaTypes = new HashSet (StringComparer.Ordinal); foreach (var line in File.ReadLines (AcwMapFile)) { var separator = line.IndexOf (";", StringComparison.Ordinal); @@ -71,7 +85,7 @@ List LoadJavaTypesFromAcwMap (HashSet retainedTypeKeys) } var managedTypeName = line.Substring (0, separator); var javaTypeName = line.Substring (separator + 1); - if (retainedTypeKeys.Contains (managedTypeName) && seenJavaTypes.Add (javaTypeName)) { + if ((retainedTypeKeys == null || retainedTypeKeys.Contains (managedTypeName)) && seenJavaTypes.Add (javaTypeName)) { javaTypes.Add (javaTypeName); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index 7a6f121832d..1bbcea9b768 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -271,6 +271,7 @@ public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) }, AcwMapFile = acwMapFile, OutputFile = outputFile, + TrimJavaCallableWrappers = true, }; Assert.IsTrue (task.Execute (), "Task should succeed."); @@ -282,6 +283,37 @@ public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata StringAssert.DoesNotContain ("other.Type", proguard); } + [Test] + public void Execute_GenerateNativeAotProguardConfiguration_KeepsAllWhenTrimmingDisabled () + { + var path = Path.Combine (Root, "temp", TestName); + var acwMapFile = Path.Combine (path, "acw-map.txt"); + var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); + Directory.CreateDirectory (path); + File.WriteAllText (acwMapFile, """ + UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity + Android.App.Activity, Mono.Android;android.app.Activity + Duplicate.Type, My.Assembly;my.app.Duplicate + Other.Type;other.Type + """); + + // No DGML is provided: with trimming disabled the task must keep every ACW from the map + // rather than shrinking to the DGML-retained subset. + var task = new GenerateNativeAotProguardConfiguration { + BuildEngine = new MockBuildEngine (TestContext.Out), + AcwMapFile = acwMapFile, + OutputFile = outputFile, + TrimJavaCallableWrappers = false, + }; + + Assert.IsTrue (task.Execute (), "Task should succeed without a DGML when trimming is disabled."); + var proguard = File.ReadAllText (outputFile); + StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); + StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); + StringAssert.Contains ("-keep class my.app.Duplicate { *; }", proguard); + StringAssert.Contains ("-keep class other.Type { *; }", proguard); + } + GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir, IList? messages = null, IList? warnings = null, string tfv = "v11.0") { From 86aaedf624553782729b9cb17e1f80213f0bdd96 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 4 Jul 2026 03:31:18 +0200 Subject: [PATCH 06/14] [TrimmableTypeMap][NativeAOT] Generate per-process runtime provider Java sources A component with a non-default android:process (e.g. [BroadcastReceiver(Process=":remote")]) makes the manifest generator emit an extra runtime provider (NativeAotRuntimeProvider_1) and return its name in AdditionalProviderSources. On the legacy/LLVM-IR path GenerateAdditionalProviderSources writes the matching Java source, but the trimmable path only surfaced the item and never generated the .java, so NativeAotRuntimeProvider_1 was missing from classes.dex (Desugar failed on NativeAOT). Extract the provider-source writing into a shared GenerateAdditionalProviderSources.WriteAdditionalRuntimeProviderSources helper and call it from GenerateNativeAotBootstrapSources (which already runs on the trimmable path), passing @(_AdditionalProviderSources). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 934cd3b53382d27c33246fcd612dc0d1ada6878d) --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 + .../GenerateAdditionalProviderSources.cs | 35 +++++++++++++------ .../GenerateNativeAotBootstrapSources.cs | 8 +++++ 3 files changed, 33 insertions(+), 11 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 2b1af9cd713..3950c59877b 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 @@ -375,6 +375,7 @@ OutputDirectory="$(IntermediateOutputPath)android" TargetName="$(TargetName)" Environments="@(_EnvironmentFiles)" + AdditionalProviderSources="@(_AdditionalProviderSources)" EnableSGenConcurrent="$(AndroidEnableSGenConcurrent)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs index 9afb10b3404..436b06d7602 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs @@ -70,18 +70,8 @@ void Generate (NativeCodeGenStateObject codeGenState) Xamarin.Android.Tasks.AndroidRuntime.CoreCLR => true, _ => false, }; - string providerTemplateFile = isMonoVM ? - "MonoRuntimeProvider.Bundled.java" : - "NativeAotRuntimeProvider.java"; - string providerTemplate = GetResource (providerTemplateFile); - foreach (var provider in AdditionalProviderSources) { - var contents = providerTemplate.Replace (isMonoVM ? "MonoRuntimeProvider" : "NativeAotRuntimeProvider", provider); - var real_provider = isMonoVM ? - Path.Combine (OutputDirectory, "src", "mono", provider + ".java") : - Path.Combine (OutputDirectory, "src", "net", "dot", "jni", "nativeaot", provider + ".java"); - Files.CopyIfStringChanged (contents, real_provider); - } + WriteAdditionalRuntimeProviderSources (OutputDirectory, isMonoVM, AdditionalProviderSources); // For NativeAOT, generate JavaInteropRuntime.java and NativeAotEnvironmentVars.java if (androidRuntime == Xamarin.Android.Tasks.AndroidRuntime.NativeAOT) { @@ -122,6 +112,29 @@ static string GetResource (string resource) return reader.ReadToEnd (); } + /// + /// Writes the additional per-process runtime provider Java sources (e.g. NativeAotRuntimeProvider_1.java) + /// by cloning the runtime provider template for each name. Shared between the legacy (ILLink) and + /// trimmable build paths so both emit the extra providers a multi-process app declares in its manifest. + /// + internal static void WriteAdditionalRuntimeProviderSources (string outputDirectory, bool isMonoVM, string [] additionalProviderSources) + { + if (additionalProviderSources.Length == 0) { + return; + } + string providerTemplateFile = isMonoVM ? + "MonoRuntimeProvider.Bundled.java" : + "NativeAotRuntimeProvider.java"; + string providerTemplate = GetResource (providerTemplateFile); + foreach (var provider in additionalProviderSources) { + var contents = providerTemplate.Replace (isMonoVM ? "MonoRuntimeProvider" : "NativeAotRuntimeProvider", provider); + var realProvider = isMonoVM ? + Path.Combine (outputDirectory, "src", "mono", provider + ".java") : + Path.Combine (outputDirectory, "src", "net", "dot", "jni", "nativeaot", provider + ".java"); + Files.CopyIfStringChanged (contents, realProvider); + } + } + void SaveResource (string resource, string filename, string destDir, Func applyTemplate) { string template = GetResource (resource); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotBootstrapSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotBootstrapSources.cs index ba2505f71f3..39c4f4e3eb0 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotBootstrapSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotBootstrapSources.cs @@ -29,11 +29,19 @@ public sealed class GenerateNativeAotBootstrapSources : AndroidTask public bool EnableSGenConcurrent { get; set; } + // Names of the extra per-process runtime providers (e.g. NativeAotRuntimeProvider_1) that the + // manifest declares for components with a non-default android:process; their Java sources must be + // generated too. On the legacy path GenerateAdditionalProviderSources writes these; the trimmable + // path has no such task, so the bootstrap step handles them here. + public string [] AdditionalProviderSources { get; set; } = []; + public override bool RunTask () { GenerateAdditionalProviderSources.GenerateNativeAotBootstrapFiles ( Log, OutputDirectory, TargetName, Environments, EnableSGenConcurrent); + GenerateAdditionalProviderSources.WriteAdditionalRuntimeProviderSources (OutputDirectory, isMonoVM: false, AdditionalProviderSources); + return !Log.HasLoggedErrors; } } From b116e83ae0e4e2db722a515cfd9e3356ec977c75 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 4 Jul 2026 15:12:07 +0200 Subject: [PATCH 07/14] [TrimmableTypeMap][NativeAOT] Keep user AndroidJavaSource under R8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the trimmable NativeAOT path R8 was given only a comment for proguard_project_primary.cfg, so user-authored AndroidJavaSource types (Bind != true) — which have no managed peer and are absent from the acw-map — were tree-shaken out of classes.dex. Among other things this dropped large unreferenced sources, so an app that required multidex no longer did and classes2.dex was never produced (BuildAfterMultiDexIsNotRequired failed on NativeAOT). Emit the GetUserJavaTypes() -keep rules into proguard_project_primary.cfg on the trimmable path too (the ACW keeps still come from proguard_project_references.cfg). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 8cfd0fdeffe427b3d082caf6a3b6ae195f9f22a3) --- src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index a525bab3a8c..e713d68bf19 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -150,7 +150,16 @@ protected override string CreateResponseFile () if (EnableShrinking) { if (UseTrimmableNativeAotProguardConfiguration && !ProguardGeneratedApplicationConfiguration.IsNullOrEmpty ()) { - File.WriteAllText (ProguardGeneratedApplicationConfiguration, "# ACW keep rules are generated from NativeAOT ILC metadata.\n"); + // ACW keep rules come from the DGML/acw-map-driven proguard_project_references.cfg on + // the trimmable path. User-authored AndroidJavaSource (Bind != true) has no managed peer + // and is absent from that map, so keep it here explicitly; otherwise R8 shrinks it away + // (e.g. dropping large unreferenced sources so an app that needs multidex no longer does). + using (var appcfg = File.CreateText (ProguardGeneratedApplicationConfiguration)) { + appcfg.WriteLine ("# ACW keep rules are generated from NativeAOT ILC metadata."); + foreach (var java in GetUserJavaTypes ()) { + appcfg.WriteLine ($"-keep class {java} {{ *; }}"); + } + } } else if (!AcwMapFile.IsNullOrEmpty ()) { var acwMap = MonoAndroidHelper.LoadMapFile (BuildEngine4, Path.GetFullPath (AcwMapFile), StringComparer.OrdinalIgnoreCase); var javaTypes = new List (acwMap.Values.Count); From 3a9ff1cb8f3af0e14ff947b4add46c67f6d52ca2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 5 Jul 2026 21:41:39 +0200 Subject: [PATCH 08/14] [NativeAOT] Keep Resource.Designer assembly on the trimmable type map path Accessing a resource id under the trimmable type map (the NativeAOT default) threw at runtime: System.TypeInitializationException ---> System.IO.FileNotFoundException: _Microsoft.Android.Resource.Designer Root cause: the _Microsoft.Android.Resource.Designer assembly IS added to the publish set (_AddResourceDesignerToPublishFiles marks it PostprocessAssembly=true, so the SDK adds it as an IlcReference), but it is left trimmable (IsTrimmable=true). Resource ids are resolved via reflection at runtime (Android.Runtime.ResourceIdManager -> ResourceDesignerAttribute), which the trimmer cannot follow, so ILC trims the whole designer assembly away and any reflection-only resource-id access (e.g. an NUnit [TestCaseSource] static field) fails to load it. The non-trimmable (LlvmIr) path avoids this because PreTrimmingFixLegacyDesigner rewrites legacy field loads and, more importantly, it roots all publish assemblies for ILC. That rewrite is a no-op for modern assemblies (whose Resource already derives from the designer), so it is not the right fix here. Instead, simply keep the designer assembly: add it to TrimmerRootAssembly on the trimmable path. TrimmerRootAssembly is honored by both ILLink (CoreCLR) and ILC (NativeAOT, emitted as --root:); ILC's TrimMode-based rooting can't be used because _AndroidComputeIlcCompileInputs clears @(_IlcManagedInputAssemblies). No IL rewriting is required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 449e2a1e991a7b58b3c1fea1968f27b9c9a777d6) --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 3950c59877b..5c4d5974e14 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 @@ -430,4 +430,30 @@ + + + + + + + From 7ada871a638ee588fbd2884dd1d4d49c3e585817 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 5 Jul 2026 23:56:09 +0200 Subject: [PATCH 09/14] [NativeAOT] Reference the designer assembly before rooting it for ILC ILC only loads assemblies passed to it as references; _Microsoft.Android.Resource.Designer is a compile-time reference but not in @(IlcReference), so rooting it via TrimmerRootAssembly alone fails: ilc error 'Failed to load assembly _Microsoft.Android.Resource.Designer'. Add it to @(IlcReference) too, driven off @(ReferencePath) filtered to the designer so nothing is referenced/rooted when the designer assembly was not generated. @(IlcReference) is ignored by ILLink, so the CoreCLR trimmable path only gets the root. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 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 5c4d5974e14..f6fb61e4b85 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 @@ -444,16 +444,27 @@ code path) throws a TypeInitializationException wrapping System.IO.FileNotFoundException: _Microsoft.Android.Resource.Designer at runtime. - Root the whole _Microsoft.Android.Resource.Designer assembly so it is kept. TrimmerRootAssembly - is honored by both ILLink (CoreCLR) and ILC (NativeAOT). Note that ILC's TrimMode-based rooting - reads @(_IlcManagedInputAssemblies), which _AndroidComputeIlcCompileInputs clears, so - TrimmerRootAssembly is the reliable mechanism here. + Keep the whole _Microsoft.Android.Resource.Designer assembly. Two things are required on the + NativeAOT (ILC) path: + * ILC only loads the assemblies passed to it as references; the designer is a compile-time + reference but is NOT in @(IlcReference), so add it. Rooting an assembly ILC never loaded + fails hard with "Failed to load assembly '_Microsoft.Android.Resource.Designer'". + * Root it so ILC/ILLink keep it whole (TrimMode-based rooting can't be used here because + _AndroidComputeIlcCompileInputs clears @(_IlcManagedInputAssemblies)). + + Drive both off @(ReferencePath) filtered to the designer: the item is empty when the designer + assembly was not generated (e.g. no resources / AndroidGenerateResourceDesigner=false), so we + never reference or root a non-existent assembly. @(IlcReference) is ignored by ILLink, so the + CoreCLR path just gets the root. --> - + <_ResourceDesignerReferenceToRoot Include="@(ReferencePath)" + Condition=" '%(Filename)' == '_Microsoft.Android.Resource.Designer' " /> + + From b77daf96533d66df60a19c73cd7e74c63dddb5b0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Jul 2026 00:16:54 +0200 Subject: [PATCH 10/14] [TrimmableTypeMap][NativeAOT] Address review feedback Track the Java trimming toggle in the proguard configuration incremental inputs. Sort user Java keep rules and cover the disabled-with-DGML case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 18 ++++++++- src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 7 +++- .../Tasks/GenerateTrimmableTypeMapTests.cs | 38 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index 35ede0cd9ef..e99f70ab8cc 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -20,6 +20,7 @@ true <_UseTrimmableNativeAotProguardConfiguration Condition=" '$(_UseTrimmableNativeAotProguardConfiguration)' == '' ">true + <_TrimmableNativeAotProguardConfigurationInputsStamp>$(_AndroidStampDirectory)_GenerateTrimmableTypeMapProguardConfiguration.inputs <_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_GenerateTrimmableTypeMapProguardConfiguration @@ -110,10 +111,23 @@ + + + + + + + + GetUserJavaTypes () yield break; } var seen = new HashSet (StringComparer.Ordinal); + var javaTypes = new List (); foreach (var item in JavaSourceFiles) { var path = item.ItemSpec; if (path.IsNullOrEmpty () || !File.Exists (path)) { @@ -77,9 +78,13 @@ IEnumerable GetUserJavaTypes () typeName = $"{package}.{typeName}"; } if (seen.Add (typeName)) { - yield return typeName; + javaTypes.Add (typeName); } } + javaTypes.Sort (StringComparer.Ordinal); + foreach (var javaType in javaTypes) { + yield return javaType; + } } internal static string? ReadJavaPackage (string path) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index 1bbcea9b768..5ecd668649e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -314,6 +314,44 @@ public void Execute_GenerateNativeAotProguardConfiguration_KeepsAllWhenTrimmingD StringAssert.Contains ("-keep class other.Type { *; }", proguard); } + [Test] + public void Execute_GenerateNativeAotProguardConfiguration_IgnoresDgmlWhenTrimmingDisabled () + { + var path = Path.Combine (Root, "temp", TestName); + var dgmlFile = Path.Combine (path, "app.scan.dgml.xml"); + var acwMapFile = Path.Combine (path, "acw-map.txt"); + var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); + Directory.CreateDirectory (path); + File.WriteAllText (dgmlFile, """ + + + + + + + + """); + File.WriteAllText (acwMapFile, """ + UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity + Android.App.Activity, Mono.Android;android.app.Activity + Other.Type;other.Type + """); + + var task = new GenerateNativeAotProguardConfiguration { + BuildEngine = new MockBuildEngine (TestContext.Out), + NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) }, + AcwMapFile = acwMapFile, + OutputFile = outputFile, + TrimJavaCallableWrappers = false, + }; + + Assert.IsTrue (task.Execute (), "Task should succeed and ignore the DGML when trimming is disabled."); + var proguard = File.ReadAllText (outputFile); + StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); + StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); + StringAssert.Contains ("-keep class other.Type { *; }", proguard); + } + GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir, IList? messages = null, IList? warnings = null, string tfv = "v11.0") { From 2b56fb0d4768d4d6c94109a7e0b43e8036fd5f01 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Jul 2026 09:56:24 +0200 Subject: [PATCH 11/14] [TrimmableTypeMap] Route typemap dependencies through a property Route _GenerateTrimmableTypeMap through _GenerateTrimmableTypeMapDependsOn so NativeAOT-specific setup dependencies can be composed with _GetLibraryImports instead of hard-coding one dependency list. This also makes the overlap with the runtime-only assembly scan PR explicit, avoiding silent semantic loss when the branches are merged in either order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 f6fb61e4b85..07aff00daad 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 @@ -44,6 +44,12 @@ stays incremental while still reacting to post-trim JCW regeneration. --> <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTrimmableTypeMapJavaStamp) <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' ">$(_TrimmableTypeMapOutputStamp) + + <_GenerateTrimmableTypeMapDependsOn Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">$(IlcDynamicBuildPropertyDependencies) + + <_GenerateTrimmableTypeMapDependsOn>$(_GenerateTrimmableTypeMapDependsOn);_GetLibraryImports @@ -98,7 +104,7 @@ From 7c2ce79a53b5873b038d51a5fc6e55ec3545521e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 4 Jul 2026 01:24:44 +0200 Subject: [PATCH 12/14] [Tests] BuildProguardEnabledProject: check references.cfg on NativeAOT On the trimmable NativeAOT path R8 consolidates the ACW keep rules into the per-RID proguard_project_references.cfg (UseTrimmableNativeAotProguardConfiguration); proguard_project_primary.cfg is intentionally left as a comment. The test asserted the app's MainActivity keep in proguard_project_primary.cfg, which only holds on the legacy/CoreCLR path. For NativeAOT, look for the keep in the generated proguard_project_references.cfg instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index ea79e9bd883..9abbf9079e8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -1636,9 +1636,19 @@ public void BuildProguardEnabledProject ([Values ("", "android-arm64")] string r } var toolbar_class = "androidx.appcompat.widget.Toolbar"; - var proguardProjectPrimary = Path.Combine (intermediate, "proguard", "proguard_project_primary.cfg"); - FileAssert.Exists (proguardProjectPrimary); - Assert.IsTrue (StringAssertEx.ContainsText (File.ReadAllLines (proguardProjectPrimary), $"-keep class {proj.JavaPackageName}.MainActivity"), $"`{proj.JavaPackageName}.MainActivity` should exist in `proguard_project_primary.cfg`!"); + if (runtime == AndroidRuntime.NativeAOT) { + // On the trimmable NativeAOT path R8 consolidates the ACW keep rules into the per-RID + // proguard_project_references.cfg (see R8.UseTrimmableNativeAotProguardConfiguration); + // proguard_project_primary.cfg is intentionally left as just a comment. + var referencesFiles = Directory.GetFiles (Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath), "proguard_project_references.cfg", SearchOption.AllDirectories); + Assert.IsNotEmpty (referencesFiles, "proguard_project_references.cfg should have been generated."); + Assert.IsTrue (referencesFiles.Any (f => StringAssertEx.ContainsText (File.ReadAllLines (f), $"-keep class {proj.JavaPackageName}.MainActivity")), + $"`{proj.JavaPackageName}.MainActivity` should exist in a `proguard_project_references.cfg`!"); + } else { + var proguardProjectPrimary = Path.Combine (intermediate, "proguard", "proguard_project_primary.cfg"); + FileAssert.Exists (proguardProjectPrimary); + Assert.IsTrue (StringAssertEx.ContainsText (File.ReadAllLines (proguardProjectPrimary), $"-keep class {proj.JavaPackageName}.MainActivity"), $"`{proj.JavaPackageName}.MainActivity` should exist in `proguard_project_primary.cfg`!"); + } var aapt_rules = Path.Combine (intermediate, "aapt_rules.txt"); FileAssert.Exists (aapt_rules); From 6f3a76cb3c15c2372f4282ad770c314f6427d808 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Jul 2026 11:24:53 +0200 Subject: [PATCH 13/14] [Tests] Check exact NativeAOT proguard config path Tighten BuildProguardEnabledProject so the NativeAOT case checks the expected proguard/proguard_project_references.cfg path directly instead of recursively searching the intermediate directory. The ACW keep rules intentionally live in references.cfg on the trimmable NativeAOT path because R8 owns proguard_project_primary.cfg for app/user-Java keeps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 9abbf9079e8..028e3b55d6f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -1636,19 +1636,11 @@ public void BuildProguardEnabledProject ([Values ("", "android-arm64")] string r } var toolbar_class = "androidx.appcompat.widget.Toolbar"; - if (runtime == AndroidRuntime.NativeAOT) { - // On the trimmable NativeAOT path R8 consolidates the ACW keep rules into the per-RID - // proguard_project_references.cfg (see R8.UseTrimmableNativeAotProguardConfiguration); - // proguard_project_primary.cfg is intentionally left as just a comment. - var referencesFiles = Directory.GetFiles (Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath), "proguard_project_references.cfg", SearchOption.AllDirectories); - Assert.IsNotEmpty (referencesFiles, "proguard_project_references.cfg should have been generated."); - Assert.IsTrue (referencesFiles.Any (f => StringAssertEx.ContainsText (File.ReadAllLines (f), $"-keep class {proj.JavaPackageName}.MainActivity")), - $"`{proj.JavaPackageName}.MainActivity` should exist in a `proguard_project_references.cfg`!"); - } else { - var proguardProjectPrimary = Path.Combine (intermediate, "proguard", "proguard_project_primary.cfg"); - FileAssert.Exists (proguardProjectPrimary); - Assert.IsTrue (StringAssertEx.ContainsText (File.ReadAllLines (proguardProjectPrimary), $"-keep class {proj.JavaPackageName}.MainActivity"), $"`{proj.JavaPackageName}.MainActivity` should exist in `proguard_project_primary.cfg`!"); - } + var proguardProjectConfiguration = Path.Combine (intermediate, "proguard", + runtime == AndroidRuntime.NativeAOT ? "proguard_project_references.cfg" : "proguard_project_primary.cfg"); + FileAssert.Exists (proguardProjectConfiguration); + Assert.IsTrue (StringAssertEx.ContainsText (File.ReadAllLines (proguardProjectConfiguration), $"-keep class {proj.JavaPackageName}.MainActivity"), + $"`{proj.JavaPackageName}.MainActivity` should exist in `{proguardProjectConfiguration}`!"); var aapt_rules = Path.Combine (intermediate, "aapt_rules.txt"); FileAssert.Exists (aapt_rules); From 362a129142d642c27401c8ab20cf7fb868414b91 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Jul 2026 15:02:40 +0200 Subject: [PATCH 14/14] [Tests] Check NativeAOT proguard config per RID When BuildProguardEnabledProject runs NativeAOT without an explicit RuntimeIdentifier, the SDK builds the default RuntimeIdentifiers and writes the generated proguard_project_references.cfg under each RID-nested intermediate path. Check those exact files instead of the flat outer intermediate path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 028e3b55d6f..3f9f13cece6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -1636,11 +1636,17 @@ public void BuildProguardEnabledProject ([Values ("", "android-arm64")] string r } var toolbar_class = "androidx.appcompat.widget.Toolbar"; - var proguardProjectConfiguration = Path.Combine (intermediate, "proguard", - runtime == AndroidRuntime.NativeAOT ? "proguard_project_references.cfg" : "proguard_project_primary.cfg"); - FileAssert.Exists (proguardProjectConfiguration); - Assert.IsTrue (StringAssertEx.ContainsText (File.ReadAllLines (proguardProjectConfiguration), $"-keep class {proj.JavaPackageName}.MainActivity"), - $"`{proj.JavaPackageName}.MainActivity` should exist in `{proguardProjectConfiguration}`!"); + IEnumerable proguardProjectConfigurations = [Path.Combine (intermediate, "proguard", + runtime == AndroidRuntime.NativeAOT ? "proguard_project_references.cfg" : "proguard_project_primary.cfg")]; + if (runtime == AndroidRuntime.NativeAOT && string.IsNullOrEmpty (rid)) { + proguardProjectConfigurations = proj.GetRuntimeIdentifiers () + .Select (r => Path.Combine (intermediate, r, "proguard", "proguard_project_references.cfg")); + } + foreach (var proguardProjectConfiguration in proguardProjectConfigurations) { + FileAssert.Exists (proguardProjectConfiguration); + Assert.IsTrue (StringAssertEx.ContainsText (File.ReadAllLines (proguardProjectConfiguration), $"-keep class {proj.JavaPackageName}.MainActivity"), + $"`{proj.JavaPackageName}.MainActivity` should exist in `{proguardProjectConfiguration}`!"); + } var aapt_rules = Path.Combine (intermediate, "aapt_rules.txt"); FileAssert.Exists (aapt_rules);