From e588500b47e5608426f017eeb647603f9281e254 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 25 Mar 2026 14:30:35 +0100 Subject: [PATCH 1/2] Root manifest-referenced types as unconditional in trimmable path When users hand-edit AndroidManifest.xml with component entries that have no corresponding C# attribute (e.g. [Activity]), the referenced types may be trimmed by the linker, causing runtime crashes when Android tries to launch them. This change adds RootManifestReferencedTypes() which parses the manifest template before typemap assembly generation, finds matching peers by their Java name, and marks them as unconditional so the linker preserves them. A warning is logged for manifest-referenced types not found in any scanned assembly (likely framework types). Changes: - JavaPeerInfo.IsUnconditional: changed from init to set - GenerateTrimmableTypeMap: added RootManifestReferencedTypes method - Added two tests for rooting and warning behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerInfo.cs | 4 +- .../Tasks/GenerateTrimmableTypeMapTests.cs | 134 ++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index a393240474e..0b412ac6489 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/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 3c4665ae958..40d41ab2b22 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 @@ -204,6 +204,140 @@ public void Execute_NoPeersFound_ReturnsEmpty () "Should log that no peers were found."); } + [Test] + public void Execute_WithMonoAndroid_PopulatesAcwMap () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + var acwMapFile = Path.Combine (Root, path, "acw-map.txt"); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + var task = CreateTask (new [] { monoAndroidItem }, outputDir, javaDir); + task.AcwMapOutputFile = acwMapFile; + + Assert.IsTrue (task.Execute (), "Task should succeed."); + FileAssert.Exists (acwMapFile); + + var lines = File.ReadAllLines (acwMapFile); + Assert.IsNotEmpty (lines, "acw-map.txt should not be empty when types are found."); + + // Each type produces 3 lines, so the line count should be a multiple of 3 + Assert.AreEqual (0, lines.Length % 3, "acw-map.txt should have 3 lines per type."); + + // Check that Activity mapping exists (Mono.Android contains Android.App.Activity) + Assert.IsTrue (lines.Any (l => l.Contains ("Android.App.Activity") && l.Contains ("android.app.Activity")), + "Should contain Activity mapping."); + + // Verify format: each line should be "key;value" + foreach (var line in lines) { + Assert.IsTrue (line.Contains (';'), $"Line should contain ';' separator: {line}"); + var parts = line.Split (';'); + Assert.AreEqual (2, parts.Length, $"Line should have exactly 2 parts: {line}"); + Assert.IsNotEmpty (parts [0], $"Key should not be empty: {line}"); + Assert.IsNotEmpty (parts [1], $"Value should not be empty: {line}"); + } + } + + [Test] + public void Execute_EmptyAssemblyList_WritesEmptyAcwMap () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + var acwMapFile = Path.Combine (Root, path, "acw-map.txt"); + + var task = CreateTask ([], outputDir, javaDir); + task.AcwMapOutputFile = acwMapFile; + + Assert.IsTrue (task.Execute (), "Task should succeed."); + FileAssert.Exists (acwMapFile); + Assert.IsEmpty (File.ReadAllText (acwMapFile), + "acw-map.txt should be empty when no peers are found."); + } + + [Test] + public void Execute_ManifestReferencedType_IsRootedAsUnconditional () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + // Create a manifest template that references a known MCW binding type. + // android.app.Activity has DoNotGenerateAcw=true so it is normally conditional. + var manifestDir = Path.Combine (Root, path, "manifest"); + Directory.CreateDirectory (manifestDir); + var manifestPath = Path.Combine (manifestDir, "AndroidManifest.xml"); + File.WriteAllText (manifestPath, """ + + + + + + + """); + + var messages = new List (); + var task = CreateTask (new [] { monoAndroidItem }, outputDir, javaDir, messages); + task.ManifestTemplate = manifestPath; + + Assert.IsTrue (task.Execute (), "Task should succeed."); + Assert.IsTrue (messages.Any (m => m.Message.Contains ("Rooting manifest-referenced type")), + "Should log that a manifest-referenced type was rooted."); + } + + [Test] + public void Execute_ManifestReferencedType_NotFound_LogsWarning () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + var manifestDir = Path.Combine (Root, path, "manifest"); + Directory.CreateDirectory (manifestDir); + var manifestPath = Path.Combine (manifestDir, "AndroidManifest.xml"); + File.WriteAllText (manifestPath, """ + + + + + + + """); + + var warnings = new List (); + var task = new GenerateTrimmableTypeMap { + BuildEngine = new MockBuildEngine (TestContext.Out, warnings: warnings), + ResolvedAssemblies = new [] { monoAndroidItem }, + OutputDirectory = outputDir, + JavaSourceOutputDirectory = javaDir, + AcwMapDirectory = Path.Combine (outputDir, "..", "acw-maps"), + TargetFrameworkVersion = "v11.0", + ManifestTemplate = manifestPath, + }; + + Assert.IsTrue (task.Execute (), "Task should succeed even with unresolved manifest types."); + Assert.IsTrue (warnings.Any (w => w.Message.Contains ("com.example.NonExistentActivity")), + "Should warn about unresolved manifest-referenced type."); + } + GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir, IList? messages = null, string tfv = "v11.0") { From 6831cef88c210ed04dd0a08fe1cace94bf9fc4b3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 25 Mar 2026 17:27:37 +0100 Subject: [PATCH 2/2] Add RootManifestReferencedTypes to TrimmableTypeMapGenerator Parse the user's AndroidManifest.xml template to find hand-edited component entries (activity, service, receiver, provider) and mark matching Java peers as unconditional so they survive trimming. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index b9536fe3d1b..d15827e7961 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; @@ -73,6 +74,8 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult ([], [], null); } + RootManifestReferencedTypes (allPeers, manifestTemplatePath); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths, outputDirectory); // Generate JCW .java files for user assemblies + framework Implementor types. @@ -263,4 +266,74 @@ void ValidateComponents (List allPeers, AssemblyManifestInfo assem log.LogError (null, "XA4217", null, null, 0, 0, 0, 0, "Application cannot have both a type with an [Application] attribute and an [assembly:Application] attribute."); } } + + void RootManifestReferencedTypes (List allPeers, string? manifestTemplatePath) + { + if (string.IsNullOrEmpty (manifestTemplatePath) || !File.Exists (manifestTemplatePath)) { + return; + } + + XDocument doc; + try { + doc = XDocument.Load (manifestTemplatePath); + } catch (Exception ex) { + log.LogWarning ("Failed to parse ManifestTemplate '{0}': {1}", manifestTemplatePath, ex.Message); + return; + } + + RootManifestReferencedTypes (allPeers, doc); + } + + 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; + log.LogMessage (MessageImportance.Low, "Rooting manifest-referenced type '{0}' ({1}) as unconditional.", name, peer.ManagedTypeName); + } + } + } else { + log.LogWarning ("Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type.", name); + } + } + } }