diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
index aac84cd9803..96bd0f729ab 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
@@ -42,43 +42,26 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
public sealed class JcwJavaSourceGenerator
{
///
- /// Generates .java source files for all ACW types and writes them to the output directory.
- /// Returns the list of generated file paths.
+ /// Generates .java source content for all ACW types and returns them as in-memory
+ /// (relativePath, content) pairs. No filesystem IO is performed.
///
- public IReadOnlyList Generate (IReadOnlyList types, string outputDirectory)
+ public IReadOnlyList GenerateContent (IReadOnlyList types)
{
- if (types is null) {
- throw new ArgumentNullException (nameof (types));
- }
- if (outputDirectory is null) {
- throw new ArgumentNullException (nameof (outputDirectory));
- }
-
- var generatedFiles = new List ();
-
+ if (types is null) throw new ArgumentNullException (nameof (types));
+ var results = new List ();
foreach (var type in types) {
- if (type.DoNotGenerateAcw || type.IsInterface) {
- continue;
- }
-
- string filePath = GetOutputFilePath (type, outputDirectory);
- string? dir = Path.GetDirectoryName (filePath);
- if (dir != null) {
- Directory.CreateDirectory (dir);
- }
-
- using var writer = new StreamWriter (filePath);
+ if (type.DoNotGenerateAcw || type.IsInterface) continue;
+ using var writer = new StringWriter ();
Generate (type, writer);
- generatedFiles.Add (filePath);
+ results.Add (new GeneratedJavaSource (GetRelativePath (type), writer.ToString ()));
}
-
- return generatedFiles;
+ return results;
}
///
/// Generates a single .java source file for the given type.
///
- internal void Generate (JavaPeerInfo type, TextWriter writer)
+ public void Generate (JavaPeerInfo type, TextWriter writer)
{
writer.NewLine = "\n";
WritePackageDeclaration (type, writer);
@@ -91,13 +74,13 @@ internal void Generate (JavaPeerInfo type, TextWriter writer)
WriteClassClose (writer);
}
- static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory)
+ static string GetRelativePath (JavaPeerInfo type)
{
JniSignatureHelper.ValidateJniName (type.JavaName);
- string relativePath = type.JavaName + ".java";
- return Path.Combine (outputDirectory, relativePath);
+ return type.JavaName + ".java";
}
+
///
/// Validates that the JNI name is well-formed: non-empty, each segment separated by '/'
/// contains only valid Java identifier characters (letters, digits, '_', '$').
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs
index bf913ce7f19..56ae75638b2 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs
@@ -94,20 +94,6 @@ public void EmitPreamble (string assemblyName, string moduleName, ReadOnlySpan
- /// Serialises the metadata + IL into a PE DLL at .
- ///
- public void WritePE (string outputPath)
- {
- var dir = Path.GetDirectoryName (outputPath);
- if (!string.IsNullOrEmpty (dir)) {
- Directory.CreateDirectory (dir);
- }
-
- using var fs = File.Create (outputPath);
- WritePE (fs);
- }
-
///
/// Serialises the metadata + IL into a PE DLL and writes it to the given .
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
index 6fa3e7a648b..01f29e322c1 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
@@ -31,28 +31,6 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion)
_systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
}
- ///
- /// Generates the root typemap assembly and writes it to a file.
- ///
- /// Names of per-assembly typemap assemblies to reference.
- /// Path to write the output .dll.
- /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps).
- public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outputPath, string? assemblyName = null)
- {
- if (outputPath is null) {
- throw new ArgumentNullException (nameof (outputPath));
- }
-
- var dir = Path.GetDirectoryName (outputPath);
- if (!string.IsNullOrEmpty (dir)) {
- Directory.CreateDirectory (dir);
- }
-
- var moduleName = Path.GetFileName (outputPath);
- using var fs = File.Create (outputPath);
- Generate (perAssemblyTypeMapNames, fs, assemblyName, moduleName);
- }
-
///
/// Generates the root typemap assembly and writes it to the given stream.
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
index 9edb001f9d0..34bfd15a9b6 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
@@ -118,22 +118,6 @@ public TypeMapAssemblyEmitter (Version systemRuntimeVersion)
_pe = new PEAssemblyBuilder (_systemRuntimeVersion);
}
- ///
- /// Emits a PE assembly from the given model and writes it to .
- ///
- public void Emit (TypeMapAssemblyData model, string outputPath)
- {
- if (model is null) {
- throw new ArgumentNullException (nameof (model));
- }
- if (outputPath is null) {
- throw new ArgumentNullException (nameof (outputPath));
- }
-
- EmitCore (model);
- _pe.WritePE (outputPath);
- }
-
///
/// Emits a PE assembly from the given model and writes it to .
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs
index 34e84f64459..2cb92963816 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs
@@ -18,19 +18,6 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion)
_systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
}
- ///
- /// Generates a TypeMap PE assembly from the given Java peer info records.
- ///
- /// Scanned Java peer types.
- /// Path where the output .dll will be written.
- /// Optional explicit assembly name. Derived from outputPath if null.
- public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null)
- {
- var model = ModelBuilder.Build (peers, outputPath, assemblyName);
- var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion);
- emitter.Emit (model, outputPath);
- }
-
///
/// Generates a TypeMap PE assembly from the given Java peer info records and writes it to .
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs
new file mode 100644
index 00000000000..30c441b81cd
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs
@@ -0,0 +1,18 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+// The static methods in System.String are not NRT annotated in netstandard2.0,
+// so we need our own extension methods to make them nullable aware.
+static class NullableExtensions
+{
+ public static bool IsNullOrEmpty ([NotNullWhen (false)] this string? str)
+ {
+ return string.IsNullOrEmpty (str);
+ }
+
+ public static bool IsNullOrWhiteSpace ([NotNullWhen (false)] this string? str)
+ {
+ return string.IsNullOrWhiteSpace (str);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
index 2d4803c8983..e63dcc63202 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
@@ -18,7 +17,6 @@ sealed class AssemblyIndex : IDisposable
public MetadataReader Reader { get; }
public string AssemblyName { get; }
- public string FilePath { get; }
///
/// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle.
@@ -35,21 +33,18 @@ sealed class AssemblyIndex : IDisposable
///
public Dictionary AttributesByType { get; } = new ();
- AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath)
+ AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName)
{
this.peReader = peReader;
this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader);
Reader = reader;
AssemblyName = assemblyName;
- FilePath = filePath;
}
- public static AssemblyIndex Create (string filePath)
+ public static AssemblyIndex Create (PEReader peReader, string assemblyName)
{
- var peReader = new PEReader (File.OpenRead (filePath));
var reader = peReader.GetMetadataReader ();
- var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
- var index = new AssemblyIndex (peReader, reader, assemblyName, filePath);
+ var index = new AssemblyIndex (peReader, reader, assemblyName);
index.Build ();
return index;
}
@@ -477,7 +472,6 @@ static PropertyInfo CreatePropertyInfo (string name, Dictionary
public void Dispose ()
{
- peReader.Dispose ();
}
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 55fdd47a0a6..4b298352b75 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -5,6 +5,7 @@
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
+using System.Reflection.PortableExecutable;
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
@@ -79,24 +80,18 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan
/// Phase 1: Build indices for all assemblies.
/// Phase 2: Scan all types and produce JavaPeerInfo records.
///
- public List Scan (IReadOnlyList assemblyPaths)
+ public List Scan (IReadOnlyList<(string Name, PEReader Reader)> assemblies)
{
- // Phase 1: Build indices for all assemblies
- foreach (var path in assemblyPaths) {
- var index = AssemblyIndex.Create (path);
+ foreach (var (name, reader) in assemblies) {
+ var index = AssemblyIndex.Create (reader, name);
assemblyCache [index.AssemblyName] = index;
}
- // Phase 2: Analyze types using cached indices
var resultsByManagedName = new Dictionary (StringComparer.Ordinal);
-
foreach (var index in assemblyCache.Values) {
ScanAssembly (index, resultsByManagedName);
}
-
- // Phase 3: Force unconditional on types referenced by [Application] attributes
ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache);
-
return new List (resultsByManagedName.Values);
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs
new file mode 100644
index 00000000000..1a2b0691057
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public class TrimmableTypeMapGenerator
+{
+ readonly Action log;
+
+ public TrimmableTypeMapGenerator (Action log)
+ {
+ this.log = log ?? throw new ArgumentNullException (nameof (log));
+ }
+
+ public TrimmableTypeMapResult Execute (
+ IReadOnlyList<(string Name, PEReader Reader)> assemblies,
+ Version systemRuntimeVersion,
+ HashSet frameworkAssemblyNames)
+ {
+ _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies));
+ _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ _ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames));
+
+ var allPeers = ScanAssemblies (assemblies);
+ if (allPeers.Count == 0) {
+ log ("No Java peer types found, skipping typemap generation.");
+ return new TrimmableTypeMapResult ([], [], allPeers);
+ }
+
+ var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion);
+ var jcwPeers = allPeers.Where (p =>
+ !frameworkAssemblyNames.Contains (p.AssemblyName)
+ || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList ();
+ log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total).");
+ var generatedJavaSources = GenerateJcwJavaSources (jcwPeers);
+ return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers);
+ }
+
+ List ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies)
+ {
+ using var scanner = new JavaPeerScanner ();
+ var peers = scanner.Scan (assemblies);
+ log ($"Scanned {assemblies.Count} assemblies, found {peers.Count} Java peer types.");
+ return peers;
+ }
+
+ List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion)
+ {
+ var peersByAssembly = allPeers.GroupBy (p => p.AssemblyName, StringComparer.Ordinal).OrderBy (g => g.Key, StringComparer.Ordinal);
+ var generatedAssemblies = new List ();
+ var perAssemblyNames = new List ();
+ var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion);
+ foreach (var group in peersByAssembly) {
+ string assemblyName = $"_{group.Key}.TypeMap";
+ perAssemblyNames.Add (assemblyName);
+ var peers = group.ToList ();
+ var stream = new MemoryStream ();
+ generator.Generate (peers, stream, assemblyName);
+ stream.Position = 0;
+ generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream));
+ log ($" {assemblyName}: {peers.Count} types");
+ }
+ var rootStream = new MemoryStream ();
+ var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion);
+ rootGenerator.Generate (perAssemblyNames, rootStream);
+ rootStream.Position = 0;
+ generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream));
+ log ($" Root: {perAssemblyNames.Count} per-assembly refs");
+ log ($"Generated {generatedAssemblies.Count} typemap assemblies.");
+ return generatedAssemblies;
+ }
+
+ List GenerateJcwJavaSources (List allPeers)
+ {
+ var jcwGenerator = new JcwJavaSourceGenerator ();
+ var sources = jcwGenerator.GenerateContent (allPeers);
+ log ($"Generated {sources.Count} JCW Java source files.");
+ return sources.ToList ();
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs
new file mode 100644
index 00000000000..c10a2482bd2
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public record TrimmableTypeMapResult (
+ IReadOnlyList GeneratedAssemblies,
+ IReadOnlyList GeneratedJavaSources,
+ IReadOnlyList AllPeers);
+
+public record GeneratedAssembly (string Name, MemoryStream Content);
+
+public record GeneratedJavaSource (string RelativePath, string Content);
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
index ecb6529357f..4d621afd2a9 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
@@ -4,6 +4,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
using Microsoft.Android.Build.Tasks;
using Microsoft.Android.Sdk.TrimmableTypeMap;
using Microsoft.Build.Framework;
@@ -11,219 +13,179 @@
namespace Xamarin.Android.Tasks;
-///
-/// Generates trimmable TypeMap assemblies, JCW Java source files, and per-assembly
-/// acw-map files from resolved assemblies. The acw-map files are later merged into
-/// a single acw-map.txt consumed by _ConvertCustomView for layout XML fixups.
-///
public class GenerateTrimmableTypeMap : AndroidTask
{
- public override string TaskPrefix => "GTT";
-
- [Required]
- public ITaskItem [] ResolvedAssemblies { get; set; } = [];
-
- [Required]
- public string OutputDirectory { get; set; } = "";
-
- [Required]
- public string JavaSourceOutputDirectory { get; set; } = "";
-
- ///
- /// Directory for per-assembly acw-map.{AssemblyName}.txt files.
- ///
- [Required]
- public string AcwMapDirectory { get; set; } = "";
-
- ///
- /// The .NET target framework version (e.g., "v11.0"). Used to set the System.Runtime
- /// assembly reference version in generated typemap assemblies.
- ///
- [Required]
- public string TargetFrameworkVersion { get; set; } = "";
-
- [Output]
- public ITaskItem []? GeneratedAssemblies { get; set; }
-
- [Output]
- public ITaskItem []? GeneratedJavaFiles { get; set; }
-
- ///
- /// Per-assembly acw-map files produced during scanning. Each file contains
- /// three lines per type: PartialAssemblyQualifiedName;JavaKey,
- /// ManagedKey;JavaKey, and CompatJniName;JavaKey.
- ///
- [Output]
- public ITaskItem []? PerAssemblyAcwMapFiles { get; set; }
-
- public override bool RunTask ()
- {
- var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion);
- var assemblyPaths = GetJavaInteropAssemblyPaths (ResolvedAssemblies);
-
- Directory.CreateDirectory (OutputDirectory);
- Directory.CreateDirectory (JavaSourceOutputDirectory);
- Directory.CreateDirectory (AcwMapDirectory);
-
- var allPeers = ScanAssemblies (assemblyPaths);
- if (allPeers.Count == 0) {
- Log.LogDebugMessage ("No Java peer types found, skipping typemap generation.");
- return !Log.HasLoggedErrors;
- }
-
- GeneratedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths);
- GeneratedJavaFiles = GenerateJcwJavaSources (allPeers);
- PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (allPeers);
-
- return !Log.HasLoggedErrors;
- }
-
- // Future optimization: the scanner currently scans all assemblies on every run.
- // For incremental builds, we could:
- // 1. Add a Scan(allPaths, changedPaths) overload that only produces JavaPeerInfo
- // for changed assemblies while still indexing all assemblies for cross-assembly
- // resolution (base types, interfaces, activation ctors).
- // 2. Cache scan results per assembly to skip PE I/O entirely for unchanged assemblies.
- // Both require profiling to determine if they meaningfully improve build times.
- List ScanAssemblies (IReadOnlyList assemblyPaths)
- {
- using var scanner = new JavaPeerScanner ();
- var peers = scanner.Scan (assemblyPaths);
- Log.LogDebugMessage ($"Scanned {assemblyPaths.Count} assemblies, found {peers.Count} Java peer types.");
- return peers;
- }
-
- ITaskItem [] GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion,
- IReadOnlyList assemblyPaths)
- {
- // Build a map from assembly name → source path for timestamp comparison
- var sourcePathByName = new Dictionary (StringComparer.Ordinal);
- foreach (var path in assemblyPaths) {
- var name = Path.GetFileNameWithoutExtension (path);
- sourcePathByName [name] = path;
- }
-
- var peersByAssembly = allPeers
- .GroupBy (p => p.AssemblyName, StringComparer.Ordinal)
- .OrderBy (g => g.Key, StringComparer.Ordinal);
-
- var generatedAssemblies = new List ();
- var perAssemblyNames = new List ();
- var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion);
- bool anyRegenerated = false;
-
- foreach (var group in peersByAssembly) {
- string assemblyName = $"_{group.Key}.TypeMap";
- string outputPath = Path.Combine (OutputDirectory, assemblyName + ".dll");
- perAssemblyNames.Add (assemblyName);
-
- if (IsUpToDate (outputPath, group.Key, sourcePathByName)) {
- Log.LogDebugMessage ($" {assemblyName}: up to date, skipping");
- generatedAssemblies.Add (new TaskItem (outputPath));
- continue;
- }
-
- generator.Generate (group.ToList (), outputPath, assemblyName);
- generatedAssemblies.Add (new TaskItem (outputPath));
- anyRegenerated = true;
-
- Log.LogDebugMessage ($" {assemblyName}: {group.Count ()} types");
- }
-
- // Root assembly references all per-assembly typemaps — regenerate if any changed
- string rootOutputPath = Path.Combine (OutputDirectory, "_Microsoft.Android.TypeMaps.dll");
- if (anyRegenerated || !File.Exists (rootOutputPath)) {
- var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion);
- rootGenerator.Generate (perAssemblyNames, rootOutputPath);
- Log.LogDebugMessage ($" Root: {perAssemblyNames.Count} per-assembly refs");
- } else {
- Log.LogDebugMessage ($" Root: up to date, skipping");
- }
- generatedAssemblies.Add (new TaskItem (rootOutputPath));
-
- Log.LogDebugMessage ($"Generated {generatedAssemblies.Count} typemap assemblies.");
- return generatedAssemblies.ToArray ();
- }
-
- static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName)
- {
- if (!File.Exists (outputPath)) {
- return false;
- }
- if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) {
- return false;
- }
- return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath);
- }
-
- ITaskItem [] GenerateJcwJavaSources (List allPeers)
- {
- var jcwGenerator = new JcwJavaSourceGenerator ();
- var files = jcwGenerator.Generate (allPeers, JavaSourceOutputDirectory);
- Log.LogDebugMessage ($"Generated {files.Count} JCW Java source files.");
-
- var items = new ITaskItem [files.Count];
- for (int i = 0; i < files.Count; i++) {
- items [i] = new TaskItem (files [i]);
- }
- return items;
- }
-
- ITaskItem [] GeneratePerAssemblyAcwMaps (List allPeers)
- {
- var peersByAssembly = allPeers
- .GroupBy (p => p.AssemblyName, StringComparer.Ordinal)
- .OrderBy (g => g.Key, StringComparer.Ordinal);
-
- var outputFiles = new List ();
-
- foreach (var group in peersByAssembly) {
- var peers = group.ToList ();
- string outputFile = Path.Combine (AcwMapDirectory, $"acw-map.{group.Key}.txt");
-
- bool written;
- using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) {
- AcwMapWriter.Write (sw, peers);
- sw.Flush ();
- written = Files.CopyIfStreamChanged (sw.BaseStream, outputFile);
- }
-
- Log.LogDebugMessage (written
- ? $" acw-map.{group.Key}.txt: {peers.Count} types"
- : $" acw-map.{group.Key}.txt: unchanged");
-
- var item = new TaskItem (outputFile);
- item.SetMetadata ("AssemblyName", group.Key);
- outputFiles.Add (item);
- }
-
- Log.LogDebugMessage ($"Generated {outputFiles.Count} per-assembly ACW map files.");
- return outputFiles.ToArray ();
- }
-
- static Version ParseTargetFrameworkVersion (string tfv)
- {
- if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) {
- tfv = tfv.Substring (1);
- }
- if (Version.TryParse (tfv, out var version)) {
- return version;
- }
- throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version.");
- }
-
- ///
- /// Filters resolved assemblies to only those that reference Mono.Android or Java.Interop
- /// (i.e., assemblies that could contain [Register] types). Skips BCL assemblies.
- ///
- static IReadOnlyList GetJavaInteropAssemblyPaths (ITaskItem [] items)
- {
- var paths = new List (items.Length);
- foreach (var item in items) {
- if (MonoAndroidHelper.IsMonoAndroidAssembly (item)) {
- paths.Add (item.ItemSpec);
- }
- }
- return paths;
- }
+public override string TaskPrefix => "GTT";
+
+[Required]
+public ITaskItem [] ResolvedAssemblies { get; set; } = [];
+[Required]
+public string OutputDirectory { get; set; } = "";
+[Required]
+public string JavaSourceOutputDirectory { get; set; } = "";
+[Required]
+public string AcwMapDirectory { get; set; } = "";
+[Required]
+public string TargetFrameworkVersion { get; set; } = "";
+[Output]
+public ITaskItem [] GeneratedAssemblies { get; set; } = [];
+[Output]
+public ITaskItem [] GeneratedJavaFiles { get; set; } = [];
+[Output]
+public ITaskItem []? PerAssemblyAcwMapFiles { get; set; }
+
+public override bool RunTask ()
+{
+var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion);
+var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList ();
+// TODO(#10792): populate with framework assembly names to skip JCW generation for pre-compiled framework types
+var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase);
+
+Directory.CreateDirectory (OutputDirectory);
+Directory.CreateDirectory (JavaSourceOutputDirectory);
+Directory.CreateDirectory (AcwMapDirectory);
+
+var peReaders = new List ();
+var assemblies = new List<(string Name, PEReader Reader)> ();
+TrimmableTypeMapResult? result = null;
+try {
+foreach (var path in assemblyPaths) {
+var peReader = new PEReader (File.OpenRead (path));
+peReaders.Add (peReader);
+var mdReader = peReader.GetMetadataReader ();
+assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader));
+}
+
+var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg));
+result = generator.Execute (assemblies, systemRuntimeVersion, frameworkAssemblyNames);
+
+GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths);
+GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources);
+PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers);
+} finally {
+if (result is not null) {
+foreach (var assembly in result.GeneratedAssemblies) {
+assembly.Content.Dispose ();
+}
+}
+foreach (var peReader in peReaders) {
+peReader.Dispose ();
+}
+}
+
+return !Log.HasLoggedErrors;
+}
+
+ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyPaths)
+{
+// Build a map from assembly name -> source path for timestamp comparison
+var sourcePathByName = new Dictionary (StringComparer.Ordinal);
+foreach (var path in assemblyPaths) {
+var name = Path.GetFileNameWithoutExtension (path);
+sourcePathByName [name] = path;
+}
+
+var items = new List ();
+bool anyRegenerated = false;
+
+foreach (var assembly in assemblies) {
+if (assembly.Name == "_Microsoft.Android.TypeMaps") {
+continue; // Handle root assembly separately below
+}
+
+string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll");
+// Extract the original assembly name from the typemap name (e.g., "_Foo.TypeMap" -> "Foo")
+string originalName = assembly.Name;
+if (originalName.StartsWith ("_", StringComparison.Ordinal) && originalName.EndsWith (".TypeMap", StringComparison.Ordinal)) {
+originalName = originalName.Substring (1, originalName.Length - ".TypeMap".Length - 1);
+}
+
+if (IsUpToDate (outputPath, originalName, sourcePathByName)) {
+Log.LogDebugMessage ($" {assembly.Name}: up to date, skipping");
+} else {
+Files.CopyIfStreamChanged (assembly.Content, outputPath);
+anyRegenerated = true;
+Log.LogDebugMessage ($" {assembly.Name}: written");
+}
+
+items.Add (new TaskItem (outputPath));
+}
+
+// Root assembly — regenerate if any per-assembly typemap changed
+var rootAssembly = assemblies.FirstOrDefault (a => a.Name == "_Microsoft.Android.TypeMaps");
+if (rootAssembly is not null) {
+string rootOutputPath = Path.Combine (OutputDirectory, rootAssembly.Name + ".dll");
+if (anyRegenerated || !File.Exists (rootOutputPath)) {
+Files.CopyIfStreamChanged (rootAssembly.Content, rootOutputPath);
+Log.LogDebugMessage ($" Root: written");
+} else {
+Log.LogDebugMessage ($" Root: up to date, skipping");
+}
+items.Add (new TaskItem (rootOutputPath));
+}
+
+return items.ToArray ();
+}
+
+static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName)
+{
+if (!File.Exists (outputPath)) {
+return false;
+}
+if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) {
+return false;
+}
+return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath);
+}
+
+ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSources)
+{
+var items = new List ();
+foreach (var source in javaSources) {
+string outputPath = Path.Combine (JavaSourceOutputDirectory, source.RelativePath);
+string? dir = Path.GetDirectoryName (outputPath);
+if (!string.IsNullOrEmpty (dir)) {
+Directory.CreateDirectory (dir);
+}
+using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) {
+sw.Write (source.Content);
+sw.Flush ();
+Files.CopyIfStreamChanged (sw.BaseStream, outputPath);
+}
+items.Add (new TaskItem (outputPath));
+}
+return items.ToArray ();
+}
+
+ITaskItem [] GeneratePerAssemblyAcwMaps (IReadOnlyList allPeers)
+{
+var peersByAssembly = allPeers
+.GroupBy (p => p.AssemblyName, StringComparer.Ordinal)
+.OrderBy (g => g.Key, StringComparer.Ordinal);
+var outputFiles = new List ();
+foreach (var group in peersByAssembly) {
+var peers = group.ToList ();
+string outputFile = Path.Combine (AcwMapDirectory, $"acw-map.{group.Key}.txt");
+using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) {
+AcwMapWriter.Write (sw, peers);
+sw.Flush ();
+Files.CopyIfStreamChanged (sw.BaseStream, outputFile);
+}
+var item = new TaskItem (outputFile);
+item.SetMetadata ("AssemblyName", group.Key);
+outputFiles.Add (item);
+}
+return outputFiles.ToArray ();
+}
+
+static Version ParseTargetFrameworkVersion (string tfv)
+{
+if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) {
+tfv = tfv.Substring (1);
+}
+if (Version.TryParse (tfv, out var version)) {
+return version;
+}
+throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version.");
+}
}
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..25ab24fa9a1 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
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NUnit.Framework;
@@ -24,8 +23,29 @@ public void Execute_EmptyAssemblyList_Succeeds ()
var task = CreateTask ([], outputDir, javaDir);
Assert.IsTrue (task.Execute (), "Task should succeed with empty assembly list.");
- Assert.IsNull (task.GeneratedAssemblies);
- Assert.IsNull (task.GeneratedJavaFiles);
+ Assert.IsEmpty (task.GeneratedAssemblies);
+ Assert.IsEmpty (task.GeneratedJavaFiles);
+ }
+
+ [Test]
+ public void Execute_InvalidTargetFrameworkVersion_Fails ()
+ {
+ var path = Path.Combine ("temp", TestName);
+ var outputDir = Path.Combine (Root, path, "typemap");
+ var javaDir = Path.Combine (Root, path, "java");
+
+ var errors = new List ();
+ var task = new GenerateTrimmableTypeMap {
+ BuildEngine = new MockBuildEngine (TestContext.Out, errors),
+ ResolvedAssemblies = [],
+ OutputDirectory = outputDir,
+ JavaSourceOutputDirectory = javaDir,
+ AcwMapDirectory = Path.Combine (Root, path, "acw-maps"),
+ TargetFrameworkVersion = "not-a-version",
+ };
+
+ Assert.IsFalse (task.Execute (), "Task should fail with invalid TargetFrameworkVersion.");
+ Assert.IsNotEmpty (errors, "Should have logged an error.");
}
[Test]
@@ -59,7 +79,7 @@ public void Execute_WithMonoAndroid_ProducesOutputs ()
}
[Test]
- public void Execute_SecondRun_SkipsUpToDateAssemblies ()
+ public void Execute_SecondRun_OutputsAreUpToDate ()
{
var path = Path.Combine ("temp", TestName);
var outputDir = Path.Combine (Root, path, "typemap");
@@ -82,88 +102,13 @@ public void Execute_SecondRun_SkipsUpToDateAssemblies ()
.First (p => p.Contains ("_Mono.Android.TypeMap.dll"));
var firstWriteTime = File.GetLastWriteTimeUtc (typeMapPath);
- // Wait to ensure timestamp difference is detectable
- Thread.Sleep (100);
-
- // Second run: same inputs, outputs should be skipped (not rewritten)
- var messages = new List ();
- var task2 = CreateTask (assemblies, outputDir, javaDir, messages);
+ // Second run: same inputs — outputs should not be rewritten (CopyIfStreamChanged)
+ var task2 = CreateTask (assemblies, outputDir, javaDir);
Assert.IsTrue (task2.Execute (), "Second run should succeed.");
var secondWriteTime = File.GetLastWriteTimeUtc (typeMapPath);
Assert.AreEqual (firstWriteTime, secondWriteTime,
- "Typemap assembly should NOT be rewritten when source hasn't changed.");
-
- Assert.IsTrue (messages.Any (m => m.Message.Contains ("up to date")),
- "Should log 'up to date' for skipped assemblies.");
- }
-
- [Test]
- public void Execute_SourceTouched_RegeneratesOnlyChangedAssembly ()
- {
- 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;
- }
-
- // Copy Mono.Android.dll to a temp location so we can touch it
- var tempDir = Path.Combine (Root, path, "assemblies");
- Directory.CreateDirectory (tempDir);
- var tempAssemblyPath = Path.Combine (tempDir, "Mono.Android.dll");
- File.Copy (monoAndroidItem.ItemSpec, tempAssemblyPath, true);
-
- var tempItem = new TaskItem (tempAssemblyPath);
- tempItem.SetMetadata ("HasMonoAndroidReference", "True");
- var assemblies = new [] { tempItem };
-
- // First run
- var task1 = CreateTask (assemblies, outputDir, javaDir);
- Assert.IsTrue (task1.Execute (), "First run should succeed.");
-
- var typeMapPath = task1.GeneratedAssemblies
- .Select (i => i.ItemSpec)
- .First (p => p.Contains ("_Mono.Android.TypeMap.dll"));
- var firstWriteTime = File.GetLastWriteTimeUtc (typeMapPath);
-
- // Touch the source assembly to simulate a change
- Thread.Sleep (100);
- File.SetLastWriteTimeUtc (tempAssemblyPath, DateTime.UtcNow);
-
- // Second run: source is newer → should regenerate
- var tempItem2 = new TaskItem (tempAssemblyPath);
- tempItem2.SetMetadata ("HasMonoAndroidReference", "True");
- var task2 = CreateTask (new [] { tempItem2 }, outputDir, javaDir);
- Assert.IsTrue (task2.Execute (), "Second run should succeed.");
-
- var secondWriteTime = File.GetLastWriteTimeUtc (typeMapPath);
- Assert.Greater (secondWriteTime, firstWriteTime,
- "Typemap assembly should be regenerated when source is touched.");
- }
-
- [Test]
- public void Execute_InvalidTargetFrameworkVersion_Fails ()
- {
- var path = Path.Combine ("temp", TestName);
- var outputDir = Path.Combine (Root, path, "typemap");
- var javaDir = Path.Combine (Root, path, "java");
-
- var errors = new List ();
- var task = new GenerateTrimmableTypeMap {
- BuildEngine = new MockBuildEngine (TestContext.Out, errors),
- ResolvedAssemblies = [],
- OutputDirectory = outputDir,
- JavaSourceOutputDirectory = javaDir,
- AcwMapDirectory = Path.Combine (Root, path, "acw-maps"),
- TargetFrameworkVersion = "not-a-version",
- };
-
- Assert.IsFalse (task.Execute (), "Task should fail with invalid TargetFrameworkVersion.");
- Assert.IsNotEmpty (errors, "Should have logged an error.");
+ "Typemap assembly should NOT be rewritten when content hasn't changed.");
}
[TestCase ("v11.0")]
@@ -179,31 +124,6 @@ public void Execute_ParsesTargetFrameworkVersion (string tfv)
Assert.IsTrue (task.Execute (), $"Task should succeed with TargetFrameworkVersion='{tfv}'.");
}
- [Test]
- public void Execute_NoPeersFound_ReturnsEmpty ()
- {
- var path = Path.Combine ("temp", TestName);
- var outputDir = Path.Combine (Root, path, "typemap");
- var javaDir = Path.Combine (Root, path, "java");
-
- // Use a real assembly that has no [Register] types
- var testAssemblyDir = Path.GetDirectoryName (GetType ().Assembly.Location)!;
- var nunitDll = Path.Combine (testAssemblyDir, "nunit.framework.dll");
- if (!File.Exists (nunitDll)) {
- Assert.Ignore ("nunit.framework.dll not found; skipping.");
- return;
- }
-
- var messages = new List ();
- var task = CreateTask (new [] { new TaskItem (nunitDll) }, outputDir, javaDir, messages);
-
- Assert.IsTrue (task.Execute (), "Task should succeed with no peer types.");
- Assert.IsNull (task.GeneratedAssemblies);
- Assert.IsNull (task.GeneratedJavaFiles);
- Assert.IsTrue (messages.Any (m => m.Message.Contains ("No Java peer types found")),
- "Should log that no peers were found.");
- }
-
GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir,
IList? messages = null, string tfv = "v11.0")
{
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs
index 3b0d79e43d8..fcfe51cb1df 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs
@@ -1,4 +1,7 @@
+using System.IO;
using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
using Xamarin.Android.Tasks;
using Xunit;
@@ -32,7 +35,12 @@ public void ExactMarshalMethods_MonoAndroid ()
public void ScannerDiagnostics_MonoAndroid ()
{
using var scanner = new JavaPeerScanner ();
-var peers = scanner.Scan (new [] { MonoAndroidAssemblyPath });
+var peReader = new PEReader (File.OpenRead (MonoAndroidAssemblyPath));
+var mdReader = peReader.GetMetadataReader ();
+var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name);
+var assemblies = new [] { (assemblyName, peReader) };
+var peers = scanner.Scan (assemblies);
+peReader.Dispose ();
var interfaces = peers.Count (p => p.IsInterface);
var totalMethods = peers.Sum (p => p.MarshalMethods.Count);
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs
index 10d4cea2f57..d01d02746de 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs
@@ -2,10 +2,14 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
using Java.Interop.Tools.Cecil;
using Java.Interop.Tools.JavaCallableWrappers.Adapters;
using Microsoft.Build.Utilities;
using Mono.Cecil;
+using CecilAssemblyDefinition = Mono.Cecil.AssemblyDefinition;
+using CecilTypeDefinition = Mono.Cecil.TypeDefinition;
using Xamarin.Android.Tasks;
namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests;
@@ -30,7 +34,7 @@ public static (List entries, Dictionary entries, Dictionary entries, Dictionary> methodsByJavaName) RunNew (string[] assemblyPaths)
{
- var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]);
using var scanner = new JavaPeerScanner ();
- var allPeers = scanner.Scan (assemblyPaths);
+ var peReaders = new List ();
+ var assemblies = new List<(string Name, PEReader Reader)> ();
+ List allPeers;
+ string primaryAssemblyName;
+ try {
+ foreach (var path in assemblyPaths) {
+ var peReader = new PEReader (File.OpenRead (path));
+ peReaders.Add (peReader);
+ var mdReader = peReader.GetMetadataReader ();
+ assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader));
+ }
+ primaryAssemblyName = assemblies [0].Name;
+ allPeers = scanner.Scan (assemblies);
+ } finally {
+ foreach (var peReader in peReaders) {
+ peReader.Dispose ();
+ }
+ }
var peers = allPeers.Where (p => p.AssemblyName == primaryAssemblyName).ToList ();
var entries = peers
@@ -114,7 +134,7 @@ public static (List entries, Dictionary entries, Dictionary.
///
- static List ExtractMethodRegistrations (TypeDefinition typeDef, TypeDefinitionCache cache)
+ static List ExtractMethodRegistrations (CecilTypeDefinition typeDef, TypeDefinitionCache cache)
{
if (typeDef.IsInterface) {
// CecilImporter throws XA4200 for interfaces.
@@ -174,7 +194,7 @@ static List ExtractMethodRegistrations (TypeDefinition typeDef, Typ
return methods;
}
- internal static bool HasDoNotGenerateAcw (TypeDefinition typeDef)
+ internal static bool HasDoNotGenerateAcw (CecilTypeDefinition typeDef)
{
if (!typeDef.HasCustomAttributes) {
return false;
@@ -195,7 +215,7 @@ internal static bool HasDoNotGenerateAcw (TypeDefinition typeDef)
/// Fallback: extract [Register] from methods/properties directly (for interfaces
/// and DoNotGenerateAcw types that are never passed through CecilImporter).
///
- static List ExtractDirectRegisterAttributes (TypeDefinition typeDef)
+ static List ExtractDirectRegisterAttributes (CecilTypeDefinition typeDef)
{
var methods = new List ();
foreach (var method in typeDef.Methods) {
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs
index d003eb73e07..df1d24e1ea6 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs
@@ -2,11 +2,15 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
using Java.Interop.Tools.Cecil;
using Java.Interop.Tools.JavaCallableWrappers.Adapters;
using Java.Interop.Tools.TypeNameMappings;
using Microsoft.Build.Utilities;
using Mono.Cecil;
+using CecilAssemblyDefinition = Mono.Cecil.AssemblyDefinition;
+using CecilTypeDefinition = Mono.Cecil.TypeDefinition;
using Xamarin.Android.Tasks;
namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests;
@@ -40,7 +44,7 @@ public static (Dictionary perType, List perType, List BuildNew (string[] assemblyPaths)
{
- var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]);
using var scanner = new JavaPeerScanner ();
- var peers = scanner.Scan (assemblyPaths);
+ var peReaders = new List ();
+ var assemblies = new List<(string Name, PEReader Reader)> ();
+ foreach (var path in assemblyPaths) {
+ var peReader = new PEReader (File.OpenRead (path));
+ peReaders.Add (peReader);
+ var mdReader = peReader.GetMetadataReader ();
+ assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader));
+ }
+ var primaryAssemblyName = assemblies [0].Name;
+ var peers = scanner.Scan (assemblies);
+ foreach (var peReader in peReaders) peReader.Dispose ();
var perType = new Dictionary (StringComparer.Ordinal);
@@ -185,14 +198,14 @@ public static Dictionary BuildNew (string[] assembly
return perType;
}
- static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCache cache,
+ static void FindLegacyActivationCtor (CecilTypeDefinition typeDef, TypeDefinitionCache cache,
out bool found, out string? declaringType, out string? style)
{
found = false;
declaringType = null;
style = null;
- TypeDefinition? current = typeDef;
+ CecilTypeDefinition? current = typeDef;
while (current != null) {
foreach (var method in current.Methods) {
if (!method.IsConstructor || method.IsStatic || method.Parameters.Count != 2) {
@@ -222,7 +235,7 @@ static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCach
}
}
- static bool GetCecilDoNotGenerateAcw (TypeDefinition typeDef)
+ static bool GetCecilDoNotGenerateAcw (CecilTypeDefinition typeDef)
{
if (!typeDef.HasCustomAttributes) {
return false;
@@ -245,7 +258,7 @@ static bool GetCecilDoNotGenerateAcw (TypeDefinition typeDef)
return false;
}
- static void ExtractDirectRegisterCtors (TypeDefinition typeDef, List javaCtorSignatures)
+ static void ExtractDirectRegisterCtors (CecilTypeDefinition typeDef, List javaCtorSignatures)
{
foreach (var method in typeDef.Methods) {
if (!method.IsConstructor || method.IsStatic || !method.HasCustomAttributes) {
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
index e04c700f845..5cf9e1cabe9 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
+using System.Reflection.PortableExecutable;
using Xunit;
namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
@@ -23,8 +24,13 @@ static string TestFixtureAssemblyPath {
static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => {
var scanner = new JavaPeerScanner ();
- var peers = scanner.Scan (new [] { TestFixtureAssemblyPath });
+ var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath));
+ var mdReader = peReader.GetMetadataReader ();
+ var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name);
+ var assemblies = new [] { (assemblyName, peReader) };
+ var peers = scanner.Scan (assemblies);
var manifestInfo = scanner.ScanAssemblyManifestInfo ();
+ peReader.Dispose ();
return (peers, manifestInfo);
});
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs
new file mode 100644
index 00000000000..7be68db2eb4
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class TrimmableTypeMapGeneratorTests : FixtureTestBase
+{
+ readonly List logMessages = new ();
+
+ [Fact]
+ public void Execute_EmptyAssemblyList_ReturnsEmptyResults ()
+ {
+ var result = CreateGenerator ().Execute ([], new Version (11, 0), new HashSet ());
+ Assert.Empty (result.GeneratedAssemblies);
+ Assert.Empty (result.GeneratedJavaSources);
+ Assert.Empty (result.AllPeers);
+ Assert.Contains (logMessages, m => m.Contains ("No Java peer types found"));
+ }
+
+ [Fact]
+ public void Execute_AssemblyWithNoPeers_ReturnsEmpty ()
+ {
+ // Use the test assembly itself — it has no [Register] types
+ var testAssemblyPath = typeof (TrimmableTypeMapGeneratorTests).Assembly.Location;
+ using var peReader = new PEReader (File.OpenRead (testAssemblyPath));
+ var result = CreateGenerator ().Execute (
+ new List<(string, PEReader)> { ("TestAssembly", peReader) },
+ new Version (11, 0),
+ new HashSet ());
+ Assert.Empty (result.GeneratedAssemblies);
+ Assert.Empty (result.GeneratedJavaSources);
+ Assert.Contains (logMessages, m => m.Contains ("No Java peer types found"));
+ }
+
+ [Fact]
+ public void Execute_WithTestFixtures_ProducesOutputs ()
+ {
+ using var peReader = CreateTestFixturePEReader ();
+ var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ());
+ Assert.NotEmpty (result.GeneratedAssemblies);
+ Assert.NotEmpty (result.GeneratedJavaSources);
+ Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_Microsoft.Android.TypeMaps");
+ Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap");
+ }
+
+ [Fact]
+ public void Execute_NullAssemblyList_Throws ()
+ {
+ IReadOnlyList<(string Name, PEReader Reader)>? n = null;
+#pragma warning disable CS8604
+ Assert.Throws (() => CreateGenerator ().Execute (n, new Version (11, 0), new HashSet ()));
+#pragma warning restore CS8604
+ }
+
+ [Fact]
+ public void Execute_GeneratedAssembliesAreValidPE ()
+ {
+ using var peReader = CreateTestFixturePEReader ();
+ var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ());
+ foreach (var assembly in result.GeneratedAssemblies) {
+ assembly.Content.Position = 0;
+ using var vr = new PEReader (assembly.Content, PEStreamOptions.LeaveOpen);
+ var md = vr.GetMetadataReader ();
+ Assert.Equal (assembly.Name, md.GetString (md.GetAssemblyDefinition ().Name));
+ }
+ }
+
+ [Fact]
+ public void Execute_JavaSourcesHaveCorrectStructure ()
+ {
+ using var peReader = CreateTestFixturePEReader ();
+ var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ());
+ foreach (var source in result.GeneratedJavaSources)
+ Assert.Contains ("class ", source.Content);
+ }
+
+ TrimmableTypeMapGenerator CreateGenerator () => new (msg => logMessages.Add (msg));
+
+ static PEReader CreateTestFixturePEReader ()
+ {
+ var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)
+ ?? throw new InvalidOperationException ("Cannot determine test assembly directory");
+ return new PEReader (File.OpenRead (Path.Combine (dir, "TestFixtures.dll")));
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
index 10489637722..d892b0161da 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -82,7 +82,6 @@ public void Generate_ProxyType_HasCtorAndCreateInstance ()
Assert.Contains (".ctor", methods);
Assert.Contains ("CreateInstance", methods);
- Assert.Contains ("get_TargetType", methods);
}
[Fact]
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
index 7a91bc0a029..54ec3dec323 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -561,7 +561,6 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers ()
Assert.Contains (".ctor", methodNames);
Assert.Contains ("CreateInstance", methodNames);
- Assert.Contains ("get_TargetType", methodNames);
});
}