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/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); + } + } + } } 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") {