From 99b1b3c01da32eb94572de47aee26717446de5ca Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 12:30:51 +0100 Subject: [PATCH 01/14] [TrimmableTypeMap] Root manifest-referenced types as unconditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse the user's AndroidManifest.xml template for activity, service, receiver, and provider elements with android:name attributes. Mark matching scanned Java peer types as IsUnconditional = true so the ILLink TypeMap step preserves them even if no managed code references them directly. Changes: - JavaPeerInfo.IsUnconditional: init → set (must be mutated after scanning) - TrimmableTypeMapGenerator: add warn callback, RootManifestReferencedTypes() called between scanning and typemap generation - GenerateTrimmableTypeMap task: pass Log.LogWarning as warn callback - 4 new xUnit tests covering rooting, unresolved warnings, already-unconditional skip, and empty manifest Replaces #11016 (closed — depended on old PR shape with TaskLoggingHelper). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ITrimmableTypeMapLogger.cs | 2 + .../Scanner/JavaPeerInfo.cs | 4 +- .../TrimmableTypeMapGenerator.cs | 57 +++++++++ .../Tasks/GenerateTrimmableTypeMap.cs | 5 + .../TrimmableTypeMapGeneratorTests.cs | 119 +++++++++++++++++- 5 files changed, 185 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index ea1cd664a96..94e046c732f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -10,4 +10,6 @@ public interface ITrimmableTypeMapLogger void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount); void LogGeneratedTypeMapAssembliesInfo (int assemblyCount); void LogGeneratedJcwFilesInfo (int sourceCount); + void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName); + void LogManifestReferencedTypeNotFoundWarning (string javaTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index aed62453042..fb76f9e2bd9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -69,8 +69,10 @@ public sealed record JavaPeerInfo /// Types with component attributes ([Activity], [Service], etc.), /// custom views from layout XML, or manifest-declared components /// are unconditionally preserved (not trimmable). + /// May be set after scanning when the manifest references a type + /// that the scanner did not mark as unconditional. /// - public bool IsUnconditional { get; init; } + public bool IsUnconditional { get; set; } /// /// True for Application and Instrumentation types. These types cannot call diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 0697f31ea54..e12065cde94 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -38,6 +38,10 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult ([], [], allPeers); } + if (manifestTemplate is not null) { + RootManifestReferencedTypes (allPeers, manifestTemplate); + } + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) @@ -139,4 +143,57 @@ List GenerateJcwJavaSources (List allPeers) logger.LogGeneratedJcwFilesInfo (sources.Count); return sources.ToList (); } + + internal void RootManifestReferencedTypes (List allPeers, XDocument doc) + { + var root = doc.Root; + if (root is null) { + return; + } + + XNamespace androidNs = "http://schemas.android.com/apk/res/android"; + XName attName = androidNs + "name"; + + var componentNames = new HashSet (StringComparer.Ordinal); + foreach (var element in root.Descendants ()) { + switch (element.Name.LocalName) { + case "activity": + case "service": + case "receiver": + case "provider": + var name = (string?) element.Attribute (attName); + if (name is not null) { + componentNames.Add (name); + } + break; + } + } + + if (componentNames.Count == 0) { + return; + } + + var peersByDotName = new Dictionary> (StringComparer.Ordinal); + foreach (var peer in allPeers) { + var dotName = peer.JavaName.Replace ('/', '.').Replace ('$', '.'); + if (!peersByDotName.TryGetValue (dotName, out var list)) { + list = []; + peersByDotName [dotName] = list; + } + list.Add (peer); + } + + foreach (var name in componentNames) { + if (peersByDotName.TryGetValue (name, out var peers)) { + foreach (var peer in peers) { + if (!peer.IsUnconditional) { + peer.IsUnconditional = true; + logger.LogRootingManifestReferencedTypeInfo (name, peer.ManagedTypeName); + } + } + } else { + logger.LogManifestReferencedTypeNotFoundWarning (name); + } + } + } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 715eac0b495..53880ad4cd9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -33,6 +33,10 @@ public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => log.LogMessage (MessageImportance.Low, $"Generated {assemblyCount} typemap assemblies."); public void LogGeneratedJcwFilesInfo (int sourceCount) => log.LogMessage (MessageImportance.Low, $"Generated {sourceCount} JCW Java source files."); + public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) => + log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); + public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => + log.LogWarning ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type."); } public override string TaskPrefix => "GTT"; @@ -114,6 +118,7 @@ public override bool RunTask () ApplicationJavaClass: ApplicationJavaClass); } + var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log)); var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log)); XDocument? manifestTemplate = null; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index c07340ae981..8e726e02890 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -12,7 +12,7 @@ public class TrimmableTypeMapGeneratorTests : FixtureTestBase { readonly List logMessages = new (); - sealed class TestTrimmableTypeMapLogger (List logMessages) : ITrimmableTypeMapLogger + sealed class TestTrimmableTypeMapLogger (List logMessages, List? warnings = null) : ITrimmableTypeMapLogger { public void LogNoJavaPeerTypesFound () => logMessages.Add ("No Java peer types found, skipping typemap generation."); @@ -30,6 +30,10 @@ public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => logMessages.Add ($"Generated {assemblyCount} typemap assemblies."); public void LogGeneratedJcwFilesInfo (int sourceCount) => logMessages.Add ($"Generated {sourceCount} JCW Java source files."); + public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) => + logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); + public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => + warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type."); } [Fact] @@ -101,6 +105,119 @@ public void Execute_JavaSourcesHaveCorrectStructure () TrimmableTypeMapGenerator CreateGenerator () => new (new TestTrimmableTypeMapLogger (logMessages)); + TrimmableTypeMapGenerator CreateGenerator (List warnings) => + new (new TestTrimmableTypeMapLogger (logMessages, warnings)); + + [Fact] + public void RootManifestReferencedTypes_RootsMatchingPeers () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "com/example/MyService", CompatJniName = "com.example.MyService", + ManagedTypeName = "MyApp.MyService", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyService", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "MyActivity should be rooted as unconditional."); + Assert.False (peers [1].IsUnconditional, "MyService should remain conditional."); + Assert.Contains (logMessages, m => m.Contains ("Rooting manifest-referenced type")); + } + + [Fact] + public void RootManifestReferencedTypes_WarnsForUnresolvedTypes () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var warnings = new List (); + var generator = CreateGenerator (warnings); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.Contains (warnings, w => w.Contains ("com.example.NonExistentService")); + } + + [Fact] + public void RootManifestReferencedTypes_SkipsAlreadyUnconditional () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", IsUnconditional = true, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional); + Assert.DoesNotContain (logMessages, m => m.Contains ("Rooting manifest-referenced type")); + } + + [Fact] + public void RootManifestReferencedTypes_EmptyManifest_NoChanges () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.False (peers [0].IsUnconditional); + } + static PEReader CreateTestFixturePEReader () { var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) From 57a56872a72d36829ec5916684369239e65a5be9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 31 Mar 2026 18:13:22 +0200 Subject: [PATCH 02/14] Address PR review: fix manifest name matching and null guard - Fix RootManifestReferencedTypes to resolve relative android:name values (.MyActivity, MyActivity) using manifest package attribute - Keep $ separator in peer lookup keys so nested types (Outer$Inner) match correctly against manifest class names - Guard Path.GetDirectoryName against null return for acw-map path - Fix pre-existing compilation error: load XDocument from template path before passing to ManifestGenerator.Generate - Add tests for relative name resolution and nested type matching Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGeneratorTests.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 8e726e02890..1b756457acd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -218,6 +218,65 @@ public void RootManifestReferencedTypes_EmptyManifest_NoChanges () Assert.False (peers [0].IsUnconditional); } + [Fact] + public void RootManifestReferencedTypes_ResolvesRelativeNames () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "com/example/MyService", CompatJniName = "com.example.MyService", + ManagedTypeName = "MyApp.MyService", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyService", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "Dot-relative name '.MyActivity' should resolve to com.example.MyActivity."); + Assert.True (peers [1].IsUnconditional, "Simple name 'MyService' should resolve to com.example.MyService."); + } + + [Fact] + public void RootManifestReferencedTypes_MatchesNestedTypes () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/Outer$Inner", CompatJniName = "com.example.Outer$Inner", + ManagedTypeName = "MyApp.Outer.Inner", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "Inner", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "Nested type 'Outer$Inner' should be matched using '$' separator."); + } + static PEReader CreateTestFixturePEReader () { var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) From f53f676881dd8622d6c670d91b13c10a07a06b9e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 1 Apr 2026 17:21:54 +0200 Subject: [PATCH 03/14] Document IsUnconditional mutation contract: only set to true Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerInfo.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index fb76f9e2bd9..3907525d2e4 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -69,8 +69,9 @@ public sealed record JavaPeerInfo /// Types with component attributes ([Activity], [Service], etc.), /// custom views from layout XML, or manifest-declared components /// are unconditionally preserved (not trimmable). - /// May be set after scanning when the manifest references a type - /// that the scanner did not mark as unconditional. + /// May be set to true after scanning when the manifest references a type + /// that the scanner did not mark as unconditional. Should only ever be set + /// to true, never back to false. /// public bool IsUnconditional { get; set; } From 4937f44184319b12dfef77576c25ba6fc4d71980 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 1 Apr 2026 20:41:26 +0200 Subject: [PATCH 04/14] Fix RootManifestReferencedTypes: remove IO, add relative name resolution - Remove file-path overload (IO belongs in MSBuild task, not generator) - Accept XDocument? directly, handle null with pattern match - Add ResolveManifestClassName to resolve dot-relative (.MyActivity) and simple (MyService) names against the manifest package attribute - Fix '$' handling in peer lookup: manifests use '$' for nested types, don't replace it with '.' in the lookup key Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index e12065cde94..a021a8144ea 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -38,9 +38,7 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult ([], [], allPeers); } - if (manifestTemplate is not null) { - RootManifestReferencedTypes (allPeers, manifestTemplate); - } + RootManifestReferencedTypes (allPeers, manifestTemplate); var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion); var jcwPeers = allPeers.Where (p => @@ -144,15 +142,15 @@ List GenerateJcwJavaSources (List allPeers) return sources.ToList (); } - internal void RootManifestReferencedTypes (List allPeers, XDocument doc) + internal void RootManifestReferencedTypes (List allPeers, XDocument? doc) { - var root = doc.Root; - if (root is null) { + if (doc?.Root is not { } root) { return; } XNamespace androidNs = "http://schemas.android.com/apk/res/android"; XName attName = androidNs + "name"; + var packageName = (string?) root.Attribute ("package") ?? ""; var componentNames = new HashSet (StringComparer.Ordinal); foreach (var element in root.Descendants ()) { @@ -163,7 +161,7 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen case "provider": var name = (string?) element.Attribute (attName); if (name is not null) { - componentNames.Add (name); + componentNames.Add (ResolveManifestClassName (name, packageName)); } break; } @@ -173,9 +171,10 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen return; } + // Build lookup by dot-name, keeping '$' for nested types (manifests use '$' too). var peersByDotName = new Dictionary> (StringComparer.Ordinal); foreach (var peer in allPeers) { - var dotName = peer.JavaName.Replace ('/', '.').Replace ('$', '.'); + var dotName = peer.JavaName.Replace ('/', '.'); if (!peersByDotName.TryGetValue (dotName, out var list)) { list = []; peersByDotName [dotName] = list; @@ -196,4 +195,22 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen } } } + + /// + /// Resolves an android:name value to a fully-qualified class name. + /// Names starting with '.' are relative to the package. Names with no '.' at all + /// are also treated as relative (Android tooling convention). + /// + static string ResolveManifestClassName (string name, string packageName) + { + if (name.StartsWith (".", StringComparison.Ordinal)) { + return packageName + name; + } + + if (name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty ()) { + return packageName + "." + name; + } + + return name; + } } From 827da0d76f2dd0388777133bbcadc8c7328fbfb6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 08:33:24 +0200 Subject: [PATCH 05/14] [TrimmableTypeMap] Root Application and Instrumentation types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 2 ++ .../TrimmableTypeMapGeneratorTests.cs | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index a021a8144ea..45891d7413d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -155,7 +155,9 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen var componentNames = new HashSet (StringComparer.Ordinal); foreach (var element in root.Descendants ()) { switch (element.Name.LocalName) { + case "application": case "activity": + case "instrumentation": case "service": case "receiver": case "provider": diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 1b756457acd..4477c965bdf 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -141,6 +141,37 @@ public void RootManifestReferencedTypes_RootsMatchingPeers () Assert.Contains (logMessages, m => m.Contains ("Rooting manifest-referenced type")); } + [Fact] + public void RootManifestReferencedTypes_RootsApplicationAndInstrumentationTypes () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyApplication", CompatJniName = "com.example.MyApplication", + ManagedTypeName = "MyApp.MyApplication", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyApplication", + AssemblyName = "MyApp", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "com/example/MyInstrumentation", CompatJniName = "com.example.MyInstrumentation", + ManagedTypeName = "MyApp.MyInstrumentation", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyInstrumentation", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "Application type should be rooted from ."); + Assert.True (peers [1].IsUnconditional, "Instrumentation type should be rooted from ."); + } + [Fact] public void RootManifestReferencedTypes_WarnsForUnresolvedTypes () { From 7b6b48b39dec7484aeddad77879dfb49ae8512d0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 12:33:40 +0200 Subject: [PATCH 06/14] [TrimmableTypeMap] Match compat names in manifest rooting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 39 ++++++++++++++++--- .../TrimmableTypeMapGeneratorTests.cs | 26 +++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 45891d7413d..a0e302f6334 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -173,15 +173,19 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen return; } - // Build lookup by dot-name, keeping '$' for nested types (manifests use '$' too). + // Build lookup by both Java and compat dot-names. Keep '$' for nested types, + // because manifests commonly use '$', but also include the Java source form. var peersByDotName = new Dictionary> (StringComparer.Ordinal); foreach (var peer in allPeers) { - var dotName = peer.JavaName.Replace ('/', '.'); - if (!peersByDotName.TryGetValue (dotName, out var list)) { - list = []; - peersByDotName [dotName] = list; + var dotName = GetManifestLookupName (peer.JavaName); + AddPeerByDotName (peersByDotName, dotName, peer); + AddJavaSourceLookupName (peersByDotName, dotName, peer); + + var compatDotName = GetManifestLookupName (peer.CompatJniName); + if (compatDotName != dotName) { + AddPeerByDotName (peersByDotName, compatDotName, peer); + AddJavaSourceLookupName (peersByDotName, compatDotName, peer); } - list.Add (peer); } foreach (var name in componentNames) { @@ -198,6 +202,29 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen } } + static void AddPeerByDotName (Dictionary> peersByDotName, string dotName, JavaPeerInfo peer) + { + if (!peersByDotName.TryGetValue (dotName, out var list)) { + list = []; + peersByDotName [dotName] = list; + } + + list.Add (peer); + } + + static void AddJavaSourceLookupName (Dictionary> peersByDotName, string dotName, JavaPeerInfo peer) + { + var javaSourceName = dotName.Replace ('$', '.'); + if (javaSourceName != dotName) { + AddPeerByDotName (peersByDotName, javaSourceName, peer); + } + } + + static string GetManifestLookupName (string jniName) + { + return jniName.Replace ('/', '.'); + } + /// /// Resolves an android:name value to a fully-qualified class name. /// Names starting with '.' are relative to the package. Names with no '.' at all diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 4477c965bdf..dacfe17cc65 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -282,6 +282,32 @@ public void RootManifestReferencedTypes_ResolvesRelativeNames () Assert.True (peers [1].IsUnconditional, "Simple name 'MyService' should resolve to com.example.MyService."); } + [Fact] + public void RootManifestReferencedTypes_MatchesCompatNames () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "crc64123456789abc/MyActivity", CompatJniName = "my/app/MyActivity", + ManagedTypeName = "My.App.MyActivity", ManagedTypeNamespace = "My.App", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "Relative manifest name should match CompatJniName when JavaName uses a CRC64 package."); + } + [Fact] public void RootManifestReferencedTypes_MatchesNestedTypes () { From ccb0cfd2330a9a10923b6d51a066e9e9a0fe3e86 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 12:44:20 +0200 Subject: [PATCH 07/14] [TrimmableTypeMap] Add coded warning for unresolved types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/index.md | 1 + Documentation/docs-mobile/messages/xa4250.md | 33 +++++++++++++++++++ .../Properties/Resources.Designer.cs | 9 +++++ .../Properties/Resources.resx | 5 +++ .../Tasks/GenerateTrimmableTypeMap.cs | 3 +- 5 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 Documentation/docs-mobile/messages/xa4250.md diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 0ef4f1febfa..ae77f286553 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -207,6 +207,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4247](xa4247.md): Could not resolve POM file for artifact '{artifact}'. + [XA4248](xa4248.md): Could not find NuGet package '{nugetId}' version '{version}' in lock file. Ensure NuGet Restore has run since this `` was added. + [XA4235](xa4249.md): Maven artifact specification '{artifact}' is invalid. The correct format is 'group_id:artifact_id:version'. ++ [XA4250](xa4250.md): Manifest-referenced type '{type}' was not found in any scanned assembly. It may be a framework type. + 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/xa4250.md b/Documentation/docs-mobile/messages/xa4250.md new file mode 100644 index 00000000000..3ef74ddc5e5 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4250.md @@ -0,0 +1,33 @@ +--- +title: .NET for Android warning XA4250 +description: XA4250 warning code +ms.date: 04/07/2026 +f1_keywords: + - "XA4250" +--- + +# .NET for Android warning XA4250 + +## Example message + +Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type. + +```text +warning XA4250: Manifest-referenced type 'com.example.MainActivity' was not found in any scanned assembly. It may be a framework type. +``` + +## Issue + +The build found a type name in `AndroidManifest.xml`, but it could not match that name to any Java peer discovered in the app's managed assemblies. + +This can be expected for framework-provided types, but it can also indicate that the manifest entry does not match the name generated for a managed Android component. + +## Solution + +If the manifest entry refers to an Android framework type, this warning can usually be ignored. + +Otherwise: + +1. Verify the `android:name` value in the manifest. +2. Ensure the managed type is included in the app build. +3. Check for namespace, `[Register]`, or nested-type naming mismatches between the manifest and the managed type. diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index bc2d9a7d4d4..c278679f4ee 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1427,6 +1427,15 @@ public static string XA4249 { } } + /// + /// Looks up a localized string similar to Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type.. + /// + public static string XA4250 { + get { + return ResourceManager.GetString("XA4250", 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 d1ee02dc051..ca75ec196de 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1068,6 +1068,11 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS Maven artifact specification '{0}' is invalid. The correct format is 'group_id:artifact_id:version'. The following are literal names and should not be translated: Maven, group_id, artifact_id {0} - A Maven artifact specification + + + Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type. + The following are literal names and should not be translated: Manifest, framework. +{0} - Java type name from AndroidManifest.xml Command '{0}' failed.\n{1} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 53880ad4cd9..120feb3c59a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -36,7 +36,7 @@ public void LogGeneratedJcwFilesInfo (int sourceCount) => public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) => log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => - log.LogWarning ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type."); + log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName); } public override string TaskPrefix => "GTT"; @@ -118,7 +118,6 @@ public override bool RunTask () ApplicationJavaClass: ApplicationJavaClass); } - var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log)); var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log)); XDocument? manifestTemplate = null; From 0339fb599c075e25544e31d8785c85bdb2020825 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 12:52:36 +0200 Subject: [PATCH 08/14] [TrimmableTypeMap] Track manifest-related target inputs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 39 ++++++++++++++++++- 1 file changed, 38 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 d38aad44fb9..a95b342a940 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 @@ -15,6 +15,7 @@ <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java + <_TrimmableTypeMapInputsCacheFile>$(_TypeMapBaseOutputDir)trimmable-typemap.inputs @@ -33,6 +34,41 @@ + + + <_TrimmableTypeMapInputs Include="$(TargetFrameworkVersion)" /> + <_TrimmableTypeMapInputs Include="$(_AndroidPackage)" /> + <_TrimmableTypeMapInputs Include="$(_ApplicationLabel)" /> + <_TrimmableTypeMapInputs Include="$(_AndroidVersionCode)" /> + <_TrimmableTypeMapInputs Include="$(_AndroidVersionName)" /> + <_TrimmableTypeMapInputs Include="$(_AndroidApiLevel)" /> + <_TrimmableTypeMapInputs Include="$(SupportedOSPlatformVersion)" /> + <_TrimmableTypeMapInputs Include="$(_TrimmableRuntimeProviderJavaName)" /> + <_TrimmableTypeMapInputs Include="$(AndroidIncludeDebugSymbols)" /> + <_TrimmableTypeMapInputs Include="$(AndroidNeedsInternetPermission)" /> + <_TrimmableTypeMapInputs Include="$(EmbedAssembliesIntoApk)" /> + <_TrimmableTypeMapInputs Include="$(AndroidManifestPlaceholders)" /> + <_TrimmableTypeMapInputs Include="$(_AndroidCheckedBuild)" /> + <_TrimmableTypeMapInputs Include="$(AndroidApplicationJavaClass)" /> + + + + + + + + + + + + + From f837d9a68ca5bbe7c9413ad3fe3b8f9aa02e7f6b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 13:04:23 +0200 Subject: [PATCH 09/14] [TrimmableTypeMap] Use switch expression for manifest names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index a0e302f6334..40a4696362c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -232,14 +232,10 @@ static string GetManifestLookupName (string jniName) /// static string ResolveManifestClassName (string name, string packageName) { - if (name.StartsWith (".", StringComparison.Ordinal)) { - return packageName + name; - } - - if (name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty ()) { - return packageName + "." + name; - } - - return name; + return name switch { + _ when name.StartsWith (".", StringComparison.Ordinal) => packageName + name, + _ when name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty () => packageName + "." + name, + _ => name, + }; } } From 1bc4cff4b58ba07c6f251d368751d6ee3c9b4c26 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 15:25:16 +0200 Subject: [PATCH 10/14] Revert "[TrimmableTypeMap] Track manifest-related target inputs" Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 39 +------------------ 1 file changed, 1 insertion(+), 38 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 a95b342a940..d38aad44fb9 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 @@ -15,7 +15,6 @@ <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java - <_TrimmableTypeMapInputsCacheFile>$(_TypeMapBaseOutputDir)trimmable-typemap.inputs @@ -34,41 +33,6 @@ - - - <_TrimmableTypeMapInputs Include="$(TargetFrameworkVersion)" /> - <_TrimmableTypeMapInputs Include="$(_AndroidPackage)" /> - <_TrimmableTypeMapInputs Include="$(_ApplicationLabel)" /> - <_TrimmableTypeMapInputs Include="$(_AndroidVersionCode)" /> - <_TrimmableTypeMapInputs Include="$(_AndroidVersionName)" /> - <_TrimmableTypeMapInputs Include="$(_AndroidApiLevel)" /> - <_TrimmableTypeMapInputs Include="$(SupportedOSPlatformVersion)" /> - <_TrimmableTypeMapInputs Include="$(_TrimmableRuntimeProviderJavaName)" /> - <_TrimmableTypeMapInputs Include="$(AndroidIncludeDebugSymbols)" /> - <_TrimmableTypeMapInputs Include="$(AndroidNeedsInternetPermission)" /> - <_TrimmableTypeMapInputs Include="$(EmbedAssembliesIntoApk)" /> - <_TrimmableTypeMapInputs Include="$(AndroidManifestPlaceholders)" /> - <_TrimmableTypeMapInputs Include="$(_AndroidCheckedBuild)" /> - <_TrimmableTypeMapInputs Include="$(AndroidApplicationJavaClass)" /> - - - - - - - - - - - - - From 7413d54da0a90def25ea0ed4051f76b5d0283908 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 15:47:41 +0200 Subject: [PATCH 11/14] [TrimmableTypeMap] Merge manifest matching tests into theory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGeneratorTests.cs | 117 ++++-------------- 1 file changed, 21 insertions(+), 96 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index dacfe17cc65..1083258ea15 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -108,27 +108,37 @@ public void Execute_JavaSourcesHaveCorrectStructure () TrimmableTypeMapGenerator CreateGenerator (List warnings) => new (new TestTrimmableTypeMapLogger (logMessages, warnings)); - [Fact] - public void RootManifestReferencedTypes_RootsMatchingPeers () + [Theory] + [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example.MyActivity", "activity", "com.example.MyActivity")] + [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] + [InlineData ("com/example/MyService", "com.example.MyService", "com.example", "service", "MyService")] + [InlineData ("crc64123456789abc/MyActivity", "my/app/MyActivity", "my.app", "activity", ".MyActivity")] + [InlineData ("com/example/Outer$Inner", "com.example.Outer$Inner", "com.example", "activity", "com.example.Outer$Inner")] + public void RootManifestReferencedTypes_RootsManifestReferencedTypes ( + string javaName, + string compatJniName, + string packageName, + string elementName, + string manifestName) { var peers = new List { new JavaPeerInfo { - JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", - ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + JavaName = javaName, CompatJniName = compatJniName, + ManagedTypeName = "MyApp.MyTarget", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyTarget", AssemblyName = "MyApp", IsUnconditional = false, }, new JavaPeerInfo { - JavaName = "com/example/MyService", CompatJniName = "com.example.MyService", - ManagedTypeName = "MyApp.MyService", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyService", + JavaName = "com/example/OtherType", CompatJniName = "com.example.OtherType", + ManagedTypeName = "MyApp.OtherType", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "OtherType", AssemblyName = "MyApp", IsUnconditional = false, }, }; - var doc = System.Xml.Linq.XDocument.Parse (""" + var doc = System.Xml.Linq.XDocument.Parse ($$""" - + - + <{{elementName}} android:name="{{manifestName}}" /> """); @@ -136,8 +146,8 @@ public void RootManifestReferencedTypes_RootsMatchingPeers () var generator = CreateGenerator (); generator.RootManifestReferencedTypes (peers, doc); - Assert.True (peers [0].IsUnconditional, "MyActivity should be rooted as unconditional."); - Assert.False (peers [1].IsUnconditional, "MyService should remain conditional."); + Assert.True (peers [0].IsUnconditional, "The manifest-referenced type should be rooted as unconditional."); + Assert.False (peers [1].IsUnconditional, "Non-matching peers should remain conditional."); Assert.Contains (logMessages, m => m.Contains ("Rooting manifest-referenced type")); } @@ -249,91 +259,6 @@ public void RootManifestReferencedTypes_EmptyManifest_NoChanges () Assert.False (peers [0].IsUnconditional); } - [Fact] - public void RootManifestReferencedTypes_ResolvesRelativeNames () - { - var peers = new List { - new JavaPeerInfo { - JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", - ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", - AssemblyName = "MyApp", IsUnconditional = false, - }, - new JavaPeerInfo { - JavaName = "com/example/MyService", CompatJniName = "com.example.MyService", - ManagedTypeName = "MyApp.MyService", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyService", - AssemblyName = "MyApp", IsUnconditional = false, - }, - }; - - var doc = System.Xml.Linq.XDocument.Parse (""" - - - - - - - - """); - - var generator = CreateGenerator (); - generator.RootManifestReferencedTypes (peers, doc); - - Assert.True (peers [0].IsUnconditional, "Dot-relative name '.MyActivity' should resolve to com.example.MyActivity."); - Assert.True (peers [1].IsUnconditional, "Simple name 'MyService' should resolve to com.example.MyService."); - } - - [Fact] - public void RootManifestReferencedTypes_MatchesCompatNames () - { - var peers = new List { - new JavaPeerInfo { - JavaName = "crc64123456789abc/MyActivity", CompatJniName = "my/app/MyActivity", - ManagedTypeName = "My.App.MyActivity", ManagedTypeNamespace = "My.App", ManagedTypeShortName = "MyActivity", - AssemblyName = "MyApp", IsUnconditional = false, - }, - }; - - var doc = System.Xml.Linq.XDocument.Parse (""" - - - - - - - """); - - var generator = CreateGenerator (); - generator.RootManifestReferencedTypes (peers, doc); - - Assert.True (peers [0].IsUnconditional, "Relative manifest name should match CompatJniName when JavaName uses a CRC64 package."); - } - - [Fact] - public void RootManifestReferencedTypes_MatchesNestedTypes () - { - var peers = new List { - new JavaPeerInfo { - JavaName = "com/example/Outer$Inner", CompatJniName = "com.example.Outer$Inner", - ManagedTypeName = "MyApp.Outer.Inner", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "Inner", - AssemblyName = "MyApp", IsUnconditional = false, - }, - }; - - var doc = System.Xml.Linq.XDocument.Parse (""" - - - - - - - """); - - var generator = CreateGenerator (); - generator.RootManifestReferencedTypes (peers, doc); - - Assert.True (peers [0].IsUnconditional, "Nested type 'Outer$Inner' should be matched using '$' separator."); - } - static PEReader CreateTestFixturePEReader () { var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) From cac14e8fe6192f231d76486c823f0001155d9525 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 15:58:05 +0200 Subject: [PATCH 12/14] [TrimmableTypeMap] Fix theory package name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TrimmableTypeMapGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 1083258ea15..496d860eb80 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -109,7 +109,7 @@ TrimmableTypeMapGenerator CreateGenerator (List warnings) => new (new TestTrimmableTypeMapLogger (logMessages, warnings)); [Theory] - [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example.MyActivity", "activity", "com.example.MyActivity")] + [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", "com.example.MyActivity")] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] [InlineData ("com/example/MyService", "com.example.MyService", "com.example", "service", "MyService")] [InlineData ("crc64123456789abc/MyActivity", "my/app/MyActivity", "my.app", "activity", ".MyActivity")] From 116fa683140fa5db455a5463484b70a9dcae2a2a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 16:45:46 +0200 Subject: [PATCH 13/14] Fix manifest-rooting resolution and deferred registration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ManifestGenerator.cs | 11 +-- .../Scanner/JavaPeerInfo.cs | 5 +- .../TrimmableTypeMapGenerator.cs | 79 +++++++++++++++---- .../TrimmableTypeMapGeneratorTests.cs | 31 ++++++++ 4 files changed, 104 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 27806a679bd..5b2d6204eb5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -116,10 +116,7 @@ class ManifestGenerator } // Apply manifest placeholders - string? placeholders = ManifestPlaceholders; - if (placeholders is not null && placeholders.Length > 0) { - ApplyPlaceholders (doc, placeholders); - } + ApplyPlaceholders (doc, ManifestPlaceholders); return (doc, providerNames); } @@ -250,8 +247,12 @@ XElement CreateRuntimeProvider (string name, string? processName, int initOrder) /// Replaces ${key} placeholders in all attribute values throughout the document. /// Placeholder format: "key1=value1;key2=value2" /// - static void ApplyPlaceholders (XDocument doc, string placeholders) + internal static void ApplyPlaceholders (XDocument doc, string? placeholders) { + if (placeholders.IsNullOrEmpty ()) { + return; + } + var replacements = new Dictionary (StringComparer.Ordinal); foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) { var eqIndex = entry.IndexOf ('='); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 3907525d2e4..eff38fd1d51 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -80,8 +80,11 @@ public sealed record JavaPeerInfo /// registerNatives in their static initializer because the native library /// (libmonodroid.so) is not loaded until after the Application class is instantiated. /// Registration is deferred to ApplicationRegistration.registerApplications(). + /// This may also be set after scanning when a type is only discovered from + /// manifest android:name usage on <application> or + /// <instrumentation>. /// - public bool CannotRegisterInStaticConstructor { get; init; } + public bool CannotRegisterInStaticConstructor { get; set; } /// /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 40a4696362c..edb0171758f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -38,7 +38,7 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult ([], [], allPeers); } - RootManifestReferencedTypes (allPeers, manifestTemplate); + RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion); var jcwPeers = allPeers.Where (p => @@ -153,6 +153,7 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen var packageName = (string?) root.Attribute ("package") ?? ""; var componentNames = new HashSet (StringComparer.Ordinal); + var deferredRegistrationNames = new HashSet (StringComparer.Ordinal); foreach (var element in root.Descendants ()) { switch (element.Name.LocalName) { case "application": @@ -163,7 +164,12 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen case "provider": var name = (string?) element.Attribute (attName); if (name is not null) { - componentNames.Add (ResolveManifestClassName (name, packageName)); + var resolvedName = ResolveManifestClassName (name, packageName); + componentNames.Add (resolvedName); + + if (element.Name.LocalName is "application" or "instrumentation") { + deferredRegistrationNames.Add (resolvedName); + } } break; } @@ -177,20 +183,19 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen // because manifests commonly use '$', but also include the Java source form. var peersByDotName = new Dictionary> (StringComparer.Ordinal); foreach (var peer in allPeers) { - var dotName = GetManifestLookupName (peer.JavaName); - AddPeerByDotName (peersByDotName, dotName, peer); - AddJavaSourceLookupName (peersByDotName, dotName, peer); - - var compatDotName = GetManifestLookupName (peer.CompatJniName); - if (compatDotName != dotName) { - AddPeerByDotName (peersByDotName, compatDotName, peer); - AddJavaSourceLookupName (peersByDotName, compatDotName, peer); + AddJniLookupNames (peersByDotName, peer.JavaName, peer); + if (peer.CompatJniName != peer.JavaName) { + AddJniLookupNames (peersByDotName, peer.CompatJniName, peer); } } foreach (var name in componentNames) { if (peersByDotName.TryGetValue (name, out var peers)) { foreach (var peer in peers) { + if (deferredRegistrationNames.Contains (name)) { + peer.CannotRegisterInStaticConstructor = true; + } + if (!peer.IsUnconditional) { peer.IsUnconditional = true; logger.LogRootingManifestReferencedTypeInfo (name, peer.ManagedTypeName); @@ -212,17 +217,59 @@ static void AddPeerByDotName (Dictionary> peersByDotN list.Add (peer); } - static void AddJavaSourceLookupName (Dictionary> peersByDotName, string dotName, JavaPeerInfo peer) + static XDocument? PrepareManifestForRooting (XDocument? manifestTemplate, ManifestConfig? manifestConfig) { - var javaSourceName = dotName.Replace ('$', '.'); - if (javaSourceName != dotName) { - AddPeerByDotName (peersByDotName, javaSourceName, peer); + if (manifestTemplate is null && manifestConfig is null) { + return null; + } + + var doc = manifestTemplate is not null + ? new XDocument (manifestTemplate) + : new XDocument ( + new XElement ( + "manifest", + new XAttribute (XNamespace.Xmlns + "android", ManifestConstants.AndroidNs.NamespaceName))); + + if (doc.Root is not { } root) { + return doc; + } + + if (manifestConfig is null) { + return doc; + } + + if (((string?) root.Attribute ("package")).IsNullOrEmpty () && !manifestConfig.PackageName.IsNullOrEmpty ()) { + root.SetAttributeValue ("package", manifestConfig.PackageName); } + + ManifestGenerator.ApplyPlaceholders (doc, manifestConfig.ManifestPlaceholders); + + if (!manifestConfig.ApplicationJavaClass.IsNullOrEmpty ()) { + var app = root.Element ("application"); + if (app is null) { + app = new XElement ("application"); + root.Add (app); + } + + if (app.Attribute (ManifestConstants.AttName) is null) { + app.SetAttributeValue (ManifestConstants.AttName, manifestConfig.ApplicationJavaClass); + } + } + + return doc; } - static string GetManifestLookupName (string jniName) + static void AddJniLookupNames (Dictionary> peersByDotName, string jniName, JavaPeerInfo peer) { - return jniName.Replace ('/', '.'); + var simpleName = JniSignatureHelper.GetJavaSimpleName (jniName); + var packageName = JniSignatureHelper.GetJavaPackageName (jniName); + var manifestName = packageName.IsNullOrEmpty () ? simpleName : packageName + "." + simpleName; + AddPeerByDotName (peersByDotName, manifestName, peer); + + var javaSourceName = JniSignatureHelper.JniNameToJavaName (jniName); + if (javaSourceName != manifestName) { + AddPeerByDotName (peersByDotName, javaSourceName, peer); + } } /// diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 496d860eb80..54efaf3f6ef 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -103,6 +103,35 @@ public void Execute_JavaSourcesHaveCorrectStructure () Assert.Contains ("class ", source.Content); } + [Fact] + public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () + { + using var peReader = CreateTestFixturePEReader (); + var manifestTemplate = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var result = CreateGenerator ().Execute ( + new List<(string, PEReader)> { ("TestFixtures", peReader) }, + new Version (11, 0), + new HashSet (), + new ManifestConfig ( + PackageName: "my.app", + AndroidApiLevel: "35", + SupportedOSPlatformVersion: "21", + RuntimeProviderJavaName: "mono.MonoRuntimeProvider", + ManifestPlaceholders: "applicationId=my.app"), + manifestTemplate); + + var peer = result.AllPeers.First (p => p.ManagedTypeName == "MyApp.SimpleActivity"); + Assert.True (peer.IsUnconditional, "Relative manifest names should root correctly after placeholder substitution."); + } + TrimmableTypeMapGenerator CreateGenerator () => new (new TestTrimmableTypeMapLogger (logMessages)); TrimmableTypeMapGenerator CreateGenerator (List warnings) => @@ -180,6 +209,8 @@ public void RootManifestReferencedTypes_RootsApplicationAndInstrumentationTypes Assert.True (peers [0].IsUnconditional, "Application type should be rooted from ."); Assert.True (peers [1].IsUnconditional, "Instrumentation type should be rooted from ."); + Assert.True (peers [0].CannotRegisterInStaticConstructor, "Application type should defer Runtime.registerNatives()."); + Assert.True (peers [1].CannotRegisterInStaticConstructor, "Instrumentation type should defer Runtime.registerNatives()."); } [Fact] From 6f42c3b6fe3c5b44cb6a7041a3fc411d7782f94e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Apr 2026 17:06:46 +0200 Subject: [PATCH 14/14] Add host test for placeholder-based typemap rooting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMapTests.cs | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) 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 f76d95f246b..096fd7d9f34 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 @@ -123,11 +123,60 @@ public void Execute_ParsesTargetFrameworkVersion (string tfv) Assert.IsTrue (task.Execute (), $"Task should succeed with TargetFrameworkVersion='{tfv}'."); } + [Test] + public void Execute_ManifestPlaceholdersAreResolvedForRooting () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + var manifestTemplate = Path.Combine (Root, path, "AndroidManifest.xml"); + var mergedManifest = Path.Combine (Root, path, "obj", "android", "AndroidManifest.xml"); + var applicationRegistration = Path.Combine (Root, path, "src", "net", "dot", "android", "ApplicationRegistration.java"); + var warnings = new List (); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + var manifestDirectory = Path.GetDirectoryName (manifestTemplate); + if (manifestDirectory is null) { + Assert.Fail ("Could not determine manifest template directory."); + } + Directory.CreateDirectory (manifestDirectory); + File.WriteAllText (manifestTemplate, """ + + + + + + """); + + var task = CreateTask (new [] { monoAndroidItem }, outputDir, javaDir, warnings: warnings); + task.ManifestTemplate = manifestTemplate; + task.MergedAndroidManifestOutput = mergedManifest; + task.ApplicationRegistrationOutputFile = applicationRegistration; + task.PackageName = "android.app"; + task.AndroidApiLevel = "35"; + task.SupportedOSPlatformVersion = "21"; + task.RuntimeProviderJavaName = "mono.MonoRuntimeProvider"; + task.ManifestPlaceholders = "applicationId=android.app"; + + Assert.IsTrue (task.Execute (), "Task should succeed."); + FileAssert.Exists (applicationRegistration); + + var registrationText = File.ReadAllText (applicationRegistration); + StringAssert.Contains ("mono.android.Runtime.registerNatives (android.app.Application.class);", registrationText); + StringAssert.Contains ("mono.android.Runtime.registerNatives (android.app.Instrumentation.class);", registrationText); + Assert.IsFalse (warnings.Any (w => w.Code == "XA4250"), "Resolved placeholder-based manifest references should not log XA4250."); + } + GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir, - IList? messages = null, string tfv = "v11.0") + IList? messages = null, IList? warnings = null, string tfv = "v11.0") { return new GenerateTrimmableTypeMap { - BuildEngine = new MockBuildEngine (TestContext.Out, messages: messages), + BuildEngine = new MockBuildEngine (TestContext.Out, warnings: warnings, messages: messages), ResolvedAssemblies = assemblies, OutputDirectory = outputDir, JavaSourceOutputDirectory = javaDir,