Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public bool IsUnconditional { get; init; }
public bool IsUnconditional { get; set; }

/// <summary>
/// True for Application and Instrumentation types. These types cannot call
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -263,4 +266,74 @@ void ValidateComponents (List<JavaPeerInfo> 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<JavaPeerInfo> 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<JavaPeerInfo> 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<string> (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<string, List<JavaPeerInfo>> (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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, """
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.test">
<application>
<activity android:name="android.app.Activity" />
</application>
</manifest>
""");

var messages = new List<BuildMessageEventArgs> ();
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, """
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.test">
<application>
<activity android:name="com.example.NonExistentActivity" />
</application>
</manifest>
""");

var warnings = new List<BuildWarningEventArgs> ();
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<BuildMessageEventArgs>? messages = null, string tfv = "v11.0")
{
Expand Down