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); }); }