Skip to content

Commit ca824a5

Browse files
[TrimmableTypeMap] Root manifest-referenced types as unconditional
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>
1 parent a049075 commit ca824a5

4 files changed

Lines changed: 195 additions & 3 deletions

File tree

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ public sealed record JavaPeerInfo
6969
/// Types with component attributes ([Activity], [Service], etc.),
7070
/// custom views from layout XML, or manifest-declared components
7171
/// are unconditionally preserved (not trimmable).
72+
/// May be set after scanning when the manifest references a type
73+
/// that the scanner did not mark as unconditional.
7274
/// </summary>
73-
public bool IsUnconditional { get; init; }
75+
public bool IsUnconditional { get; set; }
7476

7577
/// <summary>
7678
/// True for Application and Instrumentation types. These types cannot call

src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
using System.IO;
44
using System.Linq;
55
using System.Reflection.PortableExecutable;
6+
using System.Xml.Linq;
67

78
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
89

910
public class TrimmableTypeMapGenerator
1011
{
1112
readonly Action<string> log;
13+
readonly Action<string>? warn;
1214

13-
public TrimmableTypeMapGenerator (Action<string> log)
15+
public TrimmableTypeMapGenerator (Action<string> log, Action<string>? warn = null)
1416
{
1517
this.log = log ?? throw new ArgumentNullException (nameof (log));
18+
this.warn = warn;
1619
}
1720

1821
/// <summary>
@@ -38,6 +41,8 @@ public TrimmableTypeMapResult Execute (
3841
return new TrimmableTypeMapResult ([], [], allPeers);
3942
}
4043

44+
RootManifestReferencedTypes (allPeers, manifestTemplatePath);
45+
4146
var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion);
4247
var jcwPeers = allPeers.Where (p =>
4348
!frameworkAssemblyNames.Contains (p.AssemblyName)
@@ -144,4 +149,74 @@ List<GeneratedJavaSource> GenerateJcwJavaSources (List<JavaPeerInfo> allPeers)
144149
log ($"Generated {sources.Count} JCW Java source files.");
145150
return sources.ToList ();
146151
}
152+
153+
void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, string? manifestTemplatePath)
154+
{
155+
if (manifestTemplatePath.IsNullOrEmpty () || !File.Exists (manifestTemplatePath)) {
156+
return;
157+
}
158+
159+
XDocument doc;
160+
try {
161+
doc = XDocument.Load (manifestTemplatePath);
162+
} catch (Exception ex) {
163+
warn?.Invoke ($"Failed to parse ManifestTemplate '{manifestTemplatePath}': {ex.Message}");
164+
return;
165+
}
166+
167+
RootManifestReferencedTypes (allPeers, doc);
168+
}
169+
170+
internal void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, XDocument doc)
171+
{
172+
var root = doc.Root;
173+
if (root is null) {
174+
return;
175+
}
176+
177+
XNamespace androidNs = "http://schemas.android.com/apk/res/android";
178+
XName attName = androidNs + "name";
179+
180+
var componentNames = new HashSet<string> (StringComparer.Ordinal);
181+
foreach (var element in root.Descendants ()) {
182+
switch (element.Name.LocalName) {
183+
case "activity":
184+
case "service":
185+
case "receiver":
186+
case "provider":
187+
var name = (string?) element.Attribute (attName);
188+
if (name is not null) {
189+
componentNames.Add (name);
190+
}
191+
break;
192+
}
193+
}
194+
195+
if (componentNames.Count == 0) {
196+
return;
197+
}
198+
199+
var peersByDotName = new Dictionary<string, List<JavaPeerInfo>> (StringComparer.Ordinal);
200+
foreach (var peer in allPeers) {
201+
var dotName = peer.JavaName.Replace ('/', '.').Replace ('$', '.');
202+
if (!peersByDotName.TryGetValue (dotName, out var list)) {
203+
list = [];
204+
peersByDotName [dotName] = list;
205+
}
206+
list.Add (peer);
207+
}
208+
209+
foreach (var name in componentNames) {
210+
if (peersByDotName.TryGetValue (name, out var peers)) {
211+
foreach (var peer in peers) {
212+
if (!peer.IsUnconditional) {
213+
peer.IsUnconditional = true;
214+
log ($"Rooting manifest-referenced type '{name}' ({peer.ManagedTypeName}) as unconditional.");
215+
}
216+
}
217+
} else {
218+
warn?.Invoke ($"Manifest-referenced type '{name}' was not found in any scanned assembly. It may be a framework type.");
219+
}
220+
}
221+
}
147222
}

src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ public override bool RunTask ()
9494
ApplicationJavaClass: ApplicationJavaClass);
9595
}
9696

