From a399675f208af2d26f0000db49bf3931b8cf3bad Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 10:59:57 +0100 Subject: [PATCH 01/15] [TrimmableTypeMap] Extract TrimmableTypeMapGenerator from MSBuild task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the core generation pipeline (scan → typemaps → JCW → acw-map) from GenerateTrimmableTypeMap into a standalone TrimmableTypeMapGenerator class that takes Action for logging, keeping the TrimmableTypeMap project free of Microsoft.Build.* dependencies. The MSBuild task becomes a thin adapter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../NullableExtensions.cs | 18 ++ .../TrimmableTypeMapGenerator.cs | 167 ++++++++++++++ .../TrimmableTypeMapTypes.cs | 10 + .../Tasks/GenerateTrimmableTypeMap.cs | 209 +++--------------- .../Tasks/GenerateTrimmableTypeMapTests.cs | 8 +- .../TypeMapAssemblyGeneratorTests.cs | 1 - .../Generator/TypeMapModelBuilderTests.cs | 1 - 7 files changed, 234 insertions(+), 180 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs 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/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs new file mode 100644 index 00000000000..90a1d3a4d4d --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Core logic for generating trimmable TypeMap assemblies, JCW Java sources, and acw-map files. +/// Extracted from the MSBuild task so it can be tested directly without MSBuild ceremony. +/// +public class TrimmableTypeMapGenerator +{ + readonly Action log; + + public TrimmableTypeMapGenerator (Action log) + { + if (log is null) { + throw new ArgumentNullException (nameof (log)); + } + this.log = log; + } + + /// + /// Runs the full generation pipeline: scan assemblies, generate typemap + /// assemblies, generate JCW Java sources, and write acw-map files. + /// + public TrimmableTypeMapResult Execute ( + IReadOnlyList assemblyPaths, + string outputDirectory, + string javaSourceOutputDirectory, + Version systemRuntimeVersion, + HashSet frameworkAssemblyNames, + string? acwMapOutputPath = null) + { + Directory.CreateDirectory (outputDirectory); + Directory.CreateDirectory (javaSourceOutputDirectory); + + var allPeers = ScanAssemblies (assemblyPaths); + + if (allPeers.Count == 0) { + log ("No Java peer types found, skipping typemap generation."); + return new TrimmableTypeMapResult ([], []); + } + + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths, outputDirectory); + + // Generate JCW .java files for user assemblies + framework Implementor types. + // Framework binding types already have compiled JCWs in the SDK but their constructors + // use the legacy TypeManager.Activate() JNI native which isn't available in the + // trimmable runtime. Implementor types (View_OnClickListenerImplementor, etc.) are + // in the mono.* Java package so we use the mono/ prefix to identify them. + // We generate fresh JCWs that use Runtime.registerNatives() for activation. + 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 generatedJavaFiles = GenerateJcwJavaSources (jcwPeers, javaSourceOutputDirectory); + + // Write acw-map.txt so _ConvertCustomView and _UpdateAndroidResgen can resolve custom view names. + if (!acwMapOutputPath.IsNullOrEmpty ()) { + using var writer = new StreamWriter (acwMapOutputPath, append: false); + AcwMapWriter.Write (writer, allPeers); + log ($"Written acw-map.txt with {allPeers.Count} entries to {acwMapOutputPath}."); + } + + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaFiles); + } + + // 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 ($"Scanned {assemblyPaths.Count} assemblies, found {peers.Count} Java peer types."); + return peers; + } + + List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, + IReadOnlyList assemblyPaths, string outputDir) + { + // 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 (outputDir, assemblyName + ".dll"); + perAssemblyNames.Add (assemblyName); + + if (IsUpToDate (outputPath, group.Key, sourcePathByName)) { + log ($" {assemblyName}: up to date, skipping"); + generatedAssemblies.Add (outputPath); + continue; + } + + var peers = group.ToList (); + generator.Generate (peers, outputPath, assemblyName); + generatedAssemblies.Add (outputPath); + anyRegenerated = true; + + log ($" {assemblyName}: {peers.Count} types"); + } + + // Root assembly references all per-assembly typemaps — regenerate if any changed + string rootOutputPath = Path.Combine (outputDir, "_Microsoft.Android.TypeMaps.dll"); + if (anyRegenerated || !File.Exists (rootOutputPath)) { + var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); + rootGenerator.Generate (perAssemblyNames, rootOutputPath); + log ($" Root: {perAssemblyNames.Count} per-assembly refs"); + } else { + log (" Root: up to date, skipping"); + } + generatedAssemblies.Add (rootOutputPath); + + log ($"Generated {generatedAssemblies.Count} typemap assemblies."); + return generatedAssemblies; + } + + internal 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); + } + + List GenerateJcwJavaSources (List allPeers, string javaSourceOutputDirectory) + { + var jcwGenerator = new JcwJavaSourceGenerator (); + var files = jcwGenerator.Generate (allPeers, javaSourceOutputDirectory); + log ($"Generated {files.Count} JCW Java source files."); + return files.ToList (); + } + + public 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/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs new file mode 100644 index 00000000000..4039fed2ac1 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Result of the trimmable type map generation. +/// +public record TrimmableTypeMapResult ( + IReadOnlyList GeneratedAssemblies, + IReadOnlyList GeneratedJavaFiles); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index ecb6529357f..78303fde6fb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -12,9 +12,8 @@ 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. +/// MSBuild task adapter for . +/// Opens files and maps ITaskItem to/from strings, then delegates to the core class. /// public class GenerateTrimmableTypeMap : AndroidTask { @@ -29,12 +28,14 @@ public class GenerateTrimmableTypeMap : AndroidTask [Required] public string JavaSourceOutputDirectory { get; set; } = ""; - /// - /// Directory for per-assembly acw-map.{AssemblyName}.txt files. - /// [Required] public string AcwMapDirectory { get; set; } = ""; + /// + /// Output path for the merged acw-map.txt consumed by _ConvertCustomView and _UpdateAndroidResgen. + /// + public string? AcwMapOutputFile { get; set; } + /// /// The .NET target framework version (e.g., "v11.0"). Used to set the System.Runtime /// assembly reference version in generated typemap assemblies. @@ -43,187 +44,47 @@ public class GenerateTrimmableTypeMap : AndroidTask public string TargetFrameworkVersion { get; set; } = ""; [Output] - public ITaskItem []? GeneratedAssemblies { get; set; } + public ITaskItem [] GeneratedAssemblies { get; set; } = []; [Output] - public ITaskItem []? GeneratedJavaFiles { get; set; } + 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; + var systemRuntimeVersion = TrimmableTypeMapGenerator.ParseTargetFrameworkVersion (TargetFrameworkVersion); + // Don't filter by HasMonoAndroidReference — ReferencePath items from the compiler + // don't carry this metadata. The scanner handles non-Java assemblies gracefully. + var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); + + // Framework binding types (Activity, View, etc.) already exist in java_runtime.dex and don't + // need JCW .java files. Framework Implementor types (mono/ prefix, e.g. OnClickListenerImplementor) + // DO need JCWs — they're included via the mono/ filter below. + // User NuGet libraries also need JCWs, so we only filter by FrameworkReferenceName. + // Note: Pre-generating SDK-compatible JCWs (mono.android-trimmable.jar) is tracked by #10792. + var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); + foreach (var item in ResolvedAssemblies) { + if (!item.GetMetadata ("FrameworkReferenceName").IsNullOrEmpty ()) { + frameworkAssemblyNames.Add (Path.GetFileNameWithoutExtension (item.ItemSpec)); } - - 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"); + Directory.CreateDirectory (AcwMapDirectory); - var item = new TaskItem (outputFile); - item.SetMetadata ("AssemblyName", group.Key); - outputFiles.Add (item); - } + var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); + var result = generator.Execute ( + assemblyPaths, + OutputDirectory, + JavaSourceOutputDirectory, + systemRuntimeVersion, + frameworkAssemblyNames, + AcwMapOutputFile); - Log.LogDebugMessage ($"Generated {outputFiles.Count} per-assembly ACW map files."); - return outputFiles.ToArray (); - } + GeneratedAssemblies = result.GeneratedAssemblies.Select (p => (ITaskItem) new TaskItem (p)).ToArray (); + GeneratedJavaFiles = result.GeneratedJavaFiles.Select (p => (ITaskItem) new TaskItem (p)).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; + return !Log.HasLoggedErrors; } } 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..426a6fa5f6d 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 @@ -24,8 +24,8 @@ 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] @@ -198,8 +198,8 @@ public void Execute_NoPeersFound_ReturnsEmpty () 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.IsEmpty (task.GeneratedAssemblies); + Assert.IsEmpty (task.GeneratedJavaFiles); Assert.IsTrue (messages.Any (m => m.Message.Contains ("No Java peer types found")), "Should log that no peers were found."); } 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); }); } From bcde9df1fc72891cd319d960ea20dff53534f953 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 11:07:12 +0100 Subject: [PATCH 02/15] Keep per-assembly acw-map generation in MSBuild task The per-assembly acw-map files are consumed by existing targets. Keep GeneratePerAssemblyAcwMaps in the task (it uses MSBuild types like MemoryStreamPool and Files) rather than moving the merged acw-map.txt logic to the generator, which is PR 4 work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 14 ++---- .../TrimmableTypeMapTypes.cs | 3 +- .../Tasks/GenerateTrimmableTypeMap.cs | 46 ++++++++++++++++--- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 90a1d3a4d4d..225899d2beb 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -30,8 +30,7 @@ public TrimmableTypeMapResult Execute ( string outputDirectory, string javaSourceOutputDirectory, Version systemRuntimeVersion, - HashSet frameworkAssemblyNames, - string? acwMapOutputPath = null) + HashSet frameworkAssemblyNames) { Directory.CreateDirectory (outputDirectory); Directory.CreateDirectory (javaSourceOutputDirectory); @@ -40,7 +39,7 @@ public TrimmableTypeMapResult Execute ( if (allPeers.Count == 0) { log ("No Java peer types found, skipping typemap generation."); - return new TrimmableTypeMapResult ([], []); + return new TrimmableTypeMapResult ([], [], allPeers); } var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths, outputDirectory); @@ -57,14 +56,7 @@ public TrimmableTypeMapResult Execute ( log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total)."); var generatedJavaFiles = GenerateJcwJavaSources (jcwPeers, javaSourceOutputDirectory); - // Write acw-map.txt so _ConvertCustomView and _UpdateAndroidResgen can resolve custom view names. - if (!acwMapOutputPath.IsNullOrEmpty ()) { - using var writer = new StreamWriter (acwMapOutputPath, append: false); - AcwMapWriter.Write (writer, allPeers); - log ($"Written acw-map.txt with {allPeers.Count} entries to {acwMapOutputPath}."); - } - - return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaFiles); + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaFiles, allPeers); } // Future optimization: the scanner currently scans all assemblies on every run. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index 4039fed2ac1..45f2fe30cf8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -7,4 +7,5 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// public record TrimmableTypeMapResult ( IReadOnlyList GeneratedAssemblies, - IReadOnlyList GeneratedJavaFiles); + IReadOnlyList GeneratedJavaFiles, + IReadOnlyList AllPeers); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 78303fde6fb..b379c68674a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -31,11 +31,6 @@ public class GenerateTrimmableTypeMap : AndroidTask [Required] public string AcwMapDirectory { get; set; } = ""; - /// - /// Output path for the merged acw-map.txt consumed by _ConvertCustomView and _UpdateAndroidResgen. - /// - public string? AcwMapOutputFile { get; set; } - /// /// The .NET target framework version (e.g., "v11.0"). Used to set the System.Runtime /// assembly reference version in generated typemap assemblies. @@ -49,6 +44,11 @@ public class GenerateTrimmableTypeMap : AndroidTask [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; } @@ -79,12 +79,44 @@ public override bool RunTask () OutputDirectory, JavaSourceOutputDirectory, systemRuntimeVersion, - frameworkAssemblyNames, - AcwMapOutputFile); + frameworkAssemblyNames); GeneratedAssemblies = result.GeneratedAssemblies.Select (p => (ITaskItem) new TaskItem (p)).ToArray (); GeneratedJavaFiles = result.GeneratedJavaFiles.Select (p => (ITaskItem) new TaskItem (p)).ToArray (); + PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers); return !Log.HasLoggedErrors; } + + 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"); + + 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 (); + } } From 24d340cf9c626da27636a964ca1cec5c51029476 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 11:10:35 +0100 Subject: [PATCH 03/15] Address review: restore assembly filtering, add input validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restore GetJavaInteropAssemblyPaths (MonoAndroidHelper.IsMonoAndroidAssembly) filtering to match main's behavior — removing the filter is a build pipeline change that belongs in a later PR. - Add ArgumentNullException checks to TrimmableTypeMapGenerator.Execute(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 16 ++++++++++++++++ .../Tasks/GenerateTrimmableTypeMap.cs | 15 ++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 225899d2beb..dfebb01e207 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -32,6 +32,22 @@ public TrimmableTypeMapResult Execute ( Version systemRuntimeVersion, HashSet frameworkAssemblyNames) { + if (assemblyPaths is null) { + throw new ArgumentNullException (nameof (assemblyPaths)); + } + if (outputDirectory is null) { + throw new ArgumentNullException (nameof (outputDirectory)); + } + if (javaSourceOutputDirectory is null) { + throw new ArgumentNullException (nameof (javaSourceOutputDirectory)); + } + if (systemRuntimeVersion is null) { + throw new ArgumentNullException (nameof (systemRuntimeVersion)); + } + if (frameworkAssemblyNames is null) { + throw new ArgumentNullException (nameof (frameworkAssemblyNames)); + } + Directory.CreateDirectory (outputDirectory); Directory.CreateDirectory (javaSourceOutputDirectory); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index b379c68674a..c66f5a75af4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -55,9 +55,7 @@ public class GenerateTrimmableTypeMap : AndroidTask public override bool RunTask () { var systemRuntimeVersion = TrimmableTypeMapGenerator.ParseTargetFrameworkVersion (TargetFrameworkVersion); - // Don't filter by HasMonoAndroidReference — ReferencePath items from the compiler - // don't carry this metadata. The scanner handles non-Java assemblies gracefully. - var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); + var assemblyPaths = GetJavaInteropAssemblyPaths (ResolvedAssemblies); // Framework binding types (Activity, View, etc.) already exist in java_runtime.dex and don't // need JCW .java files. Framework Implementor types (mono/ prefix, e.g. OnClickListenerImplementor) @@ -88,6 +86,17 @@ public override bool RunTask () return !Log.HasLoggedErrors; } + 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; + } + ITaskItem [] GeneratePerAssemblyAcwMaps (IReadOnlyList allPeers) { var peersByAssembly = allPeers From e3553d5f230a25b7406aab6f0c9202b56d1e8751 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 11:15:34 +0100 Subject: [PATCH 04/15] =?UTF-8?q?Split=20tests:=20generator=20logic=20?= =?UTF-8?q?=E2=86=92=20TrimmableTypeMap.Tests,=20MSBuild=20=E2=86=92=20Bui?= =?UTF-8?q?ld.Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move incremental build, parsing, and empty-input tests to TrimmableTypeMapGeneratorTests (xUnit, uses TestFixtures.dll). Keep MSBuild-specific tests (error handling, Mono.Android integration) in GenerateTrimmableTypeMapTests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMapTests.cs | 162 ++-------------- .../TrimmableTypeMapGeneratorTests.cs | 178 ++++++++++++++++++ 2 files changed, 196 insertions(+), 144 deletions(-) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs 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 426a6fa5f6d..5914af480d9 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; @@ -28,123 +27,6 @@ public void Execute_EmptyAssemblyList_Succeeds () Assert.IsEmpty (task.GeneratedJavaFiles); } - [Test] - public void Execute_WithMonoAndroid_ProducesOutputs () - { - var path = Path.Combine ("temp", TestName); - var outputDir = Path.Combine (Root, path, "typemap"); - var javaDir = Path.Combine (Root, path, "java"); - - var monoAndroidItem = FindMonoAndroidDll (); - if (monoAndroidItem is null) { - Assert.Ignore ("Mono.Android.dll not found; skipping."); - return; - } - - var task = CreateTask (new [] { monoAndroidItem }, outputDir, javaDir); - - Assert.IsTrue (task.Execute (), "Task should succeed."); - Assert.IsNotNull (task.GeneratedAssemblies); - Assert.IsNotEmpty (task.GeneratedAssemblies); - - var assemblyPaths = task.GeneratedAssemblies.Select (i => i.ItemSpec).ToList (); - Assert.IsTrue (assemblyPaths.Any (p => p.Contains ("_Microsoft.Android.TypeMaps.dll")), - "Should produce root _Microsoft.Android.TypeMaps.dll"); - Assert.IsTrue (assemblyPaths.Any (p => p.Contains ("_Mono.Android.TypeMap.dll")), - "Should produce _Mono.Android.TypeMap.dll"); - - foreach (var assembly in task.GeneratedAssemblies) { - FileAssert.Exists (assembly.ItemSpec); - } - } - - [Test] - public void Execute_SecondRun_SkipsUpToDateAssemblies () - { - var path = Path.Combine ("temp", TestName); - var outputDir = Path.Combine (Root, path, "typemap"); - var javaDir = Path.Combine (Root, path, "java"); - - var monoAndroidItem = FindMonoAndroidDll (); - if (monoAndroidItem is null) { - Assert.Ignore ("Mono.Android.dll not found; skipping."); - return; - } - - var assemblies = new [] { monoAndroidItem }; - - // First run: generates everything - 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); - - // 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); - 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 () { @@ -166,42 +48,34 @@ public void Execute_InvalidTargetFrameworkVersion_Fails () Assert.IsNotEmpty (errors, "Should have logged an error."); } - [TestCase ("v11.0")] - [TestCase ("v10.0")] - [TestCase ("11.0")] - public void Execute_ParsesTargetFrameworkVersion (string tfv) - { - var path = Path.Combine ("temp", TestName); - var outputDir = Path.Combine (Root, path, "typemap"); - var javaDir = Path.Combine (Root, path, "java"); - - var task = CreateTask ([], outputDir, javaDir, tfv: tfv); - Assert.IsTrue (task.Execute (), $"Task should succeed with TargetFrameworkVersion='{tfv}'."); - } - [Test] - public void Execute_NoPeersFound_ReturnsEmpty () + public void Execute_WithMonoAndroid_ProducesOutputs () { 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."); + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); return; } - var messages = new List (); - var task = CreateTask (new [] { new TaskItem (nunitDll) }, outputDir, javaDir, messages); + var task = CreateTask (new [] { monoAndroidItem }, outputDir, javaDir); - Assert.IsTrue (task.Execute (), "Task should succeed with no peer types."); - Assert.IsEmpty (task.GeneratedAssemblies); - Assert.IsEmpty (task.GeneratedJavaFiles); - Assert.IsTrue (messages.Any (m => m.Message.Contains ("No Java peer types found")), - "Should log that no peers were found."); + Assert.IsTrue (task.Execute (), "Task should succeed."); + Assert.IsNotNull (task.GeneratedAssemblies); + Assert.IsNotEmpty (task.GeneratedAssemblies); + + var assemblyPaths = task.GeneratedAssemblies.Select (i => i.ItemSpec).ToList (); + Assert.IsTrue (assemblyPaths.Any (p => p.Contains ("_Microsoft.Android.TypeMaps.dll")), + "Should produce root _Microsoft.Android.TypeMaps.dll"); + Assert.IsTrue (assemblyPaths.Any (p => p.Contains ("_Mono.Android.TypeMap.dll")), + "Should produce _Mono.Android.TypeMap.dll"); + + foreach (var assembly in task.GeneratedAssemblies) { + FileAssert.Exists (assembly.ItemSpec); + } } GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir, 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..d125226a8d8 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class TrimmableTypeMapGeneratorTests : FixtureTestBase, IDisposable +{ + readonly string testDir; + readonly List logMessages = new (); + + public TrimmableTypeMapGeneratorTests () + { + testDir = Path.Combine (Path.GetTempPath (), "TrimmableTypeMapGeneratorTests", Guid.NewGuid ().ToString ("N")); + Directory.CreateDirectory (testDir); + } + + public void Dispose () + { + if (Directory.Exists (testDir)) { + Directory.Delete (testDir, recursive: true); + } + } + + [Fact] + public void Execute_EmptyAssemblyList_ReturnsEmptyResults () + { + var generator = CreateGenerator (); + var result = generator.Execute ( + Array.Empty (), + Path.Combine (testDir, "typemap"), + Path.Combine (testDir, "java"), + new Version (11, 0), + new HashSet ()); + + Assert.Empty (result.GeneratedAssemblies); + Assert.Empty (result.GeneratedJavaFiles); + Assert.Empty (result.AllPeers); + Assert.Contains (logMessages, m => m.Contains ("No Java peer types found")); + } + + [Fact] + public void Execute_WithTestFixtures_ProducesOutputs () + { + var assemblyPath = GetTestFixtureAssemblyPath (); + var outputDir = Path.Combine (testDir, "typemap"); + var javaDir = Path.Combine (testDir, "java"); + + var generator = CreateGenerator (); + var result = generator.Execute ( + new [] { assemblyPath }, + outputDir, + javaDir, + new Version (11, 0), + new HashSet ()); + + Assert.NotEmpty (result.GeneratedAssemblies); + Assert.NotEmpty (result.GeneratedJavaFiles); + Assert.NotEmpty (result.AllPeers); + + Assert.Contains (result.GeneratedAssemblies, p => p.Contains ("_Microsoft.Android.TypeMaps.dll")); + Assert.Contains (result.GeneratedAssemblies, p => p.Contains ("_TestFixtures.TypeMap.dll")); + + foreach (var assembly in result.GeneratedAssemblies) { + Assert.True (File.Exists (assembly), $"Generated assembly should exist: {assembly}"); + } + foreach (var javaFile in result.GeneratedJavaFiles) { + Assert.True (File.Exists (javaFile), $"Generated Java file should exist: {javaFile}"); + } + } + + [Fact] + public void Execute_SecondRun_SkipsUpToDateAssemblies () + { + var assemblyPath = GetTestFixtureAssemblyPath (); + var outputDir = Path.Combine (testDir, "typemap"); + var javaDir = Path.Combine (testDir, "java"); + var args = new object [] { assemblyPath, outputDir, javaDir }; + + // First run + var generator1 = CreateGenerator (); + var result1 = generator1.Execute ( + new [] { assemblyPath }, outputDir, javaDir, + new Version (11, 0), new HashSet ()); + + var typeMapPath = result1.GeneratedAssemblies.First (p => p.Contains ("_TestFixtures.TypeMap.dll")); + var firstWriteTime = File.GetLastWriteTimeUtc (typeMapPath); + + // Second run with fresh log + logMessages.Clear (); + var generator2 = CreateGenerator (); + generator2.Execute ( + new [] { assemblyPath }, outputDir, javaDir, + new Version (11, 0), new HashSet ()); + + var secondWriteTime = File.GetLastWriteTimeUtc (typeMapPath); + Assert.Equal (firstWriteTime, secondWriteTime); + Assert.Contains (logMessages, m => m.Contains ("up to date")); + } + + [Fact] + public void Execute_SourceTouched_RegeneratesAssembly () + { + var originalPath = GetTestFixtureAssemblyPath (); + var tempDir = Path.Combine (testDir, "assemblies"); + Directory.CreateDirectory (tempDir); + var assemblyPath = Path.Combine (tempDir, "TestFixtures.dll"); + File.Copy (originalPath, assemblyPath); + + var outputDir = Path.Combine (testDir, "typemap"); + var javaDir = Path.Combine (testDir, "java"); + + // First run + var generator1 = CreateGenerator (); + var result1 = generator1.Execute ( + new [] { assemblyPath }, outputDir, javaDir, + new Version (11, 0), new HashSet ()); + + var typeMapPath = result1.GeneratedAssemblies.First (p => p.Contains ("_TestFixtures.TypeMap.dll")); + var firstWriteTime = File.GetLastWriteTimeUtc (typeMapPath); + + // Touch the source assembly + File.SetLastWriteTimeUtc (assemblyPath, DateTime.UtcNow.AddSeconds (1)); + + // Second run + var generator2 = CreateGenerator (); + var result2 = generator2.Execute ( + new [] { assemblyPath }, outputDir, javaDir, + new Version (11, 0), new HashSet ()); + + var secondWriteTime = File.GetLastWriteTimeUtc (typeMapPath); + Assert.True (secondWriteTime > firstWriteTime, + "Typemap assembly should be regenerated when source is touched."); + } + + [Theory] + [InlineData ("v11.0")] + [InlineData ("v10.0")] + [InlineData ("11.0")] + public void ParseTargetFrameworkVersion_ValidInput_Succeeds (string tfv) + { + var version = TrimmableTypeMapGenerator.ParseTargetFrameworkVersion (tfv); + Assert.NotNull (version); + } + + [Theory] + [InlineData ("not-a-version")] + [InlineData ("")] + public void ParseTargetFrameworkVersion_InvalidInput_Throws (string tfv) + { + Assert.Throws (() => TrimmableTypeMapGenerator.ParseTargetFrameworkVersion (tfv)); + } + + [Fact] + public void Execute_NullAssemblyPaths_Throws () + { + var generator = CreateGenerator (); + Assert.Throws (() => generator.Execute ( + null!, Path.Combine (testDir, "out"), Path.Combine (testDir, "java"), + new Version (11, 0), new HashSet ())); + } + + TrimmableTypeMapGenerator CreateGenerator () + { + return new TrimmableTypeMapGenerator (msg => logMessages.Add (msg)); + } + + static string GetTestFixtureAssemblyPath () + { + var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); + var path = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (path), $"TestFixtures.dll not found at {path}"); + return path; + } +} From 6b6ff3db57747e11c44a6920417e2a82b0fac0f3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 11:17:49 +0100 Subject: [PATCH 05/15] Restore AcwMapDirectory doc comment, scan all assemblies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scanner needs all assemblies — not just those with HasMonoAndroidReference. Framework JCW pre-compilation is tracked by #10792; until then we generate JCWs for everything. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMap.cs | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index c66f5a75af4..3679734868a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -28,6 +28,9 @@ public class GenerateTrimmableTypeMap : AndroidTask [Required] public string JavaSourceOutputDirectory { get; set; } = ""; + /// + /// Directory for per-assembly acw-map.{AssemblyName}.txt files. + /// [Required] public string AcwMapDirectory { get; set; } = ""; @@ -55,19 +58,14 @@ public class GenerateTrimmableTypeMap : AndroidTask public override bool RunTask () { var systemRuntimeVersion = TrimmableTypeMapGenerator.ParseTargetFrameworkVersion (TargetFrameworkVersion); - var assemblyPaths = GetJavaInteropAssemblyPaths (ResolvedAssemblies); + // Don't filter by HasMonoAndroidReference — ReferencePath items from the compiler + // don't carry this metadata. The scanner handles non-Java assemblies gracefully. + var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); - // Framework binding types (Activity, View, etc.) already exist in java_runtime.dex and don't - // need JCW .java files. Framework Implementor types (mono/ prefix, e.g. OnClickListenerImplementor) - // DO need JCWs — they're included via the mono/ filter below. - // User NuGet libraries also need JCWs, so we only filter by FrameworkReferenceName. - // Note: Pre-generating SDK-compatible JCWs (mono.android-trimmable.jar) is tracked by #10792. + // Currently we generate JCWs for ALL assemblies including framework bindings. + // Pre-generating SDK-compatible JCWs (mono.android-trimmable.jar) is tracked by #10792. + // Once that's done, we can skip framework assemblies here. var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); - foreach (var item in ResolvedAssemblies) { - if (!item.GetMetadata ("FrameworkReferenceName").IsNullOrEmpty ()) { - frameworkAssemblyNames.Add (Path.GetFileNameWithoutExtension (item.ItemSpec)); - } - } Directory.CreateDirectory (AcwMapDirectory); @@ -86,17 +84,6 @@ public override bool RunTask () return !Log.HasLoggedErrors; } - 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; - } - ITaskItem [] GeneratePerAssemblyAcwMaps (IReadOnlyList allPeers) { var peersByAssembly = allPeers From b9ce5c68bb0337d5067ecf491f59e19507c8a175 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 11:21:32 +0100 Subject: [PATCH 06/15] Move ParseTargetFrameworkVersion to GenerateTrimmableTypeMap task This is MSBuild input parsing, not core generator logic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 11 ----------- .../Tasks/GenerateTrimmableTypeMap.cs | 13 ++++++++++++- .../TrimmableTypeMapGeneratorTests.cs | 18 ------------------ 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index dfebb01e207..efe7d0642c1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -161,15 +161,4 @@ List GenerateJcwJavaSources (List allPeers, string javaSou log ($"Generated {files.Count} JCW Java source files."); return files.ToList (); } - - public 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/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 3679734868a..513ed3ad1ee 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -57,7 +57,7 @@ public class GenerateTrimmableTypeMap : AndroidTask public override bool RunTask () { - var systemRuntimeVersion = TrimmableTypeMapGenerator.ParseTargetFrameworkVersion (TargetFrameworkVersion); + var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion); // Don't filter by HasMonoAndroidReference — ReferencePath items from the compiler // don't carry this metadata. The scanner handles non-Java assemblies gracefully. var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); @@ -115,4 +115,15 @@ ITaskItem [] GeneratePerAssemblyAcwMaps (IReadOnlyList allPeers) 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."); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index d125226a8d8..8c061a40c1b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -135,24 +135,6 @@ public void Execute_SourceTouched_RegeneratesAssembly () "Typemap assembly should be regenerated when source is touched."); } - [Theory] - [InlineData ("v11.0")] - [InlineData ("v10.0")] - [InlineData ("11.0")] - public void ParseTargetFrameworkVersion_ValidInput_Succeeds (string tfv) - { - var version = TrimmableTypeMapGenerator.ParseTargetFrameworkVersion (tfv); - Assert.NotNull (version); - } - - [Theory] - [InlineData ("not-a-version")] - [InlineData ("")] - public void ParseTargetFrameworkVersion_InvalidInput_Throws (string tfv) - { - Assert.Throws (() => TrimmableTypeMapGenerator.ParseTargetFrameworkVersion (tfv)); - } - [Fact] public void Execute_NullAssemblyPaths_Throws () { From 7a66696353a0ed3f118cf6535ca98c12b553f7de Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 13:04:08 +0100 Subject: [PATCH 07/15] Address review fixes --- .../Generator/TrimmableTypeMapGeneratorTests.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 8c061a40c1b..43ef7b1810c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -29,7 +29,7 @@ public void Execute_EmptyAssemblyList_ReturnsEmptyResults () { var generator = CreateGenerator (); var result = generator.Execute ( - Array.Empty (), + [], Path.Combine (testDir, "typemap"), Path.Combine (testDir, "java"), new Version (11, 0), @@ -77,7 +77,6 @@ public void Execute_SecondRun_SkipsUpToDateAssemblies () var assemblyPath = GetTestFixtureAssemblyPath (); var outputDir = Path.Combine (testDir, "typemap"); var javaDir = Path.Combine (testDir, "java"); - var args = new object [] { assemblyPath, outputDir, javaDir }; // First run var generator1 = CreateGenerator (); @@ -126,7 +125,7 @@ public void Execute_SourceTouched_RegeneratesAssembly () // Second run var generator2 = CreateGenerator (); - var result2 = generator2.Execute ( + generator2.Execute ( new [] { assemblyPath }, outputDir, javaDir, new Version (11, 0), new HashSet ()); @@ -139,9 +138,12 @@ public void Execute_SourceTouched_RegeneratesAssembly () public void Execute_NullAssemblyPaths_Throws () { var generator = CreateGenerator (); + string[]? nullPaths = null; +#pragma warning disable CS8604 // Possible null reference argument — intentionally testing null guard Assert.Throws (() => generator.Execute ( - null!, Path.Combine (testDir, "out"), Path.Combine (testDir, "java"), + nullPaths, Path.Combine (testDir, "out"), Path.Combine (testDir, "java"), new Version (11, 0), new HashSet ())); +#pragma warning restore CS8604 } TrimmableTypeMapGenerator CreateGenerator () From dc478cf8b30ab9dd764ff449c46c13db28a80c5a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 30 Mar 2026 13:14:31 +0200 Subject: [PATCH 08/15] Remove all IO from TrimmableTypeMapGenerator - Generator accepts (name, PEReader) pairs, returns in-memory content - MSBuild task owns all filesystem IO - Tests assert on in-memory content, no temp dirs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGenerator.cs | 27 +++- .../Scanner/AssemblyIndex.cs | 14 +- .../Scanner/JavaPeerScanner.cs | 20 ++- .../TrimmableTypeMapGenerator.cs | 137 ++++------------ .../TrimmableTypeMapTypes.cs | 12 +- .../Tasks/GenerateTrimmableTypeMap.cs | 113 ++++++------- .../TrimmableTypeMapGeneratorTests.cs | 153 ++++-------------- 7 files changed, 165 insertions(+), 311 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index aac84cd9803..07092f5555c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -75,10 +75,23 @@ public IReadOnlyList Generate (IReadOnlyList types, string return generatedFiles; } - /// + public IReadOnlyList GenerateContent (IReadOnlyList types) + { + if (types is null) throw new ArgumentNullException (nameof (types)); + var results = new List (); + foreach (var type in types) { + if (type.DoNotGenerateAcw || type.IsInterface) continue; + using var writer = new StringWriter (); + Generate (type, writer); + results.Add (new GeneratedJavaSource (GetRelativePath (type), writer.ToString ())); + } + 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,11 +104,15 @@ 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"; + } + + static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory) + { + return Path.Combine (outputDirectory, GetRelativePath (type)); } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 2d4803c8983..21fffebbbe0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -18,7 +18,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,13 +34,12 @@ 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) @@ -49,7 +47,15 @@ public static AssemblyIndex Create (string filePath) 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; + } + + public static AssemblyIndex Create (PEReader peReader, string assemblyName) + { + var reader = peReader.GetMetadataReader (); + var index = new AssemblyIndex (peReader, reader, assemblyName); index.Build (); return index; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 55fdd47a0a6..0c2516e9925 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; @@ -81,22 +82,29 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan /// public List Scan (IReadOnlyList assemblyPaths) { - // Phase 1: Build indices for all assemblies foreach (var path in assemblyPaths) { var index = AssemblyIndex.Create (path); assemblyCache [index.AssemblyName] = index; } + return ScanCore (); + } - // Phase 2: Analyze types using cached indices - var resultsByManagedName = new Dictionary (StringComparer.Ordinal); + public List Scan (IReadOnlyList<(string Name, PEReader Reader)> assemblies) + { + foreach (var (name, reader) in assemblies) { + var index = AssemblyIndex.Create (reader, name); + assemblyCache [index.AssemblyName] = index; + } + return ScanCore (); + } + List ScanCore () + { + 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 index efe7d0642c1..00a94cc3909 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -2,13 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.PortableExecutable; namespace Microsoft.Android.Sdk.TrimmableTypeMap; -/// -/// Core logic for generating trimmable TypeMap assemblies, JCW Java sources, and acw-map files. -/// Extracted from the MSBuild task so it can be tested directly without MSBuild ceremony. -/// public class TrimmableTypeMapGenerator { readonly Action log; @@ -21,144 +18,68 @@ public TrimmableTypeMapGenerator (Action log) this.log = log; } - /// - /// Runs the full generation pipeline: scan assemblies, generate typemap - /// assemblies, generate JCW Java sources, and write acw-map files. - /// public TrimmableTypeMapResult Execute ( - IReadOnlyList assemblyPaths, - string outputDirectory, - string javaSourceOutputDirectory, + IReadOnlyList<(string Name, PEReader Reader)> assemblies, Version systemRuntimeVersion, HashSet frameworkAssemblyNames) { - if (assemblyPaths is null) { - throw new ArgumentNullException (nameof (assemblyPaths)); - } - if (outputDirectory is null) { - throw new ArgumentNullException (nameof (outputDirectory)); - } - if (javaSourceOutputDirectory is null) { - throw new ArgumentNullException (nameof (javaSourceOutputDirectory)); - } - if (systemRuntimeVersion is null) { - throw new ArgumentNullException (nameof (systemRuntimeVersion)); - } - if (frameworkAssemblyNames is null) { - throw new ArgumentNullException (nameof (frameworkAssemblyNames)); - } - - Directory.CreateDirectory (outputDirectory); - Directory.CreateDirectory (javaSourceOutputDirectory); - - var allPeers = ScanAssemblies (assemblyPaths); + if (assemblies is null) throw new ArgumentNullException (nameof (assemblies)); + if (systemRuntimeVersion is null) throw new ArgumentNullException (nameof (systemRuntimeVersion)); + if (frameworkAssemblyNames is null) 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, assemblyPaths, outputDirectory); - - // Generate JCW .java files for user assemblies + framework Implementor types. - // Framework binding types already have compiled JCWs in the SDK but their constructors - // use the legacy TypeManager.Activate() JNI native which isn't available in the - // trimmable runtime. Implementor types (View_OnClickListenerImplementor, etc.) are - // in the mono.* Java package so we use the mono/ prefix to identify them. - // We generate fresh JCWs that use Runtime.registerNatives() for activation. + 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 generatedJavaFiles = GenerateJcwJavaSources (jcwPeers, javaSourceOutputDirectory); - - return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaFiles, allPeers); + var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers); } - // 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) + List ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies) { using var scanner = new JavaPeerScanner (); - var peers = scanner.Scan (assemblyPaths); - log ($"Scanned {assemblyPaths.Count} assemblies, found {peers.Count} Java peer types."); + var peers = scanner.Scan (assemblies); + log ($"Scanned {assemblies.Count} assemblies, found {peers.Count} Java peer types."); return peers; } - List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, - IReadOnlyList assemblyPaths, string outputDir) + List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion) { - // 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 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 (outputDir, assemblyName + ".dll"); perAssemblyNames.Add (assemblyName); - - if (IsUpToDate (outputPath, group.Key, sourcePathByName)) { - log ($" {assemblyName}: up to date, skipping"); - generatedAssemblies.Add (outputPath); - continue; - } - var peers = group.ToList (); - generator.Generate (peers, outputPath, assemblyName); - generatedAssemblies.Add (outputPath); - anyRegenerated = true; - + var stream = new MemoryStream (); + generator.Generate (peers, stream, assemblyName); + stream.Position = 0; + generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream)); log ($" {assemblyName}: {peers.Count} types"); } - - // Root assembly references all per-assembly typemaps — regenerate if any changed - string rootOutputPath = Path.Combine (outputDir, "_Microsoft.Android.TypeMaps.dll"); - if (anyRegenerated || !File.Exists (rootOutputPath)) { - var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, rootOutputPath); - log ($" Root: {perAssemblyNames.Count} per-assembly refs"); - } else { - log (" Root: up to date, skipping"); - } - generatedAssemblies.Add (rootOutputPath); - + 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; } - internal 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); - } - - List GenerateJcwJavaSources (List allPeers, string javaSourceOutputDirectory) + List GenerateJcwJavaSources (List allPeers) { var jcwGenerator = new JcwJavaSourceGenerator (); - var files = jcwGenerator.Generate (allPeers, javaSourceOutputDirectory); - log ($"Generated {files.Count} JCW Java source files."); - return files.ToList (); + 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 index 45f2fe30cf8..c10a2482bd2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; +using System.IO; namespace Microsoft.Android.Sdk.TrimmableTypeMap; -/// -/// Result of the trimmable type map generation. -/// public record TrimmableTypeMapResult ( - IReadOnlyList GeneratedAssemblies, - IReadOnlyList GeneratedJavaFiles, + 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 513ed3ad1ee..caf7765c92a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.PortableExecutable; using Microsoft.Android.Build.Tasks; using Microsoft.Android.Sdk.TrimmableTypeMap; using Microsoft.Build.Framework; @@ -11,119 +12,105 @@ namespace Xamarin.Android.Tasks; -/// -/// MSBuild task adapter for . -/// Opens files and maps ITaskItem to/from strings, then delegates to the core class. -/// 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); - // Don't filter by HasMonoAndroidReference — ReferencePath items from the compiler - // don't carry this metadata. The scanner handles non-Java assemblies gracefully. var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); - - // Currently we generate JCWs for ALL assemblies including framework bindings. - // Pre-generating SDK-compatible JCWs (mono.android-trimmable.jar) is tracked by #10792. - // Once that's done, we can skip framework assemblies here. 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)> (); + try { + foreach (var path in assemblyPaths) { + var peReader = new PEReader (File.OpenRead (path)); + peReaders.Add (peReader); + assemblies.Add ((Path.GetFileNameWithoutExtension (path), peReader)); + } + var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); + var result = generator.Execute (assemblies, systemRuntimeVersion, frameworkAssemblyNames); + GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies); + GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); + PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers); + } finally { + foreach (var peReader in peReaders) peReader.Dispose (); + } + return !Log.HasLoggedErrors; + } - var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); - var result = generator.Execute ( - assemblyPaths, - OutputDirectory, - JavaSourceOutputDirectory, - systemRuntimeVersion, - frameworkAssemblyNames); - - GeneratedAssemblies = result.GeneratedAssemblies.Select (p => (ITaskItem) new TaskItem (p)).ToArray (); - GeneratedJavaFiles = result.GeneratedJavaFiles.Select (p => (ITaskItem) new TaskItem (p)).ToArray (); - PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers); + ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies) + { + var items = new List (); + foreach (var assembly in assemblies) { + string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll"); + Files.CopyIfStreamChanged (assembly.Content, outputPath); + items.Add (new TaskItem (outputPath)); + } + return items.ToArray (); + } - return !Log.HasLoggedErrors; + 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 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); + 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; - } + 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/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 43ef7b1810c..ab30c3d7240 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -2,41 +2,22 @@ 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, IDisposable +public class TrimmableTypeMapGeneratorTests : FixtureTestBase { - readonly string testDir; readonly List logMessages = new (); - public TrimmableTypeMapGeneratorTests () - { - testDir = Path.Combine (Path.GetTempPath (), "TrimmableTypeMapGeneratorTests", Guid.NewGuid ().ToString ("N")); - Directory.CreateDirectory (testDir); - } - - public void Dispose () - { - if (Directory.Exists (testDir)) { - Directory.Delete (testDir, recursive: true); - } - } - [Fact] public void Execute_EmptyAssemblyList_ReturnsEmptyResults () { - var generator = CreateGenerator (); - var result = generator.Execute ( - [], - Path.Combine (testDir, "typemap"), - Path.Combine (testDir, "java"), - new Version (11, 0), - new HashSet ()); - + var result = CreateGenerator ().Execute ([], new Version (11, 0), new HashSet ()); Assert.Empty (result.GeneratedAssemblies); - Assert.Empty (result.GeneratedJavaFiles); + Assert.Empty (result.GeneratedJavaSources); Assert.Empty (result.AllPeers); Assert.Contains (logMessages, m => m.Contains ("No Java peer types found")); } @@ -44,119 +25,51 @@ public void Execute_EmptyAssemblyList_ReturnsEmptyResults () [Fact] public void Execute_WithTestFixtures_ProducesOutputs () { - var assemblyPath = GetTestFixtureAssemblyPath (); - var outputDir = Path.Combine (testDir, "typemap"); - var javaDir = Path.Combine (testDir, "java"); - - var generator = CreateGenerator (); - var result = generator.Execute ( - new [] { assemblyPath }, - outputDir, - javaDir, - new Version (11, 0), - new HashSet ()); - + 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.GeneratedJavaFiles); - Assert.NotEmpty (result.AllPeers); - - Assert.Contains (result.GeneratedAssemblies, p => p.Contains ("_Microsoft.Android.TypeMaps.dll")); - Assert.Contains (result.GeneratedAssemblies, p => p.Contains ("_TestFixtures.TypeMap.dll")); - - foreach (var assembly in result.GeneratedAssemblies) { - Assert.True (File.Exists (assembly), $"Generated assembly should exist: {assembly}"); - } - foreach (var javaFile in result.GeneratedJavaFiles) { - Assert.True (File.Exists (javaFile), $"Generated Java file should exist: {javaFile}"); - } + 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_SecondRun_SkipsUpToDateAssemblies () + public void Execute_NullAssemblyList_Throws () { - var assemblyPath = GetTestFixtureAssemblyPath (); - var outputDir = Path.Combine (testDir, "typemap"); - var javaDir = Path.Combine (testDir, "java"); - - // First run - var generator1 = CreateGenerator (); - var result1 = generator1.Execute ( - new [] { assemblyPath }, outputDir, javaDir, - new Version (11, 0), new HashSet ()); - - var typeMapPath = result1.GeneratedAssemblies.First (p => p.Contains ("_TestFixtures.TypeMap.dll")); - var firstWriteTime = File.GetLastWriteTimeUtc (typeMapPath); - - // Second run with fresh log - logMessages.Clear (); - var generator2 = CreateGenerator (); - generator2.Execute ( - new [] { assemblyPath }, outputDir, javaDir, - new Version (11, 0), new HashSet ()); - - var secondWriteTime = File.GetLastWriteTimeUtc (typeMapPath); - Assert.Equal (firstWriteTime, secondWriteTime); - Assert.Contains (logMessages, m => m.Contains ("up to date")); + 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_SourceTouched_RegeneratesAssembly () + public void Execute_GeneratedAssembliesAreValidPE () { - var originalPath = GetTestFixtureAssemblyPath (); - var tempDir = Path.Combine (testDir, "assemblies"); - Directory.CreateDirectory (tempDir); - var assemblyPath = Path.Combine (tempDir, "TestFixtures.dll"); - File.Copy (originalPath, assemblyPath); - - var outputDir = Path.Combine (testDir, "typemap"); - var javaDir = Path.Combine (testDir, "java"); - - // First run - var generator1 = CreateGenerator (); - var result1 = generator1.Execute ( - new [] { assemblyPath }, outputDir, javaDir, - new Version (11, 0), new HashSet ()); - - var typeMapPath = result1.GeneratedAssemblies.First (p => p.Contains ("_TestFixtures.TypeMap.dll")); - var firstWriteTime = File.GetLastWriteTimeUtc (typeMapPath); - - // Touch the source assembly - File.SetLastWriteTimeUtc (assemblyPath, DateTime.UtcNow.AddSeconds (1)); - - // Second run - var generator2 = CreateGenerator (); - generator2.Execute ( - new [] { assemblyPath }, outputDir, javaDir, - new Version (11, 0), new HashSet ()); - - var secondWriteTime = File.GetLastWriteTimeUtc (typeMapPath); - Assert.True (secondWriteTime > firstWriteTime, - "Typemap assembly should be regenerated when source is touched."); + 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_NullAssemblyPaths_Throws () + public void Execute_JavaSourcesHaveCorrectStructure () { - var generator = CreateGenerator (); - string[]? nullPaths = null; -#pragma warning disable CS8604 // Possible null reference argument — intentionally testing null guard - Assert.Throws (() => generator.Execute ( - nullPaths, Path.Combine (testDir, "out"), Path.Combine (testDir, "java"), - new Version (11, 0), new HashSet ())); -#pragma warning restore CS8604 + 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 () - { - return new TrimmableTypeMapGenerator (msg => logMessages.Add (msg)); - } + TrimmableTypeMapGenerator CreateGenerator () => new (msg => logMessages.Add (msg)); - static string GetTestFixtureAssemblyPath () + static PEReader CreateTestFixturePEReader () { - var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) + var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); - var path = Path.Combine (testAssemblyDir, "TestFixtures.dll"); - Assert.True (File.Exists (path), $"TestFixtures.dll not found at {path}"); - return path; + return new PEReader (File.OpenRead (Path.Combine (dir, "TestFixtures.dll"))); } } From 4342ba844fd886cee8507576ae3fd883dcf4b166 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 30 Mar 2026 14:03:25 +0200 Subject: [PATCH 09/15] Drop file-path scanner overloads, simplify null checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 12 ------------ .../Scanner/JavaPeerScanner.cs | 13 ------------- .../TrimmableTypeMapGenerator.cs | 12 +++++------- .../Tasks/GenerateTrimmableTypeMap.cs | 4 +++- .../ScannerComparisonTests.cs | 10 +++++++++- .../ScannerRunner.cs | 15 +++++++++++++-- .../TypeDataBuilder.cs | 15 +++++++++++++-- .../Generator/FixtureTestBase.cs | 8 +++++++- 8 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 21fffebbbe0..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; @@ -42,16 +41,6 @@ sealed class AssemblyIndex : IDisposable AssemblyName = assemblyName; } - public static AssemblyIndex Create (string filePath) - { - 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); - index.Build (); - return index; - } - public static AssemblyIndex Create (PEReader peReader, string assemblyName) { var reader = peReader.GetMetadataReader (); @@ -483,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 0c2516e9925..4b298352b75 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -80,26 +80,13 @@ 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) - { - foreach (var path in assemblyPaths) { - var index = AssemblyIndex.Create (path); - assemblyCache [index.AssemblyName] = index; - } - return ScanCore (); - } - public List Scan (IReadOnlyList<(string Name, PEReader Reader)> assemblies) { foreach (var (name, reader) in assemblies) { var index = AssemblyIndex.Create (reader, name); assemblyCache [index.AssemblyName] = index; } - return ScanCore (); - } - List ScanCore () - { var resultsByManagedName = new Dictionary (StringComparer.Ordinal); foreach (var index in assemblyCache.Values) { ScanAssembly (index, resultsByManagedName); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 00a94cc3909..1a2b0691057 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -12,10 +12,7 @@ public class TrimmableTypeMapGenerator public TrimmableTypeMapGenerator (Action log) { - if (log is null) { - throw new ArgumentNullException (nameof (log)); - } - this.log = log; + this.log = log ?? throw new ArgumentNullException (nameof (log)); } public TrimmableTypeMapResult Execute ( @@ -23,9 +20,9 @@ public TrimmableTypeMapResult Execute ( Version systemRuntimeVersion, HashSet frameworkAssemblyNames) { - if (assemblies is null) throw new ArgumentNullException (nameof (assemblies)); - if (systemRuntimeVersion is null) throw new ArgumentNullException (nameof (systemRuntimeVersion)); - if (frameworkAssemblyNames is null) throw new ArgumentNullException (nameof (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) { @@ -35,6 +32,7 @@ public TrimmableTypeMapResult Execute ( 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); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index caf7765c92a..dc98aab4fc1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -4,6 +4,7 @@ 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; @@ -47,7 +48,8 @@ public override bool RunTask () foreach (var path in assemblyPaths) { var peReader = new PEReader (File.OpenRead (path)); peReaders.Add (peReader); - assemblies.Add ((Path.GetFileNameWithoutExtension (path), peReader)); + var mdReader = peReader.GetMetadataReader (); + assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader)); } var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); var result = generator.Execute (assemblies, systemRuntimeVersion, frameworkAssemblyNames); 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..95fa6ad61c5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -2,6 +2,8 @@ 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; @@ -77,9 +79,18 @@ public static (List 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)> (); + 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 allPeers = scanner.Scan (assemblies); + foreach (var peReader in peReaders) peReader.Dispose (); var peers = allPeers.Where (p => p.AssemblyName == primaryAssemblyName).ToList (); var entries = peers diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs index d003eb73e07..30230af547e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs @@ -2,6 +2,8 @@ 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; @@ -135,9 +137,18 @@ public static (Dictionary 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); 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); }); From 8aaf126d8811495e06655829da0b2de3fca7623d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 30 Mar 2026 15:02:28 +0200 Subject: [PATCH 10/15] Remove file-path writing overloads from generator library All IO is now exclusively in the MSBuild task. The generator library only exposes stream/content-based APIs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGenerator.cs | 38 ------------------- .../Generator/PEAssemblyBuilder.cs | 14 ------- .../Generator/RootTypeMapAssemblyGenerator.cs | 22 ----------- .../Generator/TypeMapAssemblyEmitter.cs | 16 -------- .../Generator/TypeMapAssemblyGenerator.cs | 13 ------- .../ScannerComparisonTests.cs | 1 - .../ScannerRunner.cs | 1 - .../TypeDataBuilder.cs | 1 - 8 files changed, 106 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 07092f5555c..aba9bca4bb0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -41,40 +41,6 @@ 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. - /// - public IReadOnlyList Generate (IReadOnlyList types, string outputDirectory) - { - if (types is null) { - throw new ArgumentNullException (nameof (types)); - } - if (outputDirectory is null) { - throw new ArgumentNullException (nameof (outputDirectory)); - } - - var generatedFiles = 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); - Generate (type, writer); - generatedFiles.Add (filePath); - } - - return generatedFiles; - } - public IReadOnlyList GenerateContent (IReadOnlyList types) { if (types is null) throw new ArgumentNullException (nameof (types)); @@ -110,10 +76,6 @@ static string GetRelativePath (JavaPeerInfo type) return type.JavaName + ".java"; } - static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory) - { - return Path.Combine (outputDirectory, GetRelativePath (type)); - } /// /// Validates that the JNI name is well-formed: non-empty, each segment separated by '/' 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/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index fcfe51cb1df..877022a7979 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -1,6 +1,5 @@ using System.IO; using System.Linq; -using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using Xamarin.Android.Tasks; using Xunit; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs index 95fa6ad61c5..707809fc2bf 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -2,7 +2,6 @@ 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; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs index 30230af547e..f47c0f581fa 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs @@ -2,7 +2,6 @@ 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; From fab1cc2e687d35fa3879024c664d21cf101e595d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 30 Mar 2026 16:19:48 +0200 Subject: [PATCH 11/15] Address review: dispose MemoryStreams, add doc-comments, fix formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGenerator.cs | 6 +- .../Tasks/GenerateTrimmableTypeMap.cs | 274 +++++++++++------- .../ScannerComparisonTests.cs | 1 + .../ScannerRunner.cs | 14 +- .../TypeDataBuilder.cs | 12 +- 5 files changed, 195 insertions(+), 112 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index aba9bca4bb0..96bd0f729ab 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -41,6 +41,10 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// public sealed class JcwJavaSourceGenerator { + /// + /// Generates .java source content for all ACW types and returns them as in-memory + /// (relativePath, content) pairs. No filesystem IO is performed. + /// public IReadOnlyList GenerateContent (IReadOnlyList types) { if (types is null) throw new ArgumentNullException (nameof (types)); @@ -54,7 +58,7 @@ public IReadOnlyList GenerateContent (IReadOnlyList + /// /// Generates a single .java source file for the given type. /// public void Generate (JavaPeerInfo type, TextWriter writer) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index dc98aab4fc1..dbaa245e375 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -15,104 +15,178 @@ namespace Xamarin.Android.Tasks; 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; } = ""; - [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 (); - 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)> (); - 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)); - var result = generator.Execute (assemblies, systemRuntimeVersion, frameworkAssemblyNames); - GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies); - GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); - PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers); - } finally { - foreach (var peReader in peReaders) peReader.Dispose (); - } - return !Log.HasLoggedErrors; - } - - ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies) - { - var items = new List (); - foreach (var assembly in assemblies) { - string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll"); - Files.CopyIfStreamChanged (assembly.Content, outputPath); - items.Add (new TaskItem (outputPath)); - } - return items.ToArray (); - } - - 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."); - } +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 (); +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; +var perAssemblyItems = new List<(string Name, string OutputPath)> (); + +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)); +perAssemblyItems.Add ((assembly.Name, 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/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index 877022a7979..fcfe51cb1df 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using Xamarin.Android.Tasks; using Xunit; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs index 707809fc2bf..d9be002fdb8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -2,11 +2,13 @@ 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 CecilTypeDefinition = Mono.Cecil.TypeDefinition; using Xamarin.Android.Tasks; namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; @@ -21,7 +23,7 @@ static class ScannerRunner { public static (List entries, Dictionary> methodsByJavaName) RunLegacy (string assemblyPath) { - var cache = new TypeDefinitionCache (); + var cache = new CecilTypeDefinitionCache (); var resolver = new DefaultAssemblyResolver (); resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); @@ -124,7 +126,7 @@ public static (List entries, Dictionary entries, Dictionary. /// - static List ExtractMethodRegistrations (TypeDefinition typeDef, TypeDefinitionCache cache) + static List ExtractMethodRegistrations (CecilTypeDefinition typeDef, CecilTypeDefinitionCache cache) { if (typeDef.IsInterface) { // CecilImporter throws XA4200 for interfaces. @@ -184,7 +186,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; @@ -205,7 +207,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 f47c0f581fa..8ab3ecc2c7d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs @@ -2,12 +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 Java.Interop.Tools.TypeNameMappings; using Microsoft.Build.Utilities; using Mono.Cecil; +using CecilTypeDefinition = Mono.Cecil.TypeDefinition; using Xamarin.Android.Tasks; namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; @@ -31,7 +33,7 @@ static class TypeDataBuilder { public static (Dictionary perType, List entries) BuildLegacy (string assemblyPath) { - var cache = new TypeDefinitionCache (); + var cache = new CecilTypeDefinitionCache (); var resolver = new DefaultAssemblyResolver (); resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); @@ -195,14 +197,14 @@ public static Dictionary BuildNew (string[] assembly return perType; } - static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCache cache, + static void FindLegacyActivationCtor (CecilTypeDefinition typeDef, CecilTypeDefinitionCache 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) { @@ -232,7 +234,7 @@ static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCach } } - static bool GetCecilDoNotGenerateAcw (TypeDefinition typeDef) + static bool GetCecilDoNotGenerateAcw (CecilTypeDefinition typeDef) { if (!typeDef.HasCustomAttributes) { return false; @@ -255,7 +257,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) { From 6791efb5a345977eb602c10705f872e48735988d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 30 Mar 2026 17:53:51 +0200 Subject: [PATCH 12/15] Restore dropped test coverage: incrementality, TFV parsing, no-peers - Restore Execute_SecondRun_OutputsAreUpToDate at task level (verifies Files.CopyIfStreamChanged does not rewrite unchanged outputs) - Restore Execute_ParsesTargetFrameworkVersion at task level - Add Execute_AssemblyWithNoPeers_ReturnsEmpty at generator level (uses test assembly itself which has no [Register] types) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMapTests.cs | 46 +++++++++++++++++++ .../TrimmableTypeMapGeneratorTests.cs | 15 ++++++ 2 files changed, 61 insertions(+) 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 5914af480d9..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 @@ -78,6 +78,52 @@ public void Execute_WithMonoAndroid_ProducesOutputs () } } + [Test] + public void Execute_SecondRun_OutputsAreUpToDate () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + var assemblies = new [] { monoAndroidItem }; + + // First run: generates everything + 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); + + // 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 content hasn't changed."); + } + + [TestCase ("v11.0")] + [TestCase ("v10.0")] + [TestCase ("11.0")] + public void Execute_ParsesTargetFrameworkVersion (string tfv) + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + + var task = CreateTask ([], outputDir, javaDir, tfv: tfv); + Assert.IsTrue (task.Execute (), $"Task should succeed with TargetFrameworkVersion='{tfv}'."); + } + GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir, IList? messages = null, string tfv = "v11.0") { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index ab30c3d7240..7be68db2eb4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -22,6 +22,21 @@ public void Execute_EmptyAssemblyList_ReturnsEmptyResults () 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 () { From 5d596eacca6dc65dd693a3f0712ecc4118804cd2 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 30 Mar 2026 15:54:18 -0500 Subject: [PATCH 13/15] Fix CS0246: CecilTypeDefinitionCache -> TypeDefinitionCache The type CecilTypeDefinitionCache does not exist. The correct class name is TypeDefinitionCache from Java.Interop.Tools.Cecil namespace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ScannerRunner.cs | 4 ++-- .../TypeDataBuilder.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs index d9be002fdb8..85d2837d1ec 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -23,7 +23,7 @@ static class ScannerRunner { public static (List entries, Dictionary> methodsByJavaName) RunLegacy (string assemblyPath) { - var cache = new CecilTypeDefinitionCache (); + var cache = new TypeDefinitionCache (); var resolver = new DefaultAssemblyResolver (); resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); @@ -155,7 +155,7 @@ public static string GetManagedName (CecilTypeDefinition typeDef) /// Extracts marshal methods using the real legacy JCW pipeline via /// . /// - static List ExtractMethodRegistrations (CecilTypeDefinition typeDef, CecilTypeDefinitionCache cache) + static List ExtractMethodRegistrations (CecilTypeDefinition typeDef, TypeDefinitionCache cache) { if (typeDef.IsInterface) { // CecilImporter throws XA4200 for interfaces. diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs index 8ab3ecc2c7d..445ae7abb70 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs @@ -33,7 +33,7 @@ static class TypeDataBuilder { public static (Dictionary perType, List entries) BuildLegacy (string assemblyPath) { - var cache = new CecilTypeDefinitionCache (); + var cache = new TypeDefinitionCache (); var resolver = new DefaultAssemblyResolver (); resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); @@ -197,7 +197,7 @@ public static Dictionary BuildNew (string[] assembly return perType; } - static void FindLegacyActivationCtor (CecilTypeDefinition typeDef, CecilTypeDefinitionCache cache, + static void FindLegacyActivationCtor (CecilTypeDefinition typeDef, TypeDefinitionCache cache, out bool found, out string? declaringType, out string? style) { found = false; From ba00b01a9f97adcd71739af501b9b8c1e28fbc60 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 31 Mar 2026 01:41:58 +0200 Subject: [PATCH 14/15] Fix CS0104: disambiguate AssemblyDefinition between Cecil and SRM Add CecilAssemblyDefinition alias for Mono.Cecil.AssemblyDefinition to resolve ambiguity with System.Reflection.Metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ScannerRunner.cs | 3 ++- .../TypeDataBuilder.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs index 85d2837d1ec..c16c247c615 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -8,6 +8,7 @@ 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; @@ -33,7 +34,7 @@ public static (List entries, Dictionary perType, List Date: Tue, 31 Mar 2026 19:57:38 +0200 Subject: [PATCH 15/15] Address PR review: remove dead code, add TODO, fix PEReader disposal - Remove unused perAssemblyItems list (dead code) - Add TODO(#10792) comment for empty frameworkAssemblyNames - Wrap PEReader creation in try/finally in ScannerRunner.RunNew() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMap.cs | 3 +-- .../ScannerRunner.cs | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index dbaa245e375..4d621afd2a9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -38,6 +38,7 @@ 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); @@ -86,7 +87,6 @@ ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, var items = new List (); bool anyRegenerated = false; -var perAssemblyItems = new List<(string Name, string OutputPath)> (); foreach (var assembly in assemblies) { if (assembly.Name == "_Microsoft.Android.TypeMaps") { @@ -109,7 +109,6 @@ ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, } items.Add (new TaskItem (outputPath)); -perAssemblyItems.Add ((assembly.Name, outputPath)); } // Root assembly — regenerate if any per-assembly typemap changed diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs index c16c247c615..d01d02746de 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -84,15 +84,22 @@ public static (List entries, Dictionary (); 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)); + 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 primaryAssemblyName = assemblies [0].Name; - var allPeers = scanner.Scan (assemblies); - foreach (var peReader in peReaders) peReader.Dispose (); var peers = allPeers.Where (p => p.AssemblyName == primaryAssemblyName).ToList (); var entries = peers