From eae6b5016c6e1aad42ec3d176c2544e5a1bfb2d3 Mon Sep 17 00:00:00 2001 From: Jonathan Pobst Date: Tue, 8 Apr 2025 09:39:38 -1000 Subject: [PATCH 1/3] =?UTF-8?q?=EF=BB=BF[XABT]=20Move=20JLO=20scanning=20n?= =?UTF-8?q?eeded=20for=20typemap=20generation=20to=20a=20linker=20step.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MonoDroid.Tuner/AddKeepAlivesStep.cs | 6 +- .../MonoDroid.Tuner/FindJavaObjectsStep.cs | 6 +- .../MonoDroid.Tuner/FindTypeMapObjectsStep.cs | 87 ++++++ .../MonoDroid.Tuner/FixAbstractMethodsStep.cs | 6 +- .../FixLegacyResourceDesignerStep.cs | 6 +- .../Tasks/AssemblyModifierPipeline.cs | 81 ++++-- .../Tasks/GenerateTypeMappings.cs | 98 ++++++- .../Utilities/AssemblyPipeline.cs | 11 +- .../Utilities/TypeMapCecilAdapter.cs | 63 ++-- .../Utilities/TypeMapGenerator.cs | 171 +++++++++-- .../Utilities/TypeMapObjectsXmlFile.cs | 271 ++++++++++++++++++ .../Xamarin.Android.Build.Tasks.csproj | 1 + .../Xamarin.Android.Common.targets | 4 + 13 files changed, 713 insertions(+), 98 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs index 2c1382c5458..63e6f2fc41f 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs @@ -29,13 +29,13 @@ protected override void ProcessAssembly (AssemblyDefinition assembly) } #if !ILLINK - public bool ProcessAssembly (AssemblyDefinition assembly, StepContext context) + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) { // Only run this step on user Android assemblies if (!context.IsAndroidUserAssembly) - return false; + return; - return AddKeepAlives (assembly); + context.IsAssemblyModified |= AddKeepAlives (assembly); } #endif // !ILLINK diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs index 5adc2e27582..c5a44ec3b07 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs @@ -29,7 +29,7 @@ public class FindJavaObjectsStep : BaseStep, IAssemblyModifierPipelineStep public FindJavaObjectsStep (TaskLoggingHelper log) => Log = log; - public bool ProcessAssembly (AssemblyDefinition assembly, StepContext context) + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) { var destinationJLOXml = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (context.Destination.ItemSpec); var scanned = ScanAssembly (assembly, context, destinationJLOXml); @@ -37,11 +37,7 @@ public bool ProcessAssembly (AssemblyDefinition assembly, StepContext context) if (!scanned) { // We didn't scan for Java objects, so write an empty .xml file for later steps JavaObjectsXmlFile.WriteEmptyFile (destinationJLOXml, Log); - return false; } - - // This step does not change the assembly - return false; } public bool ScanAssembly (AssemblyDefinition assembly, StepContext context, string destinationJLOXml) diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs new file mode 100644 index 00000000000..550ccbcf4a6 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs @@ -0,0 +1,87 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Mono.Linker.Steps; +using Xamarin.Android.Tasks; + +namespace MonoDroid.Tuner; + +/// +/// Scans an assembly for JLOs that need to be in the typemap and writes them to an XML file. +/// +public class FindTypeMapObjectsStep : BaseStep, IAssemblyModifierPipelineStep +{ + public bool Debug { get; set; } + + public bool ErrorOnCustomJavaObject { get; set; } + + public TaskLoggingHelper Log { get; set; } + + public FindTypeMapObjectsStep (TaskLoggingHelper log) => Log = log; + + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) + { + var destinationTypeMapXml = TypeMapObjectsXmlFile.GetTypeMapObjectsXmlFilePath (context.Destination.ItemSpec); + + // We only care about assemblies that can contains JLOs + if (!context.IsAndroidAssembly) { + Log.LogDebugMessage ($"Skipping assembly '{assembly.Name.Name}' because it is not an Android assembly"); + TypeMapObjectsXmlFile.WriteEmptyFile (destinationTypeMapXml, Log); + return; + } + + var types = ScanForJavaTypes (assembly); + + var xml = new TypeMapObjectsXmlFile { + AssemblyName = assembly.Name.Name, + }; + + if (Debug) { + var (javaToManaged, managedToJava) = TypeMapCecilAdapter.GetDebugNativeEntries (types, Context, out var foundJniNativeRegistration); + + xml.JavaToManagedDebugEntries.AddRange (javaToManaged); + xml.ManagedToJavaDebugEntries.AddRange (managedToJava); + xml.FoundJniNativeRegistration = foundJniNativeRegistration; + + if (!xml.HasDebugEntries) { + Log.LogDebugMessage ($"No Java types found in '{assembly.Name.Name}'"); + TypeMapObjectsXmlFile.WriteEmptyFile (destinationTypeMapXml, Log); + return; + } + } else { + var genState = TypeMapCecilAdapter.GetReleaseGenerationState (types, Context, out var foundJniNativeRegistration); + xml.ModuleReleaseData = genState.TempModules.SingleOrDefault ().Value; + + if (xml.ModuleReleaseData == null) { + Log.LogDebugMessage ($"No Java types found in '{assembly.Name.Name}'"); + TypeMapObjectsXmlFile.WriteEmptyFile (destinationTypeMapXml, Log); + return; + } + } + + xml.Export (destinationTypeMapXml); + + Log.LogDebugMessage ($"Wrote '{destinationTypeMapXml}', {xml.JavaToManagedDebugEntries.Count} JavaToManagedDebugEntries, {xml.ManagedToJavaDebugEntries.Count} ManagedToJavaDebugEntries, FoundJniNativeRegistration: {xml.FoundJniNativeRegistration}"); + } + + List ScanForJavaTypes (AssemblyDefinition assembly) + { + var types = new List (); + + var scanner = new XAJavaTypeScanner (Xamarin.Android.Tools.AndroidTargetArch.None, Log, Context) { + ErrorOnCustomJavaObject = ErrorOnCustomJavaObject + }; + + foreach (ModuleDefinition md in assembly.Modules) { + foreach (TypeDefinition td in md.Types) { + scanner.AddJavaType (td, types); + } + } + + return types; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixAbstractMethodsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixAbstractMethodsStep.cs index 0f979714bf0..82f1777de4b 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixAbstractMethodsStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixAbstractMethodsStep.cs @@ -83,13 +83,13 @@ internal bool FixAbstractMethods (AssemblyDefinition assembly) } #if !ILLINK - public bool ProcessAssembly (AssemblyDefinition assembly, StepContext context) + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) { // Only run this step on non-main user Android assemblies if (context.IsMainAssembly || !context.IsAndroidUserAssembly) - return false; + return; - return FixAbstractMethods (assembly); + context.IsAssemblyModified |= FixAbstractMethods (assembly); } #endif // !ILLINK diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs index 1cfc19f2d9a..b4225d159da 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs @@ -71,13 +71,13 @@ protected override void LoadDesigner () } #if !ILLINK - public bool ProcessAssembly (AssemblyDefinition assembly, Xamarin.Android.Tasks.StepContext context) + public void ProcessAssembly (AssemblyDefinition assembly, Xamarin.Android.Tasks.StepContext context) { // Only run this step on non-main user Android assemblies if (context.IsMainAssembly || !context.IsAndroidUserAssembly) - return false; + return; - return ProcessAssemblyDesigner (assembly); + context.IsAssemblyModified |= ProcessAssemblyDesigner (assembly); } #endif // !ILLINK diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs index 18935bf8d77..7ffe910b5e6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs @@ -8,6 +8,7 @@ using Java.Interop.Tools.TypeNameMappings; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; using Mono.Cecil; using MonoDroid.Tuner; using Xamarin.Android.Tools; @@ -78,10 +79,6 @@ public override bool RunTask () ReadSymbols = ReadSymbols, }; - var writerParameters = new WriterParameters { - DeterministicMvid = Deterministic, - }; - Dictionary> perArchAssemblies = MonoAndroidHelper.GetPerArchAssemblies (ResolvedAssemblies, Array.Empty (), validate: false); AssemblyPipeline? pipeline = null; @@ -122,7 +119,7 @@ public override bool RunTask () Directory.CreateDirectory (Path.GetDirectoryName (destination.ItemSpec)); - RunPipeline (pipeline!, source, destination, writerParameters); + RunPipeline (pipeline!, source, destination); } pipeline?.Dispose (); @@ -141,9 +138,26 @@ protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkCont findJavaObjectsStep.Initialize (context); pipeline.Steps.Add (findJavaObjectsStep); + + // SaveChangedAssemblyStep + var writerParameters = new WriterParameters { + DeterministicMvid = Deterministic, + }; + + var saveChangedAssemblyStep = new SaveChangedAssemblyStep (Log, writerParameters); + pipeline.Steps.Add (saveChangedAssemblyStep); + + // FindTypeMapObjectsStep - this must be run after the assembly has been saved, as saving changes the MVID + var findTypeMapObjectsStep = new FindTypeMapObjectsStep (Log) { + ErrorOnCustomJavaObject = ErrorOnCustomJavaObject, + Debug = Debug, + }; + + findTypeMapObjectsStep.Initialize (context); + pipeline.Steps.Add (findTypeMapObjectsStep); } - void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination, WriterParameters writerParameters) + void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination) { var assembly = pipeline.Resolver.GetAssembly (source.ItemSpec); @@ -157,28 +171,47 @@ void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destina IsUserAssembly = ResolvedUserAssemblies.Any (a => a.ItemSpec == source.ItemSpec), }; - var changed = pipeline.Run (assembly, context); - - if (changed) { - Log.LogDebugMessage ($"Saving modified assembly: {destination.ItemSpec}"); - Directory.CreateDirectory (Path.GetDirectoryName (destination.ItemSpec)); - writerParameters.WriteSymbols = assembly.MainModule.HasSymbols; - assembly.Write (destination.ItemSpec, writerParameters); - } else { - // If we didn't write a modified file, copy the original to the destination - CopyIfChanged (source, destination); - } + pipeline.Run (assembly, context); } - void CopyIfChanged (ITaskItem source, ITaskItem destination) + class SaveChangedAssemblyStep : IAssemblyModifierPipelineStep { - if (MonoAndroidHelper.CopyAssemblyAndSymbols (source.ItemSpec, destination.ItemSpec)) { - Log.LogDebugMessage ($"Copied: {destination.ItemSpec}"); - } else { - Log.LogDebugMessage ($"Skipped unchanged file: {destination.ItemSpec}"); + public TaskLoggingHelper Log { get; set; } + + public WriterParameters WriterParameters { get; set; } + + public SaveChangedAssemblyStep (TaskLoggingHelper log, WriterParameters writerParameters) + { + Log = log; + WriterParameters = writerParameters; + } - // NOTE: We still need to update the timestamp on this file, or this target would run again - File.SetLastWriteTimeUtc (destination.ItemSpec, DateTime.UtcNow); + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) + { + if (context.IsAssemblyModified) { + Log.LogDebugMessage ($"Saving modified assembly: {context.Destination.ItemSpec}"); + Directory.CreateDirectory (Path.GetDirectoryName (context.Destination.ItemSpec)); + WriterParameters.WriteSymbols = assembly.MainModule.HasSymbols; + assembly.Write (context.Destination.ItemSpec, WriterParameters); + } else { + // If we didn't write a modified file, copy the original to the destination + CopyIfChanged (context.Source, context.Destination); + } + + // We just saved the assembly, so it is no longer modified + context.IsAssemblyModified = false; + } + + void CopyIfChanged (ITaskItem source, ITaskItem destination) + { + if (MonoAndroidHelper.CopyAssemblyAndSymbols (source.ItemSpec, destination.ItemSpec)) { + Log.LogDebugMessage ($"Copied: {destination.ItemSpec}"); + } else { + Log.LogDebugMessage ($"Skipped unchanged file: {destination.ItemSpec}"); + + // NOTE: We still need to update the timestamp on this file, or this target would run again + File.SetLastWriteTimeUtc (destination.ItemSpec, DateTime.UtcNow); + } } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs index 231eeb49697..3fb0d05a851 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using Java.Interop.Tools.Cecil; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; @@ -11,6 +12,8 @@ namespace Xamarin.Android.Tasks; +// Note: If/When this is converted to an incremental task, every build still needs to set: +// NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent public class GenerateTypeMappings : AndroidTask { public override string TaskPrefix => "GTM"; @@ -20,25 +23,91 @@ public class GenerateTypeMappings : AndroidTask public bool Debug { get; set; } + public bool EnableMarshalMethods { get;set; } + + [Output] + public ITaskItem [] GeneratedBinaryTypeMaps { get; set; } = []; + [Required] public string IntermediateOutputDirectory { get; set; } = ""; public bool SkipJniAddNativeMethodRegistrationAttributeScan { get; set; } [Required] - public string TypemapOutputDirectory { get; set; } = ""; + public ITaskItem [] ResolvedAssemblies { get; set; } = []; - [Output] - public ITaskItem [] GeneratedBinaryTypeMaps { get; set; } = []; + // This property is temporary and is used to ensure that the new "linker step" + // JLO scanning produces the same results as the old process. It will be removed + // once the process is complete. + public bool RunCheckedBuild { get; set; } + + [Required] + public string [] SupportedAbis { get; set; } = []; public string TypemapImplementation { get; set; } = "llvm-ir"; + [Required] + public string TypemapOutputDirectory { get; set; } = ""; + AndroidRuntime androidRuntime; public override bool RunTask () { + var useMarshalMethods = !Debug && EnableMarshalMethods; + androidRuntime = MonoAndroidHelper.ParseAndroidRuntime (AndroidRuntime); + if (androidRuntime == Xamarin.Android.Tasks.AndroidRuntime.NativeAOT) { + // NativeAOT typemaps are generated in `Microsoft.Android.Sdk.ILLink.TypeMappingStep` + Log.LogDebugMessage ("Skipping type maps for NativeAOT."); + return !Log.HasLoggedErrors; + } + + // If using marshal methods, we cannot use the .typemap.xml files currently because + // the type token ids were changed by the marshal method rewriter after we wrote the .xml files. + if (!useMarshalMethods) + GenerateAllTypeMappings (); + + // Generate typemaps from the native code generator state (produced by the marshal method rewriter) + if (RunCheckedBuild || useMarshalMethods) + GenerateAllTypeMappingsFromNativeState (useMarshalMethods); + + return !Log.HasLoggedErrors; + } + + void GenerateAllTypeMappings () + { + var allAssembliesPerArch = MonoAndroidHelper.GetPerArchAssemblies (ResolvedAssemblies, SupportedAbis, validate: true); + + foreach (var set in allAssembliesPerArch) + GenerateTypeMap (set.Key, set.Value.Values.ToList ()); + } + + void GenerateTypeMap (AndroidTargetArch arch, List assemblies) + { + Log.LogDebugMessage ($"Generating type maps for architecture '{arch}'"); + + var state = TypeMapObjectsFileAdapter.Create (arch, assemblies, Log); + + // An error was already logged to Log.LogError + if (state is null) + return; + + if (TypemapImplementation != "llvm-ir") { + Log.LogDebugMessage ($"TypemapImplementation='{TypemapImplementation}' will write an empty native typemap."); + state.XmlFiles.Clear (); + } + + var tmg = new TypeMapGenerator (Log, state, androidRuntime); + tmg.Generate (Debug, SkipJniAddNativeMethodRegistrationAttributeScan, TypemapOutputDirectory); + // Set for use by task later + NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent = state.JniAddNativeMethodRegistrationAttributePresent; + + AddOutputTypeMaps (tmg, state.TargetArch); + } + + void GenerateAllTypeMappingsFromNativeState (bool useMarshalMethods) + { // Retrieve the stored NativeCodeGenState var nativeCodeGenStates = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal> ( MonoAndroidHelper.GetProjectBuildSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateRegisterTaskKey, WorkingDirectory, IntermediateOutputDirectory), @@ -50,37 +119,42 @@ public override bool RunTask () foreach (var kvp in nativeCodeGenStates) { NativeCodeGenState state = kvp.Value; templateCodeGenState = state; - WriteTypeMappings (state); + GenerateTypeMapFromNativeState (state, useMarshalMethods); } if (templateCodeGenState is null) throw new InvalidOperationException ($"Internal error: no native code generator state defined"); // Set for use by task later - NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent = templateCodeGenState.JniAddNativeMethodRegistrationAttributePresent; - - return !Log.HasLoggedErrors; + if (useMarshalMethods) + NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent = templateCodeGenState.JniAddNativeMethodRegistrationAttributePresent; } - void WriteTypeMappings (NativeCodeGenState state) + void GenerateTypeMapFromNativeState (NativeCodeGenState state, bool useMarshalMethods) { if (androidRuntime == Xamarin.Android.Tasks.AndroidRuntime.NativeAOT) { // NativeAOT typemaps are generated in `Microsoft.Android.Sdk.ILLink.TypeMappingStep` Log.LogDebugMessage ("Skipping type maps for NativeAOT."); return; } - Log.LogDebugMessage ($"Generating type maps for architecture '{state.TargetArch}'"); + Log.LogDebugMessage ($"Generating type maps from native state for architecture '{state.TargetArch}' (RunCheckedBuild = {RunCheckedBuild})"); if (TypemapImplementation != "llvm-ir") { Log.LogDebugMessage ($"TypemapImplementation='{TypemapImplementation}' will write an empty native typemap."); state = new NativeCodeGenState (state.TargetArch, new TypeDefinitionCache (), state.Resolver, [], [], state.Classifier); } - var tmg = new TypeMapGenerator (Log, state, androidRuntime); + var tmg = new TypeMapGenerator (Log, new NativeCodeGenStateAdapter (state), androidRuntime) { RunCheckedBuild = RunCheckedBuild && !useMarshalMethods }; tmg.Generate (Debug, SkipJniAddNativeMethodRegistrationAttributeScan, TypemapOutputDirectory); - string abi = MonoAndroidHelper.ArchToAbi (state.TargetArch); + AddOutputTypeMaps (tmg, state.TargetArch); + } + + void AddOutputTypeMaps (TypeMapGenerator tmg, AndroidTargetArch arch) + { + string abi = MonoAndroidHelper.ArchToAbi (arch); var items = new List (); + foreach (string file in tmg.GeneratedBinaryTypeMaps) { var item = new TaskItem (file); string fileName = Path.GetFileName (file); @@ -90,6 +164,6 @@ void WriteTypeMappings (NativeCodeGenState state) items.Add (item); } - GeneratedBinaryTypeMaps = items.ToArray (); + GeneratedBinaryTypeMaps = GeneratedBinaryTypeMaps.Concat (items).ToArray (); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyPipeline.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyPipeline.cs index 5cf54ca78af..eba39ee60ed 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyPipeline.cs @@ -20,14 +20,10 @@ public AssemblyPipeline (DirectoryAssemblyResolver resolver) Resolver = resolver; } - public bool Run (AssemblyDefinition assembly, StepContext context) + public void Run (AssemblyDefinition assembly, StepContext context) { - var changed = false; - foreach (var step in Steps) - changed |= step.ProcessAssembly (assembly, context); - - return changed; + step.ProcessAssembly (assembly, context); } protected virtual void Dispose (bool disposing) @@ -51,7 +47,7 @@ public void Dispose () public interface IAssemblyModifierPipelineStep { - bool ProcessAssembly (AssemblyDefinition assembly, StepContext context); + void ProcessAssembly (AssemblyDefinition assembly, StepContext context); } public class StepContext @@ -60,6 +56,7 @@ public class StepContext public ITaskItem Destination { get; } public bool EnableMarshalMethods { get; set; } public bool IsAndroidAssembly { get; set; } + public bool IsAssemblyModified { get; set; } public bool IsDebug { get; set; } public bool IsFrameworkAssembly { get; set; } public bool IsMainAssembly { get; set; } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs index 197948048ac..62bc71287c3 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs @@ -16,15 +16,25 @@ class TypeMapCecilAdapter { public static (List javaToManaged, List managedToJava) GetDebugNativeEntries (NativeCodeGenState state) { + var (javaToManaged, managedToJava) = GetDebugNativeEntries (state.AllJavaTypes, state.TypeCache, out var foundJniNativeRegistration); + + state.JniAddNativeMethodRegistrationAttributePresent = foundJniNativeRegistration; + + return (javaToManaged, managedToJava); + } + + public static (List javaToManaged, List managedToJava) GetDebugNativeEntries (List types, TypeDefinitionCache cache, out bool foundJniNativeRegistration) + { + var javaDuplicates = new Dictionary> (StringComparer.Ordinal); var javaToManaged = new List (); var managedToJava = new List (); + foundJniNativeRegistration = false; - var javaDuplicates = new Dictionary> (StringComparer.Ordinal); - foreach (TypeDefinition td in state.AllJavaTypes) { - UpdateApplicationConfig (state, td); + foreach (var td in types) { + foundJniNativeRegistration = JniAddNativeMethodRegistrationAttributeFound (foundJniNativeRegistration, td); - TypeMapDebugEntry entry = GetDebugEntry (td, state.TypeCache); - HandleDebugDuplicates (javaDuplicates, entry, td, state.TypeCache); + TypeMapDebugEntry entry = GetDebugEntry (td, cache); + HandleDebugDuplicates (javaDuplicates, entry, td, cache); javaToManaged.Add (entry); managedToJava.Add (entry); @@ -40,17 +50,28 @@ public static ReleaseGenerationState GetReleaseGenerationState (NativeCodeGenSta var genState = new ReleaseGenerationState (); foreach (TypeDefinition td in state.AllJavaTypes) { - ProcessReleaseType (state, genState, td); + UpdateApplicationConfig (state, td); + ProcessReleaseType (state.TypeCache, genState, td); } return genState; } - static void ProcessReleaseType (NativeCodeGenState state, ReleaseGenerationState genState, TypeDefinition td) + public static ReleaseGenerationState GetReleaseGenerationState (List types, TypeDefinitionCache cache, out bool foundJniNativeRegistration) { - UpdateApplicationConfig (state, td); - genState.AddKnownAssembly (GetAssemblyName (td)); + var genState = new ReleaseGenerationState (); + foundJniNativeRegistration = false; + foreach (TypeDefinition td in types) { + foundJniNativeRegistration = JniAddNativeMethodRegistrationAttributeFound (foundJniNativeRegistration, td); + ProcessReleaseType (cache, genState, td); + } + + return genState; + } + + static void ProcessReleaseType (TypeDefinitionCache cache, ReleaseGenerationState genState, TypeDefinition td) + { // We must NOT use Guid here! The reason is that Guid sort order is different than its corresponding // byte array representation and on the runtime we need the latter in order to be able to binary search // through the module array. @@ -65,7 +86,6 @@ static void ProcessReleaseType (NativeCodeGenState state, ReleaseGenerationState moduleData = new ModuleReleaseData { Mvid = td.Module.Mvid, MvidBytes = moduleUUID, - //Assembly = td.Module.Assembly, AssemblyName = td.Module.Assembly.Name.Name, TypesScratch = new Dictionary (StringComparer.Ordinal), DuplicateTypes = new List (), @@ -74,7 +94,7 @@ static void ProcessReleaseType (NativeCodeGenState state, ReleaseGenerationState tempModules.Add (moduleUUID, moduleData); } - string javaName = Java.Interop.Tools.TypeNameMappings.JavaNativeTypeManager.ToJniName (td, state.TypeCache); + string javaName = Java.Interop.Tools.TypeNameMappings.JavaNativeTypeManager.ToJniName (td, cache); // We will ignore generic types and interfaces when generating the Java to Managed map, but we must not // omit them from the table we output - we need the same number of entries in both java-to-managed and // managed-to-java tables. `SkipInJavaToManaged` set to `true` will cause the native assembly generator @@ -98,8 +118,6 @@ static void ProcessReleaseType (NativeCodeGenState state, ReleaseGenerationState } } - static string GetAssemblyName (TypeDefinition td) => td.Module.Assembly.FullName; - static TypeMapDebugEntry GetDebugEntry (TypeDefinition td, TypeDefinitionCache cache) { return new TypeMapDebugEntry { @@ -130,7 +148,6 @@ static string GetManagedTypeName (TypeDefinition td) return $"{managedTypeName}, {td.Module.Assembly.Name.Name}"; } - static void HandleDebugDuplicates (Dictionary> javaDuplicates, TypeMapDebugEntry entry, TypeDefinition td, TypeDefinitionCache cache) { List duplicates; @@ -147,6 +164,7 @@ static void HandleDebugDuplicates (Dictionary> j // Fix things up so the abstract type is first, and the `Invoker` is considered a duplicate. duplicates.Insert (0, entry); oldEntry.SkipInJavaToManaged = false; + oldEntry.IsInvoker = true; } else { // ¯\_(ツ)_/¯ duplicates.Add (entry); @@ -179,15 +197,20 @@ static void SyncDebugDuplicates (Dictionary> jav static void UpdateApplicationConfig (NativeCodeGenState state, TypeDefinition javaType) { - if (state.JniAddNativeMethodRegistrationAttributePresent || !javaType.HasCustomAttributes) { - return; - } + state.JniAddNativeMethodRegistrationAttributePresent = JniAddNativeMethodRegistrationAttributeFound (state.JniAddNativeMethodRegistrationAttributePresent, javaType); + } + static bool JniAddNativeMethodRegistrationAttributeFound (bool alreadyFound, TypeDefinition javaType) + { + if (alreadyFound || !javaType.HasCustomAttributes) { + return alreadyFound; + } + foreach (CustomAttribute ca in javaType.CustomAttributes) { - if (!state.JniAddNativeMethodRegistrationAttributePresent && String.Compare ("JniAddNativeMethodRegistrationAttribute", ca.AttributeType.Name, StringComparison.Ordinal) == 0) { - state.JniAddNativeMethodRegistrationAttributePresent = true; - break; + if (string.Equals ("JniAddNativeMethodRegistrationAttribute", ca.AttributeType.Name, StringComparison.Ordinal)) { + return true; } } + return false; } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs index cada872317d..00a0ede03ad 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs @@ -3,13 +3,18 @@ using System.IO; using System.Linq; using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Mono.Cecil; +using Xamarin.Android.Tools; +using static Xamarin.Android.Tasks.TypeMapGenerator; namespace Xamarin.Android.Tasks { class TypeMapGenerator { + public bool RunCheckedBuild { get; set; } + internal sealed class ModuleUUIDArrayComparer : IComparer { int Compare (byte[] left, byte[] right) @@ -62,6 +67,14 @@ internal sealed class TypeMapDebugEntry // It is not used to create the typemap. public TypeDefinition TypeDefinition; + // These fields are only used by the XML adapter for temp storage while reading. + // It is not used to create the typemap. + public string? DuplicateForJavaToManagedKey { get; set; } + public bool IsInvoker { get; set; } + public bool IsMonoAndroid { get; set; } // Types in Mono.Android take precedence over other assemblies + + public string Key => $"{JavaName}|{ManagedName}"; + public override string ToString () { return $"TypeMapDebugEntry{{JavaName={JavaName}, ManagedName={ManagedName}, SkipInJavaToManaged={SkipInJavaToManaged}, DuplicateForJavaToManaged={DuplicateForJavaToManaged}}}"; @@ -78,36 +91,26 @@ internal sealed class ModuleDebugData internal sealed class ReleaseGenerationState { - int assemblyId = 0; + // This field is only used by the Cecil adapter for temp storage while reading. + // It is not used to create the typemap. + public readonly Dictionary MvidCache; - public readonly Dictionary KnownAssemblies; - public readonly Dictionary MvidCache; - public readonly Dictionary TempModules; + public readonly Dictionary TempModules; public ReleaseGenerationState () { - KnownAssemblies = new Dictionary (StringComparer.Ordinal); - MvidCache = new Dictionary (); - TempModules = new Dictionary (); - } - - public void AddKnownAssembly (string assemblyName) - { - if (KnownAssemblies.ContainsKey (assemblyName)) { - return; - } - - KnownAssemblies.Add (assemblyName, ++assemblyId); + MvidCache = new Dictionary (); + TempModules = new Dictionary (); } } readonly TaskLoggingHelper log; - readonly NativeCodeGenState state; + readonly ITypeMapGeneratorAdapter state; readonly AndroidRuntime runtime; public IList GeneratedBinaryTypeMaps { get; } = new List (); - public TypeMapGenerator (TaskLoggingHelper log, NativeCodeGenState state, AndroidRuntime runtime) + public TypeMapGenerator (TaskLoggingHelper log, ITypeMapGeneratorAdapter state, AndroidRuntime runtime) { this.log = log ?? throw new ArgumentNullException (nameof (log)); this.state = state ?? throw new ArgumentNullException (nameof (state)); @@ -133,7 +136,7 @@ public void Generate (bool debugBuild, bool skipJniAddNativeMethodRegistrationAt void GenerateDebugNativeAssembly (string outputDirectory) { - (var javaToManaged, var managedToJava) = TypeMapCecilAdapter.GetDebugNativeEntries (state); + (var javaToManaged, var managedToJava) = state.GetDebugNativeEntries (); var data = new ModuleDebugData { EntryCount = (uint)javaToManaged.Count, @@ -150,7 +153,7 @@ void GenerateDebugNativeAssembly (string outputDirectory) void GenerateRelease (string outputDirectory) { - var genState = TypeMapCecilAdapter.GetReleaseGenerationState (state); + var genState = state.GetReleaseGenerationState (); ModuleReleaseData [] modules = genState.TempModules.Values.ToArray (); Array.Sort (modules, new ModuleUUIDArrayComparer ()); @@ -188,9 +191,135 @@ void GenerateNativeAssembly (LLVMIR.LlvmIrComposer composer, LLVMIR.LlvmIrModule throw; } finally { sw.Flush (); - Files.CopyIfStreamChanged (sw.BaseStream, outputFile); + + if (RunCheckedBuild) { + if (Files.HasStreamChanged (sw.BaseStream, outputFile)) { + Files.CopyIfStreamChanged (sw.BaseStream, outputFile + "2"); + log.LogError ("Output file changed"); + } else { + log.LogMessage ($"RunCheckedBuild: Output file '{outputFile}' unchanged"); + } + } else { + Files.CopyIfStreamChanged (sw.BaseStream, outputFile); + } + } + } + } + } + + // This abstraction is temporary to facilitate the transition from the old + // typemap generator to the new one. It will be removed once the transition + // is complete. + interface ITypeMapGeneratorAdapter + { + AndroidTargetArch TargetArch { get; } + bool JniAddNativeMethodRegistrationAttributePresent { get; set; } + (List javaToManaged, List managedToJava) GetDebugNativeEntries (); + ReleaseGenerationState GetReleaseGenerationState (); + } + + class NativeCodeGenStateAdapter : ITypeMapGeneratorAdapter + { + readonly NativeCodeGenState state; + + public NativeCodeGenStateAdapter (NativeCodeGenState state) + { + this.state = state ?? throw new ArgumentNullException (nameof (state)); + } + + public AndroidTargetArch TargetArch => state.TargetArch; + + public bool JniAddNativeMethodRegistrationAttributePresent { + get => state.JniAddNativeMethodRegistrationAttributePresent; + set => state.JniAddNativeMethodRegistrationAttributePresent = value; + } + + public (List javaToManaged, List managedToJava) GetDebugNativeEntries () + { + return TypeMapCecilAdapter.GetDebugNativeEntries (state); + } + + public ReleaseGenerationState GetReleaseGenerationState () + { + return TypeMapCecilAdapter.GetReleaseGenerationState (state); + } + } + + class TypeMapObjectsFileAdapter : ITypeMapGeneratorAdapter + { + public List XmlFiles { get; } = []; + public AndroidTargetArch TargetArch { get; } + + public TypeMapObjectsFileAdapter (AndroidTargetArch targetArch) + { + TargetArch = targetArch; + } + + public bool JniAddNativeMethodRegistrationAttributePresent { get; set; } + + public (List javaToManaged, List managedToJava) GetDebugNativeEntries () + { + var javaToManaged = new List (); + var managedToJava = new List (); + + foreach (var xml in XmlFiles) { + javaToManaged.AddRange (xml.JavaToManagedDebugEntries); + managedToJava.AddRange (xml.ManagedToJavaDebugEntries); + } + + // Handle entries with duplicate JavaNames + GroupDuplicateDebugEntries (javaToManaged); + GroupDuplicateDebugEntries (managedToJava); + + return (javaToManaged, managedToJava); + } + + void GroupDuplicateDebugEntries (List debugEntries) + { + foreach (var group in debugEntries.GroupBy (ent => ent.JavaName).Where (g => g.Count () > 1)) { + // We need to sort: + // - Types in Mono.Android come first + // - Types that are not invokers come first + var entries = group + .OrderBy (e => e.IsMonoAndroid ? 0 : 1) + .ThenBy (e => e.IsInvoker ? 1 : 0) + .ToList (); + + for (var i = 1; i < entries.Count; i++) + entries [i].DuplicateForJavaToManaged = entries [0]; + } + } + + public ReleaseGenerationState GetReleaseGenerationState () + { + var state = new ReleaseGenerationState (); + + foreach (var xml in XmlFiles) + if (xml.HasReleaseEntries) + state.TempModules.Add (xml.ModuleReleaseData.MvidBytes, xml.ModuleReleaseData); + + return state; + } + + public static TypeMapObjectsFileAdapter? Create (AndroidTargetArch targetArch, List assemblies, TaskLoggingHelper log) + { + var adapter = new TypeMapObjectsFileAdapter (targetArch); + + foreach (var assembly in assemblies) { + var typeMapPath = TypeMapObjectsXmlFile.GetTypeMapObjectsXmlFilePath (assembly.ItemSpec); + + if (!File.Exists (typeMapPath)) { + log.LogError ($"'{typeMapPath}' not found."); + return null; } + + var xml = TypeMapObjectsXmlFile.Import (typeMapPath); + + if (xml.WasScanned) + adapter.XmlFiles.Add (xml); } + + return adapter; } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs new file mode 100644 index 00000000000..118b1b03581 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs @@ -0,0 +1,271 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using NuGet.Packaging; + +using ModuleReleaseData = Xamarin.Android.Tasks.TypeMapGenerator.ModuleReleaseData; +using TypeMapDebugEntry = Xamarin.Android.Tasks.TypeMapGenerator.TypeMapDebugEntry; +using TypeMapReleaseEntry = Xamarin.Android.Tasks.TypeMapGenerator.TypeMapReleaseEntry; + +namespace Xamarin.Android.Tasks; + +class TypeMapObjectsXmlFile +{ + static XmlWriterSettings settings = new XmlWriterSettings { + Indent = true, + NewLineOnAttributes = false, + OmitXmlDeclaration = true, + }; + + static readonly TypeMapObjectsXmlFile unscanned = new TypeMapObjectsXmlFile { WasScanned = false }; + + public string? AssemblyName { get; set; } + public bool FoundJniNativeRegistration { get; set; } + public List JavaToManagedDebugEntries { get; } = []; + public List ManagedToJavaDebugEntries { get; } = []; + public ModuleReleaseData? ModuleReleaseData { get; set; } + public bool HasDebugEntries => JavaToManagedDebugEntries.Count > 0 || ManagedToJavaDebugEntries.Count > 0; + public bool HasReleaseEntries => ModuleReleaseData is not null; + + public bool WasScanned { get; private set; } + + public void Export (string filename) + { + using var sw = MemoryStreamPool.Shared.CreateStreamWriter (); + + using (var xml = XmlWriter.Create (sw, settings)) + Export (xml); + + sw.Flush (); + + Files.CopyIfStreamChanged (sw.BaseStream, filename); + } + + void Export (XmlWriter xml) + { + xml.WriteStartElement ("api"); + xml.WriteAttributeString ("type", HasDebugEntries ? "debug" : "release"); + xml.WriteAttributeStringIfNotDefault ("assembly-name", AssemblyName); + xml.WriteAttributeStringIfNotDefault ("found-jni-native-registration", FoundJniNativeRegistration); + + if (HasDebugEntries) + ExportDebugData (xml); + else if (HasReleaseEntries) + ExportReleaseData (xml); + + xml.WriteEndElement (); + } + + void ExportDebugData (XmlWriter xml) + { + if (JavaToManagedDebugEntries.Count > 0) { + xml.WriteStartElement ("java-to-managed"); + + foreach (var entry in JavaToManagedDebugEntries) + WriteTypeMapDebugEntry (xml, entry); + + xml.WriteEndElement (); + } + + if (ManagedToJavaDebugEntries.Count > 0) { + xml.WriteStartElement ("managed-to-java"); + + foreach (var entry in ManagedToJavaDebugEntries) + WriteTypeMapDebugEntry (xml, entry); + + xml.WriteEndElement (); + } + } + + void WriteTypeMapDebugEntry (XmlWriter xml, TypeMapDebugEntry entry) + { + xml.WriteStartElement ("entry"); + xml.WriteAttributeStringIfNotDefault ("java-name", entry.JavaName); + xml.WriteAttributeStringIfNotDefault ("managed-name", entry.ManagedName); + xml.WriteAttributeStringIfNotDefault ("skip-in-java-to-managed", entry.SkipInJavaToManaged); + xml.WriteAttributeStringIfNotDefault ("is-invoker", entry.IsInvoker); + xml.WriteEndElement (); + } + + void ExportReleaseData (XmlWriter xml) + { + if (ModuleReleaseData is null) + return; + + xml.WriteStartElement ("module"); + + xml.WriteAttributeStringIfNotDefault ("assembly-name", ModuleReleaseData.AssemblyName); + xml.WriteAttributeStringIfNotDefault ("mvid", ModuleReleaseData.Mvid.ToString ("N")); + xml.WriteAttributeStringIfNotDefault ("mvid-bytes", Convert.ToBase64String (ModuleReleaseData.MvidBytes)); + + if (ModuleReleaseData.Types?.Length > 0) { + xml.WriteStartElement ("types"); + + foreach (var entry in ModuleReleaseData.DuplicateTypes) + ExportTypeMapReleaseEntry (xml, entry, null); + + xml.WriteEndElement (); + } + + if (ModuleReleaseData.DuplicateTypes?.Count > 0) { + xml.WriteStartElement ("duplicates"); + + foreach (var entry in ModuleReleaseData.DuplicateTypes) + ExportTypeMapReleaseEntry (xml, entry, null); + + xml.WriteEndElement (); + } + + if (ModuleReleaseData.TypesScratch?.Count > 0) { + xml.WriteStartElement ("types-scratch"); + + foreach (var kvp in ModuleReleaseData.TypesScratch) + ExportTypeMapReleaseEntry (xml, kvp.Value, kvp.Key); + + xml.WriteEndElement (); + } + + xml.WriteEndElement (); + } + + void ExportTypeMapReleaseEntry (XmlWriter xml, TypeMapReleaseEntry entry, string? key) + { + xml.WriteStartElement ("entry"); + + xml.WriteAttributeStringIfNotDefault ("key", key); + xml.WriteAttributeStringIfNotDefault ("java-name", entry.JavaName); + xml.WriteAttributeStringIfNotDefault ("managed-type-name", entry.ManagedTypeName); + xml.WriteAttributeStringIfNotDefault ("token", entry.Token.ToString ()); + xml.WriteAttributeStringIfNotDefault ("skip-in-java-to-managed", entry.SkipInJavaToManaged); + + xml.WriteEndElement (); + } + + /// + /// Given an assembly path, return the path to the ".typemap.xml" file that should be next to it. + /// + public static string GetTypeMapObjectsXmlFilePath (string assemblyPath) + => Path.ChangeExtension (assemblyPath, ".typemap.xml"); + + public static TypeMapObjectsXmlFile Import (string filename) + { + // If the file has zero length, then the assembly wasn't scanned because it couldn't contain JLOs. + // This check is much faster than loading and parsing an empty XML file. + var fi = new FileInfo (filename); + + if (fi.Length == 0) + return unscanned; + + var xml = XDocument.Load (filename); + var root = xml.Root ?? throw new InvalidOperationException ($"Invalid XML file '{filename}'"); + + var type = root.GetRequiredAttribute ("type"); + var assemblyName = root.GetAttributeOrDefault ("assembly-name", (string?)null); + var foundJniNativeRegistration = root.GetAttributeOrDefault ("found-jni-native-registration", false); + + var file = new TypeMapObjectsXmlFile { + WasScanned = true, + AssemblyName = assemblyName, + FoundJniNativeRegistration = foundJniNativeRegistration, + }; + + if (type == "debug") + ImportDebugData (root, file); + else if (type == "release") + ImportReleaseData (root, file); + + return file; + } + + static void ImportDebugData (XElement root, TypeMapObjectsXmlFile file) + { + var isMonoAndroid = root.GetAttributeOrDefault ("assembly-name", string.Empty) == "Mono.Android"; + var javaToManaged = root.Element ("java-to-managed"); + + if (javaToManaged is not null) { + foreach (var entry in javaToManaged.Elements ("entry")) + file.JavaToManagedDebugEntries.Add (FromDebugEntryXml (entry, isMonoAndroid)); + } + + var managedToJava = root.Element ("managed-to-java"); + + if (managedToJava is not null) { + foreach (var entry in managedToJava.Elements ("entry")) + file.ManagedToJavaDebugEntries.Add (FromDebugEntryXml (entry, isMonoAndroid)); + } + } + + static void ImportReleaseData (XElement root, TypeMapObjectsXmlFile file) + { + var module = root.Element ("module"); + + if (module is null) + return; + + file.ModuleReleaseData = new ModuleReleaseData { + AssemblyName = module.GetAttributeOrDefault ("assembly-name", string.Empty), + Mvid = Guid.Parse (module.GetAttributeOrDefault ("mvid", Guid.Empty.ToString ())), + MvidBytes = Convert.FromBase64String (module.GetAttributeOrDefault ("mvid-bytes", string.Empty)), + TypesScratch = new Dictionary (StringComparer.Ordinal), + DuplicateTypes = new List (), + }; + + if (module.Element ("types") is XElement types) + file.ModuleReleaseData.Types = types.Elements ("entry") + .Select (FromReleaseEntryXml) + .ToArray (); + + if (module.Element ("duplicates") is XElement duplicates) + file.ModuleReleaseData.DuplicateTypes.AddRange (duplicates.Elements ("entry") + .Select (FromReleaseEntryXml)); + + if (module.Element ("types-scratch") is XElement typesScratch) + file.ModuleReleaseData.TypesScratch.AddRange (typesScratch.Elements ("entry") + .Select (elem => new KeyValuePair (elem.GetAttributeOrDefault ("key", string.Empty), FromReleaseEntryXml (elem)))); + } + + public static void WriteEmptyFile (string destination, TaskLoggingHelper log) + { + log.LogDebugMessage ($"Writing empty file '{destination}'"); + + // We write a zero byte file to indicate the file couldn't have JLO types and wasn't scanned + File.Create (destination).Dispose (); + } + + static TypeMapDebugEntry FromDebugEntryXml (XElement entry, bool isMonoAndroid) + { + var javaName = entry.GetAttributeOrDefault ("java-name", string.Empty); + var managedName = entry.GetAttributeOrDefault ("managed-name", string.Empty); + var skipInJavaToManaged = entry.GetAttributeOrDefault ("skip-in-java-to-managed", false); + var isInvoker = entry.GetAttributeOrDefault ("is-invoker", false); + + return new TypeMapDebugEntry { + JavaName = javaName, + ManagedName = managedName, + SkipInJavaToManaged = skipInJavaToManaged, + IsInvoker = isInvoker, + IsMonoAndroid = isMonoAndroid, + }; + } + + static TypeMapReleaseEntry FromReleaseEntryXml (XElement entry) + { + var javaName = entry.GetAttributeOrDefault ("java-name", string.Empty); + var managedTypeName = entry.GetAttributeOrDefault ("managed-type-name", string.Empty); + var token = entry.GetAttributeOrDefault ("token", 0u); + var skipInJavaToManaged = entry.GetAttributeOrDefault ("skip-in-java-to-managed", false); + + return new TypeMapReleaseEntry { + JavaName = javaName, + ManagedTypeName = managedTypeName, + Token = token, + SkipInJavaToManaged = skipInJavaToManaged, + }; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index a15756ff32d..f3a0b70801b 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -47,6 +47,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index b4b2bde2a16..9a2ddefd1fa 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1614,8 +1614,12 @@ because xbuild doesn't support framework reference assemblies. From ba85244449c2c70b0db94124b83d790c7c084ecd Mon Sep 17 00:00:00 2001 From: Jonathan Pobst Date: Mon, 21 Apr 2025 13:44:56 -1000 Subject: [PATCH 2/3] Address review feedback. --- .../MonoDroid.Tuner/FindTypeMapObjectsStep.cs | 18 ++---------------- .../Utilities/TypeMapCecilAdapter.cs | 8 ++++---- .../Utilities/TypeMapObjectsXmlFile.cs | 11 +++++++++-- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs index 550ccbcf4a6..bfc242ea63c 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs @@ -41,31 +41,17 @@ public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) }; if (Debug) { - var (javaToManaged, managedToJava) = TypeMapCecilAdapter.GetDebugNativeEntries (types, Context, out var foundJniNativeRegistration); + var (javaToManaged, managedToJava, foundJniNativeRegistration) = TypeMapCecilAdapter.GetDebugNativeEntries (types, Context); xml.JavaToManagedDebugEntries.AddRange (javaToManaged); xml.ManagedToJavaDebugEntries.AddRange (managedToJava); xml.FoundJniNativeRegistration = foundJniNativeRegistration; - - if (!xml.HasDebugEntries) { - Log.LogDebugMessage ($"No Java types found in '{assembly.Name.Name}'"); - TypeMapObjectsXmlFile.WriteEmptyFile (destinationTypeMapXml, Log); - return; - } } else { var genState = TypeMapCecilAdapter.GetReleaseGenerationState (types, Context, out var foundJniNativeRegistration); xml.ModuleReleaseData = genState.TempModules.SingleOrDefault ().Value; - - if (xml.ModuleReleaseData == null) { - Log.LogDebugMessage ($"No Java types found in '{assembly.Name.Name}'"); - TypeMapObjectsXmlFile.WriteEmptyFile (destinationTypeMapXml, Log); - return; - } } - xml.Export (destinationTypeMapXml); - - Log.LogDebugMessage ($"Wrote '{destinationTypeMapXml}', {xml.JavaToManagedDebugEntries.Count} JavaToManagedDebugEntries, {xml.ManagedToJavaDebugEntries.Count} ManagedToJavaDebugEntries, FoundJniNativeRegistration: {xml.FoundJniNativeRegistration}"); + xml.Export (destinationTypeMapXml, Log); } List ScanForJavaTypes (AssemblyDefinition assembly) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs index 62bc71287c3..7ac2f437ddc 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs @@ -16,19 +16,19 @@ class TypeMapCecilAdapter { public static (List javaToManaged, List managedToJava) GetDebugNativeEntries (NativeCodeGenState state) { - var (javaToManaged, managedToJava) = GetDebugNativeEntries (state.AllJavaTypes, state.TypeCache, out var foundJniNativeRegistration); + var (javaToManaged, managedToJava, foundJniNativeRegistration) = GetDebugNativeEntries (state.AllJavaTypes, state.TypeCache); state.JniAddNativeMethodRegistrationAttributePresent = foundJniNativeRegistration; return (javaToManaged, managedToJava); } - public static (List javaToManaged, List managedToJava) GetDebugNativeEntries (List types, TypeDefinitionCache cache, out bool foundJniNativeRegistration) + public static (List javaToManaged, List managedToJava, bool foundJniNativeRegistration) GetDebugNativeEntries (List types, TypeDefinitionCache cache) { var javaDuplicates = new Dictionary> (StringComparer.Ordinal); var javaToManaged = new List (); var managedToJava = new List (); - foundJniNativeRegistration = false; + var foundJniNativeRegistration = false; foreach (var td in types) { foundJniNativeRegistration = JniAddNativeMethodRegistrationAttributeFound (foundJniNativeRegistration, td); @@ -42,7 +42,7 @@ public static (List javaToManaged, List ma SyncDebugDuplicates (javaDuplicates); - return (javaToManaged, managedToJava); + return (javaToManaged, managedToJava, foundJniNativeRegistration); } public static ReleaseGenerationState GetReleaseGenerationState (NativeCodeGenState state) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs index 118b1b03581..13cd476cc25 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs @@ -17,7 +17,7 @@ namespace Xamarin.Android.Tasks; class TypeMapObjectsXmlFile { - static XmlWriterSettings settings = new XmlWriterSettings { + static readonly XmlWriterSettings settings = new XmlWriterSettings { Indent = true, NewLineOnAttributes = false, OmitXmlDeclaration = true, @@ -35,8 +35,13 @@ class TypeMapObjectsXmlFile public bool WasScanned { get; private set; } - public void Export (string filename) + public void Export (string filename, TaskLoggingHelper log) { + if (!HasDebugEntries && ModuleReleaseData == null) { + WriteEmptyFile (filename, log); + return; + } + using var sw = MemoryStreamPool.Shared.CreateStreamWriter (); using (var xml = XmlWriter.Create (sw, settings)) @@ -45,6 +50,8 @@ public void Export (string filename) sw.Flush (); Files.CopyIfStreamChanged (sw.BaseStream, filename); + + log.LogDebugMessage ($"Wrote '{filename}', {JavaToManagedDebugEntries.Count} JavaToManagedDebugEntries, {ManagedToJavaDebugEntries.Count} ManagedToJavaDebugEntries, FoundJniNativeRegistration: {FoundJniNativeRegistration}"); } void Export (XmlWriter xml) From 973ef24d6990d1fee0e729aa72b3176b499db46e Mon Sep 17 00:00:00 2001 From: Jonathan Pobst Date: Tue, 29 Apr 2025 11:24:58 -1000 Subject: [PATCH 3/3] Unnest `SaveChangedAssemblyStep`. --- .../Tasks/AssemblyModifierPipeline.cs | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs index 7ffe910b5e6..143a3931604 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs @@ -173,45 +173,45 @@ void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destina pipeline.Run (assembly, context); } +} - class SaveChangedAssemblyStep : IAssemblyModifierPipelineStep - { - public TaskLoggingHelper Log { get; set; } +class SaveChangedAssemblyStep : IAssemblyModifierPipelineStep +{ + public TaskLoggingHelper Log { get; set; } - public WriterParameters WriterParameters { get; set; } + public WriterParameters WriterParameters { get; set; } - public SaveChangedAssemblyStep (TaskLoggingHelper log, WriterParameters writerParameters) - { - Log = log; - WriterParameters = writerParameters; - } - - public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) - { - if (context.IsAssemblyModified) { - Log.LogDebugMessage ($"Saving modified assembly: {context.Destination.ItemSpec}"); - Directory.CreateDirectory (Path.GetDirectoryName (context.Destination.ItemSpec)); - WriterParameters.WriteSymbols = assembly.MainModule.HasSymbols; - assembly.Write (context.Destination.ItemSpec, WriterParameters); - } else { - // If we didn't write a modified file, copy the original to the destination - CopyIfChanged (context.Source, context.Destination); - } + public SaveChangedAssemblyStep (TaskLoggingHelper log, WriterParameters writerParameters) + { + Log = log; + WriterParameters = writerParameters; + } - // We just saved the assembly, so it is no longer modified - context.IsAssemblyModified = false; + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) + { + if (context.IsAssemblyModified) { + Log.LogDebugMessage ($"Saving modified assembly: {context.Destination.ItemSpec}"); + Directory.CreateDirectory (Path.GetDirectoryName (context.Destination.ItemSpec)); + WriterParameters.WriteSymbols = assembly.MainModule.HasSymbols; + assembly.Write (context.Destination.ItemSpec, WriterParameters); + } else { + // If we didn't write a modified file, copy the original to the destination + CopyIfChanged (context.Source, context.Destination); } - void CopyIfChanged (ITaskItem source, ITaskItem destination) - { - if (MonoAndroidHelper.CopyAssemblyAndSymbols (source.ItemSpec, destination.ItemSpec)) { - Log.LogDebugMessage ($"Copied: {destination.ItemSpec}"); - } else { - Log.LogDebugMessage ($"Skipped unchanged file: {destination.ItemSpec}"); + // We just saved the assembly, so it is no longer modified + context.IsAssemblyModified = false; + } + + void CopyIfChanged (ITaskItem source, ITaskItem destination) + { + if (MonoAndroidHelper.CopyAssemblyAndSymbols (source.ItemSpec, destination.ItemSpec)) { + Log.LogDebugMessage ($"Copied: {destination.ItemSpec}"); + } else { + Log.LogDebugMessage ($"Skipped unchanged file: {destination.ItemSpec}"); - // NOTE: We still need to update the timestamp on this file, or this target would run again - File.SetLastWriteTimeUtc (destination.ItemSpec, DateTime.UtcNow); - } + // NOTE: We still need to update the timestamp on this file, or this target would run again + File.SetLastWriteTimeUtc (destination.ItemSpec, DateTime.UtcNow); } } }