97-
var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg));
97+
var generator = new TrimmableTypeMapGenerator (
98+
msg => Log.LogMessage (MessageImportance.Low, msg),
99+
msg => Log.LogWarning (msg));
98100
result = generator.Execute (
99101
assemblies,
100102
systemRuntimeVersion,

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,119 @@ public void Execute_JavaSourcesHaveCorrectStructure ()
8181

8282
TrimmableTypeMapGenerator CreateGenerator () => new (msg => logMessages.Add (msg));
8383

84+
TrimmableTypeMapGenerator CreateGenerator (List<string> warnings) =>
85+
new (msg => logMessages.Add (msg), msg => warnings.Add (msg));
86+
87+
[Fact]
88+
public void RootManifestReferencedTypes_RootsMatchingPeers ()
89+
{
90+
var peers = new List<JavaPeerInfo> {
91+
new JavaPeerInfo {
92+
JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity",
93+
ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity",
94+
AssemblyName = "MyApp", IsUnconditional = false,
95+
},
96+
new JavaPeerInfo {
97+
JavaName = "com/example/MyService", CompatJniName = "com.example.MyService",
98+
ManagedTypeName = "MyApp.MyService", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyService",
99+
AssemblyName = "MyApp", IsUnconditional = false,
100+
},
101+
};
102+
103+
var doc = System.Xml.Linq.XDocument.Parse ("""
104+
<?xml version="1.0" encoding="utf-8"?>
105+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
106+
<application>
107+
<activity android:name="com.example.MyActivity" />
108+
</application>
109+
</manifest>
110+
""");
111+
112+
var generator = CreateGenerator ();
113+
generator.RootManifestReferencedTypes (peers, doc);
114+
115+
Assert.True (peers [0].IsUnconditional, "MyActivity should be rooted as unconditional.");
116+
Assert.False (peers [1].IsUnconditional, "MyService should remain conditional.");
117+
Assert.Contains (logMessages, m => m.Contains ("Rooting manifest-referenced type"));
118+
}
119+
120+
[Fact]
121+
public void RootManifestReferencedTypes_WarnsForUnresolvedTypes ()
122+
{
123+
var peers = new List<JavaPeerInfo> {
124+
new JavaPeerInfo {
125+
JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity",
126+
ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity",
127+
AssemblyName = "MyApp",
128+
},
129+
};
130+
131+
var doc = System.Xml.Linq.XDocument.Parse ("""
132+
<?xml version="1.0" encoding="utf-8"?>
133+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
134+
<application>
135+
<service android:name="com.example.NonExistentService" />
136+
</application>
137+
</manifest>
138+
""");
139+
140+
var warnings = new List<string> ();
141+
var generator = CreateGenerator (warnings);
142+
generator.RootManifestReferencedTypes (peers, doc);
143+
144+
Assert.Contains (warnings, w => w.Contains ("com.example.NonExistentService"));
145+
}
146+
147+
[Fact]
148+
public void RootManifestReferencedTypes_SkipsAlreadyUnconditional ()
149+
{
150+
var peers = new List<JavaPeerInfo> {
151+
new JavaPeerInfo {
152+
JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity",
153+
ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity",
154+
AssemblyName = "MyApp", IsUnconditional = true,
155+
},
156+
};
157+
158+
var doc = System.Xml.Linq.XDocument.Parse ("""
159+
<?xml version="1.0" encoding="utf-8"?>
160+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
161+
<application>
162+
<activity android:name="com.example.MyActivity" />
163+
</application>
164+
</manifest>
165+
""");
166+
167+
var generator = CreateGenerator ();
168+
generator.RootManifestReferencedTypes (peers, doc);
169+
170+
Assert.True (peers [0].IsUnconditional);
171+
Assert.DoesNotContain (logMessages, m => m.Contains ("Rooting manifest-referenced type"));
172+
}
173+
174+
[Fact]
175+
public void RootManifestReferencedTypes_EmptyManifest_NoChanges ()
176+
{
177+
var peers = new List<JavaPeerInfo> {
178+
new JavaPeerInfo {
179+
JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity",
180+
ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity",
181+
AssemblyName = "MyApp",
182+
},
183+
};
184+
185+
var doc = System.Xml.Linq.XDocument.Parse ("""
186+
<?xml version="1.0" encoding="utf-8"?>
187+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
188+
</manifest>
189+
""");
190+
191+
var generator = CreateGenerator ();
192+
generator.RootManifestReferencedTypes (peers, doc);
193+
194+
Assert.False (peers [0].IsUnconditional);
195+
}
196+
84197
static PEReader CreateTestFixturePEReader ()
85198
{
86199
var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)

0 commit comments

Comments
 (0)