From f78b32ea993af21e400655b38e3d1b68691b05d1 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Jul 2026 14:21:08 -0500 Subject: [PATCH 01/15] [CoreCLR] Stabilize Debug typemaps across C# rebuilds CoreCLR Debug typemaps embedded assembly MVIDs, so every C# rebuild rewrote the generated native typemap even when Java type mappings did not change. This forced native compilation, APK rebuilding, and signing during Fast Deployment. Key Debug managed-to-Java entries by assembly full name instead. Preserve the Release MVID lookup and use a trimmer feature switch so Release builds do not retrieve or marshal Assembly.FullName. Extend the incremental build test to verify a C#-only change does not rewrite typemaps or the signed APK and skips the native typemap, APK, and signing targets. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54c1e7b6-a69c-41bc-812d-5acc5bfdffee --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 8 +- .../Android.Runtime/RuntimeNativeMethods.cs | 2 +- .../RuntimeFeature.cs | 6 ++ .../MonoDroid.Tuner/FindTypeMapObjectsStep.cs | 1 + .../Microsoft.Android.Sdk.CoreCLR.targets | 4 + .../Tasks/GenerateEmptyTypemapStub.cs | 2 - .../IncrementalBuildTest.cs | 26 ++++++ .../Utilities/TypeMapCecilAdapter.cs | 1 + .../Utilities/TypeMapGenerator.cs | 1 + .../Utilities/TypeMapObjectsXmlFile.cs | 16 ++-- ...eMappingDebugNativeAssemblyGeneratorCLR.cs | 82 +++---------------- src/native/clr/host/internal-pinvokes-clr.cc | 6 +- src/native/clr/host/typemap.cc | 35 ++++---- src/native/clr/include/host/typemap.hh | 6 +- .../include/runtime-base/internal-pinvokes.hh | 2 +- src/native/clr/include/xamarin-app.hh | 10 --- .../xamarin-app-stub/application_dso_stub.cc | 2 - .../nativeaot/host/internal-pinvoke-stubs.cc | 1 + 18 files changed, 94 insertions(+), 117 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 234913e3d63..c1ba01a5be8 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -460,7 +460,13 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) } else if (RuntimeFeature.IsCoreClrRuntime) { if (type.FullName is null) return null; - ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, (IntPtr)mvidptr); + string? assemblyFullName = null; + if (RuntimeFeature.ManagedToJavaUsesAssemblyFullName) { + assemblyFullName = type.Assembly.FullName; + if (assemblyFullName is null) + return null; + } + ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, assemblyFullName, (IntPtr)mvidptr); } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); } diff --git a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs index 551b76516fd..a2a59d33ba4 100644 --- a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs +++ b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs @@ -115,7 +115,7 @@ internal unsafe static partial class RuntimeNativeMethods [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] - internal static partial IntPtr clr_typemap_managed_to_java (string fullName, IntPtr mvid); + internal static partial IntPtr clr_typemap_managed_to_java (string fullName, string? assemblyFullName, IntPtr mvid); [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index f15a79ff6f4..718f0b6f656 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -12,6 +12,7 @@ static class RuntimeFeature const bool StartupHookSupportEnabledByDefault = true; const bool TrimmableTypeMapEnabledByDefault = false; const bool ObjectReferenceLoggingEnabledByDefault = false; + const bool ManagedToJavaUsesAssemblyFullNameEnabledByDefault = false; const string FeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature."; const string StartupHookProviderSwitch = "System.StartupHookProvider.IsSupported"; @@ -44,4 +45,9 @@ static class RuntimeFeature [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}")] internal static bool ObjectReferenceLogging { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}", out bool isEnabled) ? isEnabled : ObjectReferenceLoggingEnabledByDefault; + + // Enabled for Debug builds, whose string-based typemaps support Fast Deployment without embedding assembly MVIDs. + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ManagedToJavaUsesAssemblyFullName)}")] + internal static bool ManagedToJavaUsesAssemblyFullName { get; } = + AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ManagedToJavaUsesAssemblyFullName)}", out bool isEnabled) ? isEnabled : ManagedToJavaUsesAssemblyFullNameEnabledByDefault; } 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 08863825ef9..2eb793f3a54 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs @@ -37,6 +37,7 @@ public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) var xml = new TypeMapObjectsXmlFile { AssemblyName = assembly.Name.Name, + AssemblyFullName = Debug ? assembly.Name.FullName : null, AssemblyMvid = assembly.MainModule.Mvid, }; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets index 27a57454aeb..8368d353fc0 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets @@ -34,6 +34,10 @@ This file contains the CoreCLR-specific MSBuild logic for .NET for Android. Value="false" Trim="true" /> + JavaToManagedDebugEntries { get; } = []; @@ -57,6 +58,7 @@ void Export (XmlWriter xml) xml.WriteStartElement ("api"); xml.WriteAttributeString ("type", HasDebugEntries ? "debug" : "release"); xml.WriteAttributeStringIfNotDefault ("assembly-name", AssemblyName); + xml.WriteAttributeStringIfNotDefault ("assembly-full-name", AssemblyFullName); if (AssemblyMvid != Guid.Empty) { xml.WriteAttributeString ("mvid", AssemblyMvid.ToString ("N")); @@ -183,6 +185,7 @@ public static TypeMapObjectsXmlFile Import (string filename) throw new InvalidOperationException ($"Missing required attribute 'type' in '{filename}'"); var assemblyName = reader.GetAttribute ("assembly-name"); + var assemblyFullName = reader.GetAttribute ("assembly-full-name"); var mvidValue = reader.GetAttribute ("mvid"); var mvid = mvidValue.IsNullOrWhiteSpace () ? Guid.Empty : Guid.Parse (mvidValue); var foundJniNativeRegistration = GetAttributeOrDefault (reader, "found-jni-native-registration", false); @@ -190,6 +193,7 @@ public static TypeMapObjectsXmlFile Import (string filename) var file = new TypeMapObjectsXmlFile { WasScanned = true, AssemblyName = assemblyName, + AssemblyFullName = assemblyFullName, AssemblyMvid = mvid, FoundJniNativeRegistration = foundJniNativeRegistration, }; @@ -205,6 +209,7 @@ public static TypeMapObjectsXmlFile Import (string filename) static void ImportDebugData (XmlReader reader, TypeMapObjectsXmlFile file) { var assemblyName = file.AssemblyName ?? string.Empty; + var assemblyFullName = file.AssemblyFullName ?? string.Empty; var isMonoAndroid = assemblyName == "Mono.Android"; while (reader.Read ()) { @@ -212,9 +217,9 @@ static void ImportDebugData (XmlReader reader, TypeMapObjectsXmlFile file) continue; if (reader.Name == "java-to-managed") - ReadDebugEntries (reader, file.JavaToManagedDebugEntries, assemblyName, isMonoAndroid); + ReadDebugEntries (reader, file.JavaToManagedDebugEntries, assemblyName, assemblyFullName, isMonoAndroid); else if (reader.Name == "managed-to-java") - ReadDebugEntries (reader, file.ManagedToJavaDebugEntries, assemblyName, isMonoAndroid); + ReadDebugEntries (reader, file.ManagedToJavaDebugEntries, assemblyName, assemblyFullName, isMonoAndroid); } } @@ -268,7 +273,7 @@ public static void WriteEmptyFile (string destination, TaskLoggingHelper log) File.Create (destination).Dispose (); } - static TypeMapDebugEntry FromDebugEntryXml (XmlReader reader, string assemblyName, bool isMonoAndroid) + static TypeMapDebugEntry FromDebugEntryXml (XmlReader reader, string assemblyName, string assemblyFullName, bool isMonoAndroid) { return new TypeMapDebugEntry { JavaName = reader.GetAttribute ("java-name") ?? string.Empty, @@ -278,6 +283,7 @@ static TypeMapDebugEntry FromDebugEntryXml (XmlReader reader, string assemblyNam IsInvoker = GetAttributeOrDefault (reader, "is-invoker", false), IsMonoAndroid = isMonoAndroid, AssemblyName = assemblyName, + AssemblyFullName = assemblyFullName, }; } @@ -301,7 +307,7 @@ static T GetAttributeOrDefault (XmlReader reader, string name, T defaultValue return (T) Convert.ChangeType (value, typeof (T), CultureInfo.InvariantCulture); } - static void ReadDebugEntries (XmlReader reader, List entries, string assemblyName, bool isMonoAndroid) + static void ReadDebugEntries (XmlReader reader, List entries, string assemblyName, string assemblyFullName, bool isMonoAndroid) { if (reader.IsEmptyElement) return; @@ -313,7 +319,7 @@ static void ReadDebugEntries (XmlReader reader, List entries, return; if (reader.NodeType == XmlNodeType.Element && reader.Name == "entry") - entries.Add (FromDebugEntryXml (reader, assemblyName, isMonoAndroid)); + entries.Add (FromDebugEntryXml (reader, assemblyName, assemblyFullName, isMonoAndroid)); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs index db3640f204c..cb724809853 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs @@ -14,7 +14,6 @@ class TypeMappingDebugNativeAssemblyGeneratorCLR : LlvmIrComposer // These names MUST match src/native/clr/include/xamarin-app.hh const string TypeMapSymbol = "type_map"; - const string UniqueAssembliesSymbol = "type_map_unique_assemblies"; const string AssemblyNamesBlobSymbol = "type_map_assembly_names"; const string ManagedTypeNamesBlobSymbol = "type_map_managed_type_names"; const string JavaTypeNamesBlobSymbol = "type_map_java_type_names"; @@ -66,24 +65,6 @@ public override string GetComment (object data, string fieldName) } } - sealed class TypeMapAssemblyContextDataProvider : NativeAssemblerStructContextDataProvider - { - public override string GetComment (object data, string fieldName) - { - var entry = EnsureType (data); - - if (MonoAndroidHelper.StringEquals ("module_uuid", fieldName)) { - return $" MVID: {entry.MVID}"; - } - - if (MonoAndroidHelper.StringEquals ("name_offset", fieldName)) { - return $" {entry.Name}"; - } - - return String.Empty; - } - } - sealed class TypeMapManagedTypeInfoContextDataProvider : NativeAssemblerStructContextDataProvider { public override string GetComment (object data, string fieldName) @@ -153,7 +134,6 @@ sealed class TypeMap public int ManagedToJavaCount; public uint entry_count; - public ulong unique_assemblies_count; [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] public TypeMapEntry? java_to_managed = null; @@ -162,34 +142,12 @@ sealed class TypeMap public TypeMapEntry? managed_to_java = null; }; - // Order of fields and their type must correspond *exactly* to that in - // src/native/clr/include/xamarin-app.hh TypeMapAssembly structure - [NativeAssemblerStructContextDataProvider (typeof (TypeMapAssemblyContextDataProvider))] - sealed class TypeMapAssembly - { - [NativeAssembler (Ignore = true)] - public string Name = String.Empty; - - [NativeAssembler (Ignore = true)] - public Guid MVID; - - [NativeAssembler (UsesDataProvider = true, InlineArray = true, InlineArraySize = 16)] - public byte[] module_uuid = []; - - public ulong name_length; - - [NativeAssembler (UsesDataProvider = true)] - public ulong name_offset; - } - readonly TypeMapGenerator.ModuleDebugData data; StructureInfo? typeMapEntryStructureInfo; StructureInfo? typeMapStructureInfo; - StructureInfo? typeMapAssemblyStructureInfo; StructureInfo? typeMapManagedTypeInfoStructureInfo; List> javaToManagedMap; List> managedToJavaMap; - List> uniqueAssemblies; List> managedTypeInfos; StructureInstance? type_map; @@ -204,7 +162,6 @@ public TypeMappingDebugNativeAssemblyGeneratorCLR (TaskLoggingHelper log, TypeMa javaToManagedMap = new (); managedToJavaMap = new (); - uniqueAssemblies = new (); managedTypeInfos = new (); } @@ -225,14 +182,19 @@ protected override void Construct (LlvmIrModule module) // in a callback during code generation foreach (TypeMapGenerator.TypeMapDebugEntry entry in data.ManagedToJavaMap) { - (int managedTypeNameOffset, int _) = managedTypeNames.Add (entry.ManagedName); + if (!entry.ManagedName.EndsWith (entry.AssemblyName, StringComparison.Ordinal)) { + throw new InvalidOperationException ($"Internal error: managed type name '{entry.ManagedName}' does not end with assembly name '{entry.AssemblyName}'."); + } + + string managedName = entry.ManagedName.Substring (0, entry.ManagedName.Length - entry.AssemblyName.Length) + entry.AssemblyFullName; + (int managedTypeNameOffset, int _) = managedTypeNames.Add (managedName); (int javaTypeNameOffset, int _) = javaTypeNames.Add (entry.JavaName); var m2j = new TypeMapEntry { - From = entry.ManagedName, + From = managedName, To = entry.JavaName, from = (uint)managedTypeNameOffset, - from_hash = TypeMapHelper.HashNameForCLR (entry.ManagedName), + from_hash = TypeMapHelper.HashNameForCLR (managedName), to = (uint)javaTypeNameOffset, }; managedToJavaMap.Add (new StructureInstance (typeMapEntryStructureInfo, m2j)); @@ -252,32 +214,11 @@ protected override void Construct (LlvmIrModule module) }); var assemblyNamesBlob = new LlvmIrStringBlob (); + data.UniqueAssemblies.Sort ((a, b) => String.Compare (a.Name, b.Name, StringComparison.Ordinal)); foreach (TypeMapGenerator.TypeMapDebugAssembly asm in data.UniqueAssemblies) { - (int assemblyNameOffset, int assemblyNameLength) = assemblyNamesBlob.Add (asm.Name); - - var entry = new TypeMapAssembly { - Name = asm.Name, - MVID = asm.MVID, - - module_uuid = asm.MVIDBytes, - name_length = (ulong)assemblyNameLength, // without the trailing NUL - name_offset = (ulong)assemblyNameOffset, - }; - uniqueAssemblies.Add (new StructureInstance (typeMapAssemblyStructureInfo, entry)); + assemblyNamesBlob.Add (asm.Name); } - uniqueAssemblies.Sort ((StructureInstance a, StructureInstance b) => { - if (a.Instance == null) { - return b.Instance == null ? 0 : -1; - } - - if (b.Instance == null) { - return 1; - } - - return a.Instance.module_uuid.AsSpan ().SequenceCompareTo (b.Instance.module_uuid); - }); - var managedTypeInfos = new List> (); // Java-to-managed maps don't use hashes since many mappings have multiple instances foreach (TypeMapGenerator.TypeMapDebugEntry entry in data.JavaToManagedMap) { @@ -315,7 +256,6 @@ protected override void Construct (LlvmIrModule module) ManagedToJavaCount = data.ManagedToJavaMap == null ? 0 : data.ManagedToJavaMap.Count, entry_count = data.EntryCount, - unique_assemblies_count = (ulong)data.UniqueAssemblies.Count, }; type_map = new StructureInstance (typeMapStructureInfo, map); @@ -323,7 +263,6 @@ protected override void Construct (LlvmIrModule module) module.AddGlobalVariable (ManagedToJavaSymbol, managedToJavaMap, LlvmIrVariableOptions.LocalConstant); module.AddGlobalVariable (JavaToManagedSymbol, javaToManagedMap, LlvmIrVariableOptions.LocalConstant); module.AddGlobalVariable (TypeMapManagedTypeInfoSymbol, managedTypeInfos, LlvmIrVariableOptions.GlobalConstant); - module.AddGlobalVariable (UniqueAssembliesSymbol, uniqueAssemblies, LlvmIrVariableOptions.GlobalConstant); module.AddGlobalVariable (AssemblyNamesBlobSymbol, assemblyNamesBlob, LlvmIrVariableOptions.GlobalConstant); module.AddGlobalVariable (ManagedTypeNamesBlobSymbol, managedTypeNames, LlvmIrVariableOptions.GlobalConstant); module.AddGlobalVariable (JavaTypeNamesBlobSymbol, javaTypeNames, LlvmIrVariableOptions.GlobalConstant); @@ -331,7 +270,6 @@ protected override void Construct (LlvmIrModule module) void MapStructures (LlvmIrModule module) { - typeMapAssemblyStructureInfo = module.MapStructure (); typeMapEntryStructureInfo = module.MapStructure (); typeMapStructureInfo = module.MapStructure (); typeMapManagedTypeInfoStructureInfo = module.MapStructure (); diff --git a/src/native/clr/host/internal-pinvokes-clr.cc b/src/native/clr/host/internal-pinvokes-clr.cc index 59d7e218c30..e5f0865c247 100644 --- a/src/native/clr/host/internal-pinvokes-clr.cc +++ b/src/native/clr/host/internal-pinvokes-clr.cc @@ -9,9 +9,13 @@ using namespace xamarin::android; -const char* clr_typemap_managed_to_java (const char *typeName, const uint8_t *mvid) noexcept +const char* clr_typemap_managed_to_java (const char *typeName, const char *assemblyFullName, const uint8_t *mvid) noexcept { +#if defined(RELEASE) return TypeMapper::managed_to_java (typeName, mvid); +#else + return TypeMapper::managed_to_java (typeName, assemblyFullName); +#endif } bool clr_typemap_java_to_managed (const char *java_type_name, char const** assembly_name, uint32_t *managed_type_token_id) noexcept diff --git a/src/native/clr/host/typemap.cc b/src/native/clr/host/typemap.cc index 020ec84669a..23f6e7a2afb 100644 --- a/src/native/clr/host/typemap.cc +++ b/src/native/clr/host/typemap.cc @@ -150,26 +150,14 @@ auto TypeMapper::index_to_name (ssize_t idx, const char* typeName, const TypeMap } [[gnu::always_inline, gnu::flatten]] -auto TypeMapper::managed_to_java_debug (const char *typeName, const uint8_t *mvid) noexcept -> const char* +auto TypeMapper::managed_to_java_debug (const char *typeName, const char *assemblyFullName) noexcept -> const char* { dynamic_local_path_string full_type_name; full_type_name.append (typeName); + full_type_name.append (", "sv); + full_type_name.append (assemblyFullName); - auto equal = [](TypeMapAssembly const& entry, const uint8_t *key) -> bool { return memcmp (entry.module_uuid, key, sizeof(entry.module_uuid)) == 0; }; - auto less_than = [](TypeMapAssembly const& entry, const uint8_t *key) -> bool { return memcmp (entry.module_uuid, key, sizeof(entry.module_uuid)) < 0; }; - ssize_t idx = Search::binary_search (mvid, type_map_unique_assemblies, type_map.unique_assemblies_count); - - if (idx >= 0) [[likely]] { - TypeMapAssembly const& assm = type_map_unique_assemblies[idx]; - full_type_name.append (", "sv); - - // We explicitly trust the build process here, with regards to validity of offsets - full_type_name.append (&type_map_assembly_names[assm.name_offset], assm.name_length); - } else { - log_warn (LOG_ASSEMBLY, "typemap: unable to look up assembly name for type '{}', trying without it."sv, typeName); - } - - idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA); + ssize_t idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA); return index_to_name (idx, full_type_name.get (), type_map.managed_to_java, type_map_java_type_names, MANAGED, JAVA); } @@ -320,7 +308,11 @@ auto TypeMapper::managed_to_java_release (const char *typeName, const uint8_t *m #endif // def RELEASE [[gnu::flatten]] +#if defined(RELEASE) auto TypeMapper::managed_to_java (const char *typeName, const uint8_t *mvid) noexcept -> const char* +#else +auto TypeMapper::managed_to_java (const char *typeName, const char *assemblyFullName) noexcept -> const char* +#endif { log_debug (LOG_ASSEMBLY, "managed_to_java: looking up type '{}'"sv, optional_string (typeName)); if (FastTiming::enabled ()) [[unlikely]] { @@ -332,14 +324,15 @@ auto TypeMapper::managed_to_java (const char *typeName, const uint8_t *mvid) noe return nullptr; } - auto do_map = [&typeName, &mvid]() -> const char* { #if defined(RELEASE) - return managed_to_java_release (typeName, mvid); + const char *ret = managed_to_java_release (typeName, mvid); #else - return managed_to_java_debug (typeName, mvid); + if (assemblyFullName == nullptr) [[unlikely]] { + log_warn (LOG_ASSEMBLY, "typemap: assembly name not specified in typemap_managed_to_java"sv); + return nullptr; + } + const char *ret = managed_to_java_debug (typeName, assemblyFullName); #endif - }; - const char *ret = do_map (); if (FastTiming::enabled ()) [[unlikely]] { internal_timing.end_event (); diff --git a/src/native/clr/include/host/typemap.hh b/src/native/clr/include/host/typemap.hh index 6a3ee0565e5..d8dbe37cc9d 100644 --- a/src/native/clr/include/host/typemap.hh +++ b/src/native/clr/include/host/typemap.hh @@ -13,7 +13,11 @@ namespace xamarin::android { static constexpr std::string_view JAVA { "Java" }; public: +#if defined(RELEASE) static auto managed_to_java (const char *typeName, const uint8_t *mvid) noexcept -> const char*; +#else + static auto managed_to_java (const char *typeName, const char *assemblyFullName) noexcept -> const char*; +#endif static auto java_to_managed (const char *java_type_name, char const** assembly_name, uint32_t *managed_type_token_id) noexcept -> bool; private: @@ -29,7 +33,7 @@ namespace xamarin::android { static auto index_to_name (ssize_t index, const char *typeName, const TypeMapEntry *map, const char (&name_map)[], std::string_view const& from_name, std::string_view const& to_name) -> const char*; static auto find_index_by_hash (const char *typeName, const TypeMapEntry *map, const char (&name_map)[], std::string_view const& from_name, std::string_view const& to_name) noexcept -> ssize_t; static auto find_index_by_name (const char *typeName, const TypeMapEntry *map, const char (&name_map)[], std::string_view const& from_name, std::string_view const& to_name) noexcept -> ssize_t; - static auto managed_to_java_debug (const char *typeName, const uint8_t *mvid) noexcept -> const char*; + static auto managed_to_java_debug (const char *typeName, const char *assemblyFullName) noexcept -> const char*; static auto java_to_managed_debug (const char *java_type_name, char const** assembly_name, uint32_t *managed_type_token_id) noexcept -> bool; #endif }; diff --git a/src/native/clr/include/runtime-base/internal-pinvokes.hh b/src/native/clr/include/runtime-base/internal-pinvokes.hh index 5f67a5397f9..a5408b45046 100644 --- a/src/native/clr/include/runtime-base/internal-pinvokes.hh +++ b/src/native/clr/include/runtime-base/internal-pinvokes.hh @@ -15,7 +15,7 @@ extern "C" { void _monodroid_gref_log (const char *message) noexcept; int _monodroid_gref_log_new (jobject curHandle, char curType, jobject newHandle, char newType, const char *threadName, int threadId, const char *from, int from_writable) noexcept; void _monodroid_gref_log_delete (jobject handle, char type, const char *threadName, int threadId, const char *from, int from_writable) noexcept; - const char* clr_typemap_managed_to_java (const char *typeName, const uint8_t *mvid) noexcept; + const char* clr_typemap_managed_to_java (const char *typeName, const char *assemblyFullName, const uint8_t *mvid) noexcept; bool clr_typemap_java_to_managed (const char *java_type_name, char const** assembly_name, uint32_t *managed_type_token_id) noexcept; BridgeProcessingFtn clr_initialize_gc_bridge ( BridgeProcessingStartedFtn bridge_processing_started_callback, diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index e6335f52db6..1fdb13b78a4 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -60,18 +60,9 @@ struct TypeMapManagedTypeInfo struct TypeMap { uint32_t entry_count; - uint64_t unique_assemblies_count; const TypeMapEntry *java_to_managed; const TypeMapEntry *managed_to_java; }; - -// MUST match src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs -struct TypeMapAssembly -{ - uint8_t module_uuid[16]; - uint64_t name_length; - uint64_t name_offset; // into the assembly names blob -}; #else struct TypeMapModuleEntry { @@ -295,7 +286,6 @@ extern "C" { #if defined (DEBUG) [[gnu::visibility("default")]] extern const TypeMap type_map; // MUST match src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs [[gnu::visibility("default")]] extern const TypeMapManagedTypeInfo type_map_managed_type_info[]; - [[gnu::visibility("default")]] extern const TypeMapAssembly type_map_unique_assemblies[]; [[gnu::visibility("default")]] extern const char type_map_assembly_names[]; [[gnu::visibility("default")]] extern const char type_map_managed_type_names[]; [[gnu::visibility("default")]] extern const char type_map_java_type_names[]; diff --git a/src/native/clr/xamarin-app-stub/application_dso_stub.cc b/src/native/clr/xamarin-app-stub/application_dso_stub.cc index 598418e3235..da3b6c4131c 100644 --- a/src/native/clr/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/clr/xamarin-app-stub/application_dso_stub.cc @@ -14,13 +14,11 @@ static TypeMapEntry managed_to_java[] = {}; // MUST match src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGenerator.cs const TypeMap type_map = { .entry_count = 0, - .unique_assemblies_count = 0, .java_to_managed = java_to_managed, .managed_to_java = managed_to_java, }; const TypeMapManagedTypeInfo type_map_managed_type_info[] = {}; -const TypeMapAssembly type_map_unique_assemblies[] = {}; const char type_map_assembly_names[] = {}; const char type_map_managed_type_names[] = {}; const char type_map_java_type_names[] = {}; diff --git a/src/native/nativeaot/host/internal-pinvoke-stubs.cc b/src/native/nativeaot/host/internal-pinvoke-stubs.cc index 1c59786a9b1..1e7dd83833b 100644 --- a/src/native/nativeaot/host/internal-pinvoke-stubs.cc +++ b/src/native/nativeaot/host/internal-pinvoke-stubs.cc @@ -18,6 +18,7 @@ namespace { const char* clr_typemap_managed_to_java ( [[maybe_unused]] const char *typeName, + [[maybe_unused]] const char *assemblyFullName, [[maybe_unused]] const uint8_t *mvid) noexcept { pinvoke_unreachable (); From 707b87f65e06b20caf260a8c1757164fcee75899 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Jul 2026 14:35:01 -0500 Subject: [PATCH 02/15] [CoreCLR] Fix Debug typemap review issues Keep the empty Debug LLVM typemap stub layouts synchronized with the native structures after removing the MVID table. Mark configuration-specific internal P/Invoke parameters as potentially unused and clarify the missing assembly full-name diagnostic. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54c1e7b6-a69c-41bc-812d-5acc5bfdffee --- .../Tasks/GenerateEmptyTypemapStub.cs | 4 ++-- src/native/clr/host/internal-pinvokes-clr.cc | 6 +++++- src/native/clr/host/typemap.cc | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs index 2e7f8e4006a..d2373c22d00 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs @@ -66,8 +66,8 @@ string GenerateStubLlvmIr (string abi) if (Debug) { return header + """ -%struct.TypeMap = type { i32, i32, ptr, ptr } -%struct.TypeMapManagedTypeInfo = type { i64, i32, i32 } +%struct.TypeMap = type { i32, ptr, ptr } +%struct.TypeMapManagedTypeInfo = type { i32, i32 } @type_map = dso_local constant %struct.TypeMap zeroinitializer, align 8 @type_map_managed_type_info = dso_local constant [0 x %struct.TypeMapManagedTypeInfo] zeroinitializer, align 8 diff --git a/src/native/clr/host/internal-pinvokes-clr.cc b/src/native/clr/host/internal-pinvokes-clr.cc index e5f0865c247..844f9b748f0 100644 --- a/src/native/clr/host/internal-pinvokes-clr.cc +++ b/src/native/clr/host/internal-pinvokes-clr.cc @@ -9,7 +9,11 @@ using namespace xamarin::android; -const char* clr_typemap_managed_to_java (const char *typeName, const char *assemblyFullName, const uint8_t *mvid) noexcept +const char* clr_typemap_managed_to_java ( + const char *typeName, + [[maybe_unused]] const char *assemblyFullName, + [[maybe_unused]] const uint8_t *mvid +) noexcept { #if defined(RELEASE) return TypeMapper::managed_to_java (typeName, mvid); diff --git a/src/native/clr/host/typemap.cc b/src/native/clr/host/typemap.cc index 23f6e7a2afb..d8ed207920b 100644 --- a/src/native/clr/host/typemap.cc +++ b/src/native/clr/host/typemap.cc @@ -328,7 +328,7 @@ auto TypeMapper::managed_to_java (const char *typeName, const char *assemblyFull const char *ret = managed_to_java_release (typeName, mvid); #else if (assemblyFullName == nullptr) [[unlikely]] { - log_warn (LOG_ASSEMBLY, "typemap: assembly name not specified in typemap_managed_to_java"sv); + log_warn (LOG_ASSEMBLY, "typemap: assembly full name not specified in typemap_managed_to_java"sv); return nullptr; } const char *ret = managed_to_java_debug (typeName, assemblyFullName); From 0986fe64ababcf2e9653220095db4f05fba60b71 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Jul 2026 14:58:25 -0500 Subject: [PATCH 03/15] [CoreCLR] Match runtime assembly display names Mono.Cecil does not escape assembly display-name characters in the same way as System.Reflection.Assembly.FullName. Format Debug typemap assembly identities through System.Reflection.AssemblyName so generated keys match runtime keys for names containing commas and other escaped characters. Add coverage for an escaped assembly simple name. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54c1e7b6-a69c-41bc-812d-5acc5bfdffee --- .../MonoDroid.Tuner/FindTypeMapObjectsStep.cs | 2 +- .../Tasks/LlvmIrGeneratorTests.cs | 13 ++++++++++++ .../Utilities/TypeMapCecilAdapter.cs | 20 ++++++++++++++++++- 3 files changed, 33 insertions(+), 2 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 2eb793f3a54..43a9b8de334 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs @@ -37,7 +37,7 @@ public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) var xml = new TypeMapObjectsXmlFile { AssemblyName = assembly.Name.Name, - AssemblyFullName = Debug ? assembly.Name.FullName : null, + AssemblyFullName = Debug ? TypeMapCecilAdapter.GetRuntimeAssemblyFullName (assembly.Name) : null, AssemblyMvid = assembly.MainModule.Mvid, }; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs index 7013ebc9367..6717ed30e1d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.IO; using Microsoft.Build.Utilities; +using Mono.Cecil; using NUnit.Framework; +using Xamarin.Android.Tasks; using Xamarin.Android.Tasks.LLVMIR; using Xamarin.Android.Tools; @@ -79,5 +81,16 @@ public void GeneratedIR_FunctionWithWhitespaceParameterName_ProducesValidOutput Assert.That (output, Does.Not.Contain ("%\t)"), "Generated LLVM IR should not contain 'ptr noundef %\\t)' pattern"); Assert.That (output, Does.Contain ("@test_function"), "Generated LLVM IR should contain the function name"); } + + [Test] + public void TypeMapAssemblyFullNameUsesRuntimeEscaping () + { + var assemblyName = new AssemblyNameDefinition ("Comma,Name", new Version (1, 2, 3, 4)); + + Assert.That ( + TypeMapCecilAdapter.GetRuntimeAssemblyFullName (assemblyName), + Is.EqualTo (@"Comma\,Name, Version=1.2.3.4, Culture=neutral, PublicKeyToken=null") + ); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs index 1413f46bb8d..f3327506f2c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs @@ -3,6 +3,9 @@ using Java.Interop.Tools.Cecil; using Mono.Cecil; +using ReflectionAssemblyContentType = System.Reflection.AssemblyContentType; +using ReflectionAssemblyName = System.Reflection.AssemblyName; +using ReflectionAssemblyNameFlags = System.Reflection.AssemblyNameFlags; using ModuleReleaseData = Xamarin.Android.Tasks.TypeMapGenerator.ModuleReleaseData; using ReleaseGenerationState = Xamarin.Android.Tasks.TypeMapGenerator.ReleaseGenerationState; using TypeMapDebugEntry = Xamarin.Android.Tasks.TypeMapGenerator.TypeMapDebugEntry; @@ -149,10 +152,25 @@ static TypeMapDebugEntry GetDebugEntry (TypeDefinition td, TypeDefinitionCache c TypeDefinition = td, SkipInJavaToManaged = ShouldSkipInJavaToManaged (td), AssemblyName = td.Module.Assembly.Name.Name, - AssemblyFullName = td.Module.Assembly.Name.FullName, + AssemblyFullName = GetRuntimeAssemblyFullName (td.Module.Assembly.Name), }; } + // Cecil's FullName does not escape assembly display names in the same way as Assembly.FullName. + public static string GetRuntimeAssemblyFullName (AssemblyNameReference assemblyName) + { + var runtimeAssemblyName = new ReflectionAssemblyName { + Name = assemblyName.Name, + Version = assemblyName.Version, + CultureName = assemblyName.Culture ?? "", + Flags = assemblyName.IsRetargetable ? ReflectionAssemblyNameFlags.Retargetable : ReflectionAssemblyNameFlags.None, + ContentType = assemblyName.IsWindowsRuntime ? ReflectionAssemblyContentType.WindowsRuntime : ReflectionAssemblyContentType.Default, + }; + runtimeAssemblyName.SetPublicKeyToken (assemblyName.PublicKeyToken); + + return runtimeAssemblyName.FullName ?? throw new InvalidOperationException ($"Unable to format assembly name '{assemblyName.Name}'."); + } + static string GetManagedTypeName (TypeDefinition td) { // This is necessary because Mono runtime will return to us type name with a `.` for nested types (not a From a24fe820364c791f610470cde44acaf3c364dd67 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 23 Jul 2026 16:52:11 -0500 Subject: [PATCH 04/15] [CoreCLR] Harden Debug typemap assembly identities Fail generation when a managed-to-Java entry lacks the assembly full name required by CoreCLR Debug lookup. Verify the Cecil-to-runtime formatter against the actual strong-named build-task assembly in addition to escaped simple-name coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54c1e7b6-a69c-41bc-812d-5acc5bfdffee --- .../Tasks/LlvmIrGeneratorTests.cs | 11 +++++++++++ .../TypeMappingDebugNativeAssemblyGeneratorCLR.cs | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs index 6717ed30e1d..effa0229108 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs @@ -92,5 +92,16 @@ public void TypeMapAssemblyFullNameUsesRuntimeEscaping () Is.EqualTo (@"Comma\,Name, Version=1.2.3.4, Culture=neutral, PublicKeyToken=null") ); } + + [Test] + public void TypeMapAssemblyFullNameMatchesStrongNamedRuntimeAssembly () + { + string assemblyPath = typeof (TypeMapCecilAdapter).Assembly.Location; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath); + string? runtimeFullName = System.Reflection.AssemblyName.GetAssemblyName (assemblyPath).FullName; + + Assert.That (runtimeFullName, Does.Not.EndWith ("PublicKeyToken=null")); + Assert.That (TypeMapCecilAdapter.GetRuntimeAssemblyFullName (assembly.Name), Is.EqualTo (runtimeFullName)); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs index cb724809853..62e27292ecf 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs @@ -182,6 +182,10 @@ protected override void Construct (LlvmIrModule module) // in a callback during code generation foreach (TypeMapGenerator.TypeMapDebugEntry entry in data.ManagedToJavaMap) { + if (String.IsNullOrEmpty (entry.AssemblyFullName)) { + throw new InvalidOperationException ($"Internal error: assembly full name is missing for managed type '{entry.ManagedName}'."); + } + if (!entry.ManagedName.EndsWith (entry.AssemblyName, StringComparison.Ordinal)) { throw new InvalidOperationException ($"Internal error: managed type name '{entry.ManagedName}' does not end with assembly name '{entry.AssemblyName}'."); } From 0496f5217e1f587dc4ee242bffc14131c1b730cc Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Jul 2026 08:56:09 -0500 Subject: [PATCH 05/15] [Tests] Expect C# fast deployment to skip APK install Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 54c1e7b6-a69c-41bc-812d-5acc5bfdffee --- tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs index 46fa0f491be..0c46530828a 100644 --- a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs @@ -183,7 +183,7 @@ public void SkipFastDevAlreadyInstalledFile () proj.Touch ("MainActivity.cs"); // make sure that the fastdev log tells that the relevant dll is updated but NOT for others. Assert.IsTrue (b.Install (proj, doNotCleanupOnUpdate: true, saveProject: false), "install should have succeeded."); - Assert.IsTrue (b.Output.IsApkInstalled, "app apk was not installed"); + Assert.IsFalse (b.Output.IsApkInstalled, "app apk was reinstalled"); Assert.IsTrue (b.LastBuildOutput.Any (l => l.Contains ("UnnamedProject.dll") && l.Contains ("NotifySync CopyFile")), "app dll not uploaded"); var assemblies = new[] { From 7e99205445350b9476f073380eaae915021eac62 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Jul 2026 13:35:42 -0500 Subject: [PATCH 06/15] [CoreCLR] Skip MVID work on the Debug typemap path The Debug CoreCLR typemap is keyed on the assembly display name and the native side ignores the MVID argument, yet `TypemapManagedToJava` still computed `Module.ModuleVersionId` for every lookup. Split the lookup so the MVID is only computed when it is actually consumed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 52 ++++++++++++---------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index c1ba01a5be8..02be0c6d075 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -441,34 +441,38 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) return TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName) ? jniName : null; } - if (mvid_bytes == null) - mvid_bytes = new byte[16]; - - var mvid = new Span(mvid_bytes); - byte[]? mvid_data = null; - if (!type.Module.ModuleVersionId.TryWriteBytes (mvid)) { - RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default, $"Failed to obtain module MVID using the fast method, falling back to the slow one"); - mvid_data = type.Module.ModuleVersionId.ToByteArray (); + IntPtr ret; + if (RuntimeFeature.IsCoreClrRuntime && RuntimeFeature.ManagedToJavaUsesAssemblyFullName) { + // These typemaps are keyed on the assembly display name, so computing the MVID would be wasted work. + if (type.FullName is null) + return null; + string? assemblyFullName = type.Assembly.FullName; + if (assemblyFullName is null) + return null; + ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, assemblyFullName, IntPtr.Zero); } else { - mvid_data = mvid_bytes; - } + if (mvid_bytes == null) + mvid_bytes = new byte[16]; + + var mvid = new Span(mvid_bytes); + byte[]? mvid_data = null; + if (!type.Module.ModuleVersionId.TryWriteBytes (mvid)) { + RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default, $"Failed to obtain module MVID using the fast method, falling back to the slow one"); + mvid_data = type.Module.ModuleVersionId.ToByteArray (); + } else { + mvid_data = mvid_bytes; + } - IntPtr ret; - fixed (byte* mvidptr = mvid_data) { - if (RuntimeFeature.IsMonoRuntime) { - ret = monovm_typemap_managed_to_java (type, mvidptr); - } else if (RuntimeFeature.IsCoreClrRuntime) { - if (type.FullName is null) - return null; - string? assemblyFullName = null; - if (RuntimeFeature.ManagedToJavaUsesAssemblyFullName) { - assemblyFullName = type.Assembly.FullName; - if (assemblyFullName is null) + fixed (byte* mvidptr = mvid_data) { + if (RuntimeFeature.IsMonoRuntime) { + ret = monovm_typemap_managed_to_java (type, mvidptr); + } else if (RuntimeFeature.IsCoreClrRuntime) { + if (type.FullName is null) return null; + ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, null, (IntPtr)mvidptr); + } else { + throw new NotSupportedException ("Internal error: unknown runtime not supported"); } - ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, assemblyFullName, (IntPtr)mvidptr); - } else { - throw new NotSupportedException ("Internal error: unknown runtime not supported"); } } From 777d0789c3c49bcbc44954a5f780e4a5fa348ce1 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Jul 2026 13:38:58 -0500 Subject: [PATCH 07/15] [CoreCLR] Simplify the typemap MVID branching Keep the single `fixed`/runtime dispatch block and only guard the MVID computation itself, instead of duplicating the CoreCLR call. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 39 +++++++++++----------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 02be0c6d075..370ad012789 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -441,38 +441,39 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) return TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName) ? jniName : null; } - IntPtr ret; - if (RuntimeFeature.IsCoreClrRuntime && RuntimeFeature.ManagedToJavaUsesAssemblyFullName) { - // These typemaps are keyed on the assembly display name, so computing the MVID would be wasted work. - if (type.FullName is null) - return null; - string? assemblyFullName = type.Assembly.FullName; - if (assemblyFullName is null) - return null; - ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, assemblyFullName, IntPtr.Zero); - } else { + // These typemaps are keyed on the assembly display name, so computing the MVID would be wasted work. + bool useAssemblyFullName = RuntimeFeature.IsCoreClrRuntime && RuntimeFeature.ManagedToJavaUsesAssemblyFullName; + + byte[]? mvid_data = null; + if (!useAssemblyFullName) { if (mvid_bytes == null) mvid_bytes = new byte[16]; var mvid = new Span(mvid_bytes); - byte[]? mvid_data = null; if (!type.Module.ModuleVersionId.TryWriteBytes (mvid)) { RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default, $"Failed to obtain module MVID using the fast method, falling back to the slow one"); mvid_data = type.Module.ModuleVersionId.ToByteArray (); } else { mvid_data = mvid_bytes; } + } - fixed (byte* mvidptr = mvid_data) { - if (RuntimeFeature.IsMonoRuntime) { - ret = monovm_typemap_managed_to_java (type, mvidptr); - } else if (RuntimeFeature.IsCoreClrRuntime) { - if (type.FullName is null) + IntPtr ret; + fixed (byte* mvidptr = mvid_data) { + if (RuntimeFeature.IsMonoRuntime) { + ret = monovm_typemap_managed_to_java (type, mvidptr); + } else if (RuntimeFeature.IsCoreClrRuntime) { + if (type.FullName is null) + return null; + string? assemblyFullName = null; + if (useAssemblyFullName) { + assemblyFullName = type.Assembly.FullName; + if (assemblyFullName is null) return null; - ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, null, (IntPtr)mvidptr); - } else { - throw new NotSupportedException ("Internal error: unknown runtime not supported"); } + ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, assemblyFullName, (IntPtr)mvidptr); + } else { + throw new NotSupportedException ("Internal error: unknown runtime not supported"); } } From 4e0678e43bc47432e973cbf6b9ac434312eb6977 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Jul 2026 13:39:48 -0500 Subject: [PATCH 08/15] [CoreCLR] Inline the typemap MVID condition Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 370ad012789..41014896524 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -441,11 +441,9 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) return TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName) ? jniName : null; } - // These typemaps are keyed on the assembly display name, so computing the MVID would be wasted work. - bool useAssemblyFullName = RuntimeFeature.IsCoreClrRuntime && RuntimeFeature.ManagedToJavaUsesAssemblyFullName; - byte[]? mvid_data = null; - if (!useAssemblyFullName) { + // The Debug CoreCLR typemaps are keyed on the assembly display name, so computing the MVID would be wasted work. + if (!RuntimeFeature.IsCoreClrRuntime || !RuntimeFeature.ManagedToJavaUsesAssemblyFullName) { if (mvid_bytes == null) mvid_bytes = new byte[16]; @@ -466,7 +464,7 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) if (type.FullName is null) return null; string? assemblyFullName = null; - if (useAssemblyFullName) { + if (RuntimeFeature.ManagedToJavaUsesAssemblyFullName) { assemblyFullName = type.Assembly.FullName; if (assemblyFullName is null) return null; From 512d558e3ae26a12590d005036b7602365c5446a Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Jul 2026 13:44:07 -0500 Subject: [PATCH 09/15] [CoreCLR] Let native handle a null assembly display name Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 41014896524..2b014fe3a49 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -463,12 +463,7 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) } else if (RuntimeFeature.IsCoreClrRuntime) { if (type.FullName is null) return null; - string? assemblyFullName = null; - if (RuntimeFeature.ManagedToJavaUsesAssemblyFullName) { - assemblyFullName = type.Assembly.FullName; - if (assemblyFullName is null) - return null; - } + string? assemblyFullName = RuntimeFeature.ManagedToJavaUsesAssemblyFullName ? type.Assembly.FullName : null; ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, assemblyFullName, (IntPtr)mvidptr); } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); From dd0845175700d72bd9bdc23d8884134bcda89138 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 24 Jul 2026 13:45:41 -0500 Subject: [PATCH 10/15] [CoreCLR] Use log_warnf for the new typemap diagnostic Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- src/native/clr/host/typemap.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/clr/host/typemap.cc b/src/native/clr/host/typemap.cc index d8ed207920b..71748a13410 100644 --- a/src/native/clr/host/typemap.cc +++ b/src/native/clr/host/typemap.cc @@ -328,7 +328,7 @@ auto TypeMapper::managed_to_java (const char *typeName, const char *assemblyFull const char *ret = managed_to_java_release (typeName, mvid); #else if (assemblyFullName == nullptr) [[unlikely]] { - log_warn (LOG_ASSEMBLY, "typemap: assembly full name not specified in typemap_managed_to_java"sv); + log_warnf (LOG_ASSEMBLY, "typemap: assembly full name not specified in typemap_managed_to_java"); return nullptr; } const char *ret = managed_to_java_debug (typeName, assemblyFullName); From 9e6b574b0eedd7c823273d88d06506aae546fe13 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 28 Jul 2026 08:07:27 -0500 Subject: [PATCH 11/15] [Tests] Add the new activity in the incremental fast deploy build `FastDeployUpdatesTypeMapAfterAssemblyEdit` declared `SecondActivity` before the first build, so both deployments shipped the same set of Java-callable types and the test could not tell whether the type map was updated by the incremental build. Deploy only `MainActivity` first, then add `SecondActivity` with a C#-only edit for the second build, and assert at each step that the Java stub, dex, native, packaging, and signing targets all run. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- .../Tests/FastDevTest.cs | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs index 0c46530828a..a371ea9ad8a 100644 --- a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs @@ -46,6 +46,30 @@ public void FastDeployUpdatesTypeMapAfterAssemblyEdit () }; proj.SetDefaultTargetDevice (); proj.SetProperty ("_AndroidFastDevStrategy", "FastDeploy"); + + // Whenever the set of Java-callable types changes, the Java stubs, the typemap, the native + // libraries embedding it, and the .apk containing them all have to be regenerated. + var typeMapTargets = new [] { + "_GenerateJavaStubs", + "_CompileJava", + "_CompileToDalvik", + "_CompileNativeAssemblySources", + "_CreateApplicationSharedLibraries", + "_BuildApkFastDev", + "_Sign", + }; + + using var builder = CreateApkBuilder (); + + // 1. Initial build and deployment, with only MainActivity. + Assert.IsTrue (builder.Install (proj), "Initial install should have succeeded."); + foreach (var target in typeMapTargets) { + builder.Output.AssertTargetIsNotSkipped (target, occurrence: 1); + } + Assert.IsTrue (builder.Output.IsApkInstalled, "The .apk should have been installed by the initial build."); + AssertActivityStarts ("MainActivity", "initial-launch.log"); + + // 2. A C#-only change that adds a new Java-callable type, so the type map *must* be updated. proj.MainActivity = proj.DefaultMainActivity .Replace ("//${AFTER_ONCREATE}", "StartActivity (new Android.Content.Intent (this, typeof (SecondActivity)));") .Replace ("//${AFTER_MAINACTIVITY}", """ @@ -54,29 +78,30 @@ public sealed class SecondActivity : Activity { } """); - - using var builder = CreateApkBuilder (); - Assert.IsTrue (builder.Install (proj), "Initial install should have succeeded."); - AssertSecondActivityStarts ("initial-launch.log"); - - proj.MainActivity += "// Incremental C# edit."; proj.Touch ("MainActivity.cs"); Assert.IsTrue (builder.Install (proj, doNotCleanupOnUpdate: true, saveProject: false), "Incremental install should have succeeded."); - AssertSecondActivityStarts ("incremental-launch.log"); + + builder.Output.AssertTargetIsNotSkipped ("CoreCompile", occurrence: 2); + foreach (var target in typeMapTargets) { + builder.Output.AssertTargetIsNotSkipped (target, occurrence: 2); + } + Assert.IsTrue (builder.Output.IsApkInstalled, "The .apk should have been reinstalled after adding a new activity."); + AssertActivityStarts ("SecondActivity", "incremental-launch.log"); + Assert.IsTrue (builder.Uninstall (proj), "Uninstall should have succeeded."); - void AssertSecondActivityStarts (string logFileName) + void AssertActivityStarts (string activityName, string logFileName) { ClearAdbLogcat (); AdbStartActivity ($"{proj.PackageName}/{proj.JavaPackageName}.MainActivity"); Assert.IsTrue ( WaitForActivityToStart ( proj.PackageName, - "SecondActivity", + activityName, Path.Combine (Root, builder.ProjectDirectory, logFileName), ActivityStartTimeoutInSeconds ), - "SecondActivity should have started." + $"{activityName} should have started." ); } } From 5a2ccd9e05f9d5d64c3118eb8a84551d5d1a64f7 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 28 Jul 2026 08:16:02 -0500 Subject: [PATCH 12/15] [Tests] Assert the .apk is untouched by a C#-only fast deploy Split `FastDeployUpdatesTypeMapAfterAssemblyEdit` into three build + deploy steps: 1. Deploy with only `MainActivity`. 2. Add a `SecondActivity`, which introduces a new Java-callable type. Fast deployment only syncs managed assemblies, so the Java stubs, .dex, type map, .apk and signature all have to be rebuilt and the package reinstalled. This answers the review question of whether a C#-only edit that *does* change the type map is picked up automatically: it is, no clean required. 3. Add a `Console.WriteLine()` to `SecondActivity.OnCreate()`. No Java-callable types change, so the type map is stable and `_CompileNativeAssemblySources`, `_CreateApplicationSharedLibraries`, `_BuildApkFastDev` and `_Sign` are all skipped and the .apk is not reinstalled. The message is asserted in `logcat` to prove the updated assembly really was deployed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- .../Tests/FastDevTest.cs | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs index a371ea9ad8a..7ca9d3dba79 100644 --- a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs @@ -41,14 +41,17 @@ public void FastDevSimpleBuild () [Test] public void FastDeployUpdatesTypeMapAfterAssemblyEdit () { + const string logcatMessage = "FAST_DEPLOY_TYPEMAP_TEST_MESSAGE"; + var proj = new XamarinAndroidApplicationProject { PackageName = "com.xamarin.fastdeploy_typemap", }; proj.SetDefaultTargetDevice (); proj.SetProperty ("_AndroidFastDevStrategy", "FastDeploy"); - // Whenever the set of Java-callable types changes, the Java stubs, the typemap, the native - // libraries embedding it, and the .apk containing them all have to be regenerated. + // Fast deployment only syncs managed assemblies, so anything that changes the set of + // Java-callable types has to go through a new .apk: new Java stubs, a new .dex, a new + // type map inside libxamarin-app.so, and therefore a new signed package. var typeMapTargets = new [] { "_GenerateJavaStubs", "_CompileJava", @@ -63,9 +66,6 @@ public void FastDeployUpdatesTypeMapAfterAssemblyEdit () // 1. Initial build and deployment, with only MainActivity. Assert.IsTrue (builder.Install (proj), "Initial install should have succeeded."); - foreach (var target in typeMapTargets) { - builder.Output.AssertTargetIsNotSkipped (target, occurrence: 1); - } Assert.IsTrue (builder.Output.IsApkInstalled, "The .apk should have been installed by the initial build."); AssertActivityStarts ("MainActivity", "initial-launch.log"); @@ -76,17 +76,47 @@ public void FastDeployUpdatesTypeMapAfterAssemblyEdit () [Activity (Label = "Fast Deploy Result")] public sealed class SecondActivity : Activity { + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + //${SECOND_ACTIVITY_ONCREATE} + } } """); proj.Touch ("MainActivity.cs"); - Assert.IsTrue (builder.Install (proj, doNotCleanupOnUpdate: true, saveProject: false), "Incremental install should have succeeded."); + Assert.IsTrue (builder.Install (proj, doNotCleanupOnUpdate: true, saveProject: false), "Install of the new activity should have succeeded."); builder.Output.AssertTargetIsNotSkipped ("CoreCompile", occurrence: 2); foreach (var target in typeMapTargets) { builder.Output.AssertTargetIsNotSkipped (target, occurrence: 2); } Assert.IsTrue (builder.Output.IsApkInstalled, "The .apk should have been reinstalled after adding a new activity."); - AssertActivityStarts ("SecondActivity", "incremental-launch.log"); + AssertActivityStarts ("SecondActivity", "new-activity-launch.log"); + + // 3. A C#-only change that leaves the Java-callable types alone. The type map is unchanged, + // so the .apk is neither rebuilt nor reinstalled and only the assembly is fast deployed. + proj.MainActivity = proj.MainActivity.Replace ("//${SECOND_ACTIVITY_ONCREATE}", $"Console.WriteLine (\"{logcatMessage}\");"); + proj.Touch ("MainActivity.cs"); + Assert.IsTrue (builder.Install (proj, doNotCleanupOnUpdate: true, saveProject: false), "Incremental install should have succeeded."); + + builder.Output.AssertTargetIsNotSkipped ("CoreCompile", occurrence: 3); + foreach (var target in new [] { "_CompileNativeAssemblySources", "_CreateApplicationSharedLibraries", "_BuildApkFastDev", "_Sign" }) { + builder.Output.AssertTargetIsSkipped (target, occurrence: 3); + } + Assert.IsFalse (builder.Output.IsApkInstalled, "The .apk should not be reinstalled for a C#-only change."); + + ClearAdbLogcat (); + Assert.IsTrue ( + MonitorAdbLogcat ( + line => line.Contains (logcatMessage), + Path.Combine (Root, builder.ProjectDirectory, "incremental-launch.log"), + ActivityStartTimeoutInSeconds, + onMonitoringStarted: () => { + AdbStartActivity ($"{proj.PackageName}/{proj.JavaPackageName}.MainActivity"); + } + ), + $"`{logcatMessage}` should have been logged by the fast deployed assembly." + ); Assert.IsTrue (builder.Uninstall (proj), "Uninstall should have succeeded."); From ebd837cec11985e14646d1cd1bdf8bf148ab773b Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 28 Jul 2026 10:52:16 -0500 Subject: [PATCH 13/15] [Tests] Use detailed verbosity so IsApkInstalled works `FastDeployUpdatesTypeMapAfterAssemblyEdit` failed on CI with: The .apk should have been installed by the initial build. Expected: True But was: False `BuildOutput.IsApkInstalled` scans the build output for the `Installed Package` message, which `FastDeploy` writes with `LogDebugMessageWithTiming()`. That is `MessageImportance.Low`, so it never appears at the default `LoggerVerbosity.Normal` and the property is always `false`. `SkipFastDevAlreadyInstalledResources` already sets `LoggerVerbosity.Detailed` for the same reason, so do the same here. This also makes the `Assert.IsFalse (IsApkInstalled)` in the final step meaningful -- at `Normal` verbosity it passed no matter what the build actually did. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs index 7ca9d3dba79..715d4940840 100644 --- a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs @@ -63,6 +63,10 @@ public void FastDeployUpdatesTypeMapAfterAssemblyEdit () }; using var builder = CreateApkBuilder (); + // `IsApkInstalled` looks for the "Installed Package" message that `FastDeploy` logs via + // `LogDebugMessageWithTiming()`, which is `MessageImportance.Low` and therefore invisible + // at the default `Normal` verbosity. + builder.Verbosity = LoggerVerbosity.Detailed; // 1. Initial build and deployment, with only MainActivity. Assert.IsTrue (builder.Install (proj), "Initial install should have succeeded."); From 0b2456c5d3db0f1b29f76edd0007d26c2f65df29 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 28 Jul 2026 10:53:42 -0500 Subject: [PATCH 14/15] [Tests] Cover both fast deploy strategies in the type map test `FastDeploy2` is the default strategy, so pinning the test to the legacy `FastDeploy` only exercised the path most users never take. Parameterize `FastDeployUpdatesTypeMapAfterAssemblyEdit` over both strategies instead, with a distinct package name per case so the two runs cannot interfere with each other on the device. The type map assertions are strategy-independent -- both strategies only sync managed assemblies -- but the .apk install assertions are not, so it is worth confirming both behave the same. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs index 715d4940840..81d0a5f6f35 100644 --- a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs @@ -39,15 +39,17 @@ public void FastDevSimpleBuild () } [Test] - public void FastDeployUpdatesTypeMapAfterAssemblyEdit () + [TestCase ("FastDeploy")] + [TestCase ("FastDeploy2")] + public void FastDeployUpdatesTypeMapAfterAssemblyEdit (string strategy) { const string logcatMessage = "FAST_DEPLOY_TYPEMAP_TEST_MESSAGE"; var proj = new XamarinAndroidApplicationProject { - PackageName = "com.xamarin.fastdeploy_typemap", + PackageName = $"com.xamarin.fastdeploy_typemap_{strategy.ToLowerInvariant ()}", }; proj.SetDefaultTargetDevice (); - proj.SetProperty ("_AndroidFastDevStrategy", "FastDeploy"); + proj.SetProperty ("_AndroidFastDevStrategy", strategy); // Fast deployment only syncs managed assemblies, so anything that changes the set of // Java-callable types has to go through a new .apk: new Java stubs, a new .dex, a new From 1bc5afc0bf649933cea0b1ca2e0a64a6bba689fb Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 28 Jul 2026 10:56:13 -0500 Subject: [PATCH 15/15] [Tests] Make IsApkInstalled throw at the wrong verbosity `BuildOutput.IsApkInstalled` scans the build output for the `Installed Package` message that `FastDeploy` and `FastDeploy2` log with `MessageImportance.Low`. Below `LoggerVerbosity.Detailed` that message is never in the output, so the property silently returns `false` regardless of what the build did. That is a bad failure mode in both directions: `Assert.IsTrue` fails with a confusing message -- which is how `FastDeployUpdatesTypeMapAfterAssemblyEdit` broke on CI -- and `Assert.IsFalse` passes without proving anything at all. Document the requirement and throw an `InvalidOperationException` naming the current verbosity when it is not met. All existing callers already set `LoggerVerbosity.Detailed`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39550ed5-9b8f-46dc-ad55-2c43f2ea3789 --- .../Common/BuildOutput.cs | 20 +++++++++++++++++++ .../Tests/FastDevTest.cs | 4 +--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/BuildOutput.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/BuildOutput.cs index eaa02cc385f..668ecef3299 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/BuildOutput.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/BuildOutput.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Microsoft.Build.Framework; using Xamarin.Tools.Zip; namespace Xamarin.ProjectTools @@ -170,8 +171,27 @@ public TimeSpan GetTargetOrTaskTime (string targetOrTask) return TimeSpan.Zero; } + /// + /// Gets a value indicating whether the .apk was installed on the device by the last build. + /// + /// + /// This scans the build output for the Installed Package message written by the + /// FastDeploy and FastDeploy2 tasks. Both write it with + /// , so it is only present in the + /// build output at or higher. + /// At a lower verbosity this property would silently return false no matter what the + /// build did, which makes both Assert.IsTrue and Assert.IsFalse on it meaningless. + /// It therefore throws instead, so the test fails with an actionable message rather than a + /// misleading pass or an opaque assertion failure. + /// + /// + /// is running at a verbosity below + /// . + /// public bool IsApkInstalled { get { + if (Builder.Verbosity < LoggerVerbosity.Detailed) + throw new InvalidOperationException ($"`{nameof (IsApkInstalled)}` requires `{nameof (Builder)}.{nameof (Builder.Verbosity)}` to be `{nameof (LoggerVerbosity.Detailed)}` or higher, but it is `{Builder.Verbosity}`. The `Installed Package` message it looks for is logged with `MessageImportance.Low`."); foreach (var line in Builder.LastBuildOutput) { if (line.Contains ("Installed Package") || line.Contains (" pm install ")) return true; diff --git a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs index 81d0a5f6f35..249b9d2bed0 100644 --- a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs @@ -65,9 +65,7 @@ public void FastDeployUpdatesTypeMapAfterAssemblyEdit (string strategy) }; using var builder = CreateApkBuilder (); - // `IsApkInstalled` looks for the "Installed Package" message that `FastDeploy` logs via - // `LogDebugMessageWithTiming()`, which is `MessageImportance.Low` and therefore invisible - // at the default `Normal` verbosity. + // `IsApkInstalled` requires detailed verbosity, see its documentation. builder.Verbosity = LoggerVerbosity.Detailed; // 1. Initial build and deployment, with only MainActivity.