From 76ab5c8b7ae78a7311be123315e1cd1333afef13 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 3 Jul 2026 12:23:42 +0200 Subject: [PATCH 1/7] [TrimmableTypeMap] Exclude Java.Interop.ManagedPeer from the type map Java.Interop.ManagedPeer is a reflection-based helper marked [RequiresUnreferencedCode] ("Uses reflection to find constructors and invoke them."). The trimmable type map generator emitted a proxy for it (_TypeMap.Proxies.Java_Interop_ManagedPeer_Proxy) whose constructor references ManagedPeer's constructors, producing two IL2026 trim warnings, aggregated by ILC into: IL2104: Assembly '_Java.Interop.TypeMap' produced trim warnings Because the default NativeAOT type map is now trimmable, this surfaced as a real MSBuild warning and broke NativeAOT tests that assert no warnings (e.g. BuildWithJavaToolOptions). ManagedPeer is not supported by the trimmable type map: on the trimmable path native registration goes through IAndroidCallableWrapper.RegisterNatives and ManagedPeerNativeRegistration is disabled, so ManagedPeer is never activated via the type map. Exclude it in the scanner so no proxy is emitted. Verified on an arm64 emulator: the HelloWorld NativeAOT (trimmable) sample now builds with 0 warnings and still launches to MainActivity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 8d424c0a641cc724f8102e279aa7a0b220b95e26) --- .../Scanner/JavaPeerScanner.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 7eff64e8b8f..751d3a21c91 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -274,6 +274,13 @@ static void ForceUnconditionalIfPresent (Dictionary<(string ManagedName, string } } + // Managed types that must never be emitted into the trimmable type map, keyed by + // (managed full name, assembly simple name). These are reflection-based ([RequiresUnreferencedCode]) + // helpers that the trimmable runtime never activates via the type map; emitting proxies for them + // only produces IL2026 trim warnings. + static bool IsUnsupportedByTrimmableTypeMap (string managedFullName, string assemblyName) => + managedFullName == "Java.Interop.ManagedPeer" && assemblyName == "Java.Interop"; + void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results) { foreach (var typeHandle in index.Reader.TypeDefinitions) { @@ -286,6 +293,16 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + // Java.Interop.ManagedPeer is a reflection-based helper (marked + // [RequiresUnreferencedCode]) that is not supported by the trimmable type map: on the + // trimmable path native registration goes through IAndroidCallableWrapper.RegisterNatives + // and ManagedPeerNativeRegistration is disabled, so ManagedPeer is never activated via the + // type map. Emitting a proxy for it only produces IL2026 trim warnings (its constructors + // use reflection), so exclude it here. + if (IsUnsupportedByTrimmableTypeMap (fullName, index.AssemblyName)) { + continue; + } + // Temporarily allow [JniAddNativeMethodRegistrationAttribute] while we investigate // which scenarios fail later in the trimmable typemap pipeline. // if (index.MayUseJniAddNativeMethodRegistrationAttribute && From 12b3abdac27b6a486ff4d018c327dff6de4eb6fb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 4 Jul 2026 01:24:44 +0200 Subject: [PATCH 2/7] [TrimmableTypeMap] Emit element for the [Layout] attribute The trimmable manifest generator already had ComponentElementBuilder support for a child element, but the scanner never populated ComponentInfo.LayoutProperties, so [Layout(...)] on an activity produced no element (LayoutAttributeElement failed on NativeAOT). Parse the [Layout] attribute's named properties in AssemblyIndex.ParseAttributes (collected separately to tolerate attribute ordering, like [IntentFilter] and [MetaData]) and flow them through TypeAttributeInfo.LayoutProperties into ComponentInfo.LayoutProperties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 968ac2c1187b4a9e0986de93ecb4d15e22733cbe) --- .../Scanner/AssemblyIndex.cs | 24 +++++++++++++++++++ .../Scanner/JavaPeerScanner.cs | 1 + 2 files changed, 25 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 7544d61b588..3f55ef799d5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -164,6 +164,7 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen // with the wrong AttributeName. List? intentFilters = null; List? metaData = null; + Dictionary? layoutProperties = null; foreach (var caHandle in typeDef.GetCustomAttributes ()) { var ca = Reader.GetCustomAttribute (caHandle); @@ -214,6 +215,8 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen metaData ??= new List (); var (mdName, mdProps) = ParseNameAndProperties (ca); metaData.Add (CreateMetaDataInfo (mdName, mdProps)); + } else if (attrName == "LayoutAttribute") { + layoutProperties = ParseLayoutAttribute (ca); } else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) { // Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName]) var name = TryGetNameProperty (ca); @@ -232,6 +235,9 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen if (metaData is not null) { attrInfo.MetaData.AddRange (metaData); } + if (layoutProperties is not null) { + attrInfo.LayoutProperties = layoutProperties; + } } return (registerInfo, attrInfo); @@ -425,6 +431,18 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) return null; } + Dictionary ParseLayoutAttribute (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + var properties = new Dictionary (StringComparer.Ordinal); + foreach (var named in value.NamedArguments) { + if (named.Name is not null) { + properties [named.Name] = named.Value; + } + } + return properties; + } + IntentFilterInfo ParseIntentFilterAttribute (CustomAttribute ca) { var value = DecodeAttribute (ca); @@ -712,6 +730,12 @@ class TypeAttributeInfo (string attributeName) /// Metadata entries declared on this type via [MetaData] attributes. /// public List MetaData { get; } = []; + + /// + /// Named property values from a [Layout] attribute on this type, or null if none. + /// Maps to the <layout> child element of the component in the manifest. + /// + public Dictionary? LayoutProperties { get; set; } } sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute") diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 751d3a21c91..35736008d50 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2545,6 +2545,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List Date: Sat, 4 Jul 2026 01:24:44 +0200 Subject: [PATCH 3/7] [TrimmableTypeMap] Emit XA1010 for invalid $(AndroidManifestPlaceholders) ManifestGenerator.ApplyPlaceholders already invoked a WarnInvalidPlaceholder callback for placeholder entries without '=', but the callback was never wired up, so the trimmable path silently dropped the XA1010 warning the legacy ManifestDocument emits (ManifestPlaceHoldersXA1010 failed on NativeAOT). Add ITrimmableTypeMapLogger.LogInvalidManifestPlaceholderWarning (logging XA1010 from the MSBuild logger) and wire the ManifestGenerator instance's WarnInvalidPlaceholder to it. The rooting-only PrepareManifestForRooting pass stays silent so the warning is not emitted twice. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 518d9b4597ecf800f7583d6923822df95306e34c) --- .../ITrimmableTypeMapLogger.cs | 1 + .../TrimmableTypeMapGenerator.cs | 1 + .../Tasks/GenerateTrimmableTypeMap.cs | 2 ++ .../Generator/TrimmableTypeMapGeneratorTests.cs | 2 ++ 4 files changed, 6 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index cdc5cf040fe..6515c7fc3af 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -12,6 +12,7 @@ public interface ITrimmableTypeMapLogger void LogGeneratedJcwFilesInfo (int sourceCount); void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName); void LogManifestReferencedTypeNotFoundWarning (string javaTypeName); + void LogInvalidManifestPlaceholderWarning (string placeholders); void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 1aca9a1993a..eb48d857e49 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -149,6 +149,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes ForceExtractNativeLibs = forceDebuggable, ManifestPlaceholders = config.ManifestPlaceholders, ApplicationJavaClass = config.ApplicationJavaClass, + WarnInvalidPlaceholder = placeholders => logger.LogInvalidManifestPlaceholderWarning (placeholders), }; var (doc, providerNames) = generator.Generate (manifestTemplate, allPeers, assemblyManifestInfo); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 2ca8f412a48..661a52d6a73 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -44,6 +44,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName); + public void LogInvalidManifestPlaceholderWarning (string placeholders) => + log.LogCodedWarning ("XA1010", Properties.Resources.XA1010, placeholders); public void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index a60a6343e72..8000a61409a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -36,6 +36,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type."); + public void LogInvalidManifestPlaceholderWarning (string placeholders) => + warnings?.Add ($"Invalid $(AndroidManifestPlaceholders) '{placeholders}'."); public void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, From b99f044fb9c4c20ce0c81738785ca471068a7142 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 4 Jul 2026 08:11:16 +0200 Subject: [PATCH 4/7] [TrimmableTypeMap] Make generated typemap assemblies byte-deterministic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PEAssemblyBuilder.WritePE let ManagedPEBuilder fall back to a time-based PE content id, so every regeneration of a typemap assembly produced different bytes (different PE TimeDateStamp) even for identical input — the MVID was already deterministic, but the image was not. That churn broke incremental packaging on NativeAOT: an SDK CoreCompile rerun (e.g. touching a .csproj.user file, which the SDK treats as a compile input) rewrites the app assembly, reruns _GenerateTrimmableTypeMap, and — because the regenerated *.TypeMap.dll got a fresh timestamp — forced _BuildApkEmbed to repackage and _Sign to re-sign (CSProjUserFileChanges failed on NativeAOT). Supply a deterministic content-id provider (SHA-256 over the serialized image) so identical input yields byte-identical output; CopyIfStreamChanged then keeps the existing file/timestamp and downstream targets stay incremental. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 91204df034f77bc5692d9ebac8d8a9ae8191c000) --- .../Generator/PEAssemblyBuilder.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index c6f60a7d592..84ccd57a4ad 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Security.Cryptography; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -107,12 +108,28 @@ public void WritePE (Stream stream) new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), new MetadataRootBuilder (Metadata), ILBuilder, - mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null); + mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null, + // Derive the PE content id (and thus the header TimeDateStamp/debug id) from a hash of the + // image so the bytes are fully deterministic. Without this, ManagedPEBuilder falls back to a + // time-based id, so every regeneration produces different bytes even for identical input; that + // churns the generated typemap assemblies and breaks incremental packaging (a no-op rebuild + // re-touches the .dll, forcing repackage + re-sign). The MVID is already deterministic. + deterministicIdProvider: DeterministicContentId); var peBlob = new BlobBuilder (); peBuilder.Serialize (peBlob); peBlob.WriteContentTo (stream); } + static BlobContentId DeterministicContentId (IEnumerable content) + { + using var hash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256); + foreach (var blob in content) { + var segment = blob.GetBytes (); + hash.AppendData (segment.Array!, segment.Offset, segment.Count); + } + return BlobContentId.FromHash (hash.GetHashAndReset ()); + } + /// /// Adds (or retrieves from cache) an assembly reference. /// From 51cb3bc61d59162923ad8b56262c3c214e26f71f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 5 Jul 2026 22:20:26 +0200 Subject: [PATCH 5/7] [TrimmableTypeMap] Resolve java/lang/Object to Java.Lang.Object For a JNI name mapped by multiple managed types (an alias group), the trimmable type map's GetTypeForSimpleReference returns the first (index [0]) alias. Order the aliases to match the native runtime's java->managed selection (clr_typemap_java_to_managed / monovm_typemap_java_to_managed, built by NativeTypeMappingData): the Mono.Android module is processed first and the first managed type to claim a Java name wins. So java/lang/Object must resolve to Java.Lang.Object (Mono.Android), not Java.Interop.JavaObject (Java.Interop). Order alias peers Mono.Android-assembly-first, with an ordinal managed-name tiebreak for deterministic proxy naming. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 17 ++++++++++++-- .../Generator/TypeMapModelBuilderTests.cs | 23 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index a718d313950..4f6fa70c87b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -98,9 +98,22 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri string jniName = kvp.Key; var peersForName = kvp.Value; - // Sort aliases by managed type name for deterministic proxy naming + // Order aliases to match the java→managed selection the native runtime performs (see + // clr_typemap_java_to_managed / monovm_typemap_java_to_managed and NativeTypeMappingData): + // the runtime builds its java→managed map by processing the Mono.Android module first and + // keeping the first managed type that claims a Java name (first-writer-wins). GetTypeForSimpleReference + // returns the first (index [0]) alias, so put the Mono.Android peer first — e.g. java/lang/Object + // must resolve to Java.Lang.Object (Mono.Android), not Java.Interop.JavaObject. Remaining peers are + // ordered by ordinal managed type name for deterministic proxy naming. if (peersForName.Count > 1) { - peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); + peersForName.Sort ((a, b) => { + bool aMonoAndroid = string.Equals (a.AssemblyName, "Mono.Android", StringComparison.Ordinal); + bool bMonoAndroid = string.Equals (b.AssemblyName, "Mono.Android", StringComparison.Ordinal); + if (aMonoAndroid != bMonoAndroid) { + return aMonoAndroid ? -1 : 1; + } + return StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName); + }); } EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index f527953d850..9398f15ff2d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -113,6 +113,29 @@ public void Build_ThreeWayAlias_CreatesCorrectIndexedEntries () Assert.Equal (3, model.AliasHolders [0].AliasKeys.Count); } + [Fact] + public void Build_AliasGroup_MonoAndroidPeerSortsFirst () + { + // Mirror the native runtime's java→managed selection (clr_typemap_java_to_managed / + // monovm_typemap_java_to_managed): NativeTypeMappingData builds that map by processing the + // Mono.Android module first and keeping the first managed type to claim a Java name, so + // java/lang/Object resolves to Java.Lang.Object (Mono.Android), not Java.Interop.JavaObject + // (Java.Interop). GetTypeForSimpleReference returns alias index [0], so the Mono.Android peer + // must sort first. Declare them in the "wrong" order to prove the sort reorders them. + var peers = new List { + MakeMcwPeer ("java/lang/Object", "Java.Interop.JavaObject", "Java.Interop") with { IsFromJniTypeSignature = true }, + MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"), + }; + + var model = BuildModel (peers, "MonoAndroid"); + + // The Mono.Android peer (Java.Lang.Object) must be alias index [0]. + Assert.Equal ("java/lang/Object[0]", model.Entries [0].MapKey); + Assert.Contains ("Java.Lang.Object", model.Entries [0].ProxyTypeReference); + Assert.Equal ("java/lang/Object[1]", model.Entries [1].MapKey); + Assert.Contains ("Java.Interop.JavaObject", model.Entries [1].ProxyTypeReference); + } + [Fact] public void Build_AliasWithMixedActivation_PrimaryNoActivation_AliasHasActivation () { From 2764f8a54db89e3223dbc1ed41a234c03d8e97d7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Jul 2026 12:30:11 +0200 Subject: [PATCH 6/7] Address trimmable typemap review feedback Avoid null-forgiving PE blob access, keep layout emission activity-only, and trim explanatory comments while preserving legacy placeholder behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ComponentElementBuilder.cs | 3 +- .../Generator/ManifestGenerator.cs | 2 +- .../Generator/ModelBuilder.cs | 29 +++++++-------- .../Generator/PEAssemblyBuilder.cs | 16 +++++--- .../Scanner/JavaPeerScanner.cs | 11 +----- .../Generator/ManifestGeneratorTests.cs | 37 +++++++++++++++++++ .../Generator/TypeMapModelBuilderTests.cs | 8 +--- 7 files changed, 65 insertions(+), 41 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs index 82b5651a25c..f738548ca8e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -44,8 +44,7 @@ static class ComponentElementBuilder element.Add (CreateIntentFilterElement (intentFilter)); } - // Add element from a [Layout] attribute, if present - if (component.LayoutProperties is not null) { + if (component.Kind == ComponentKind.Activity && component.LayoutProperties is not null) { var layout = CreateLayoutElement (component.LayoutProperties); if (layout is not null) { element.Add (layout); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index a323bd885b4..c03e4e876c3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -513,7 +513,7 @@ internal static void ApplyPlaceholders (XDocument doc, string? placeholders, str if (!placeholders.IsNullOrEmpty ()) { foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) { var eqIndex = entry.IndexOf ('='); - if (eqIndex > 0) { + if (eqIndex >= 0) { var key = entry.Substring (0, eqIndex).Trim (); var value = entry.Substring (eqIndex + 1).Trim (); replacements ["${" + key + "}"] = value; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 4f6fa70c87b..6d60a0f5a99 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -98,22 +98,8 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri string jniName = kvp.Key; var peersForName = kvp.Value; - // Order aliases to match the java→managed selection the native runtime performs (see - // clr_typemap_java_to_managed / monovm_typemap_java_to_managed and NativeTypeMappingData): - // the runtime builds its java→managed map by processing the Mono.Android module first and - // keeping the first managed type that claims a Java name (first-writer-wins). GetTypeForSimpleReference - // returns the first (index [0]) alias, so put the Mono.Android peer first — e.g. java/lang/Object - // must resolve to Java.Lang.Object (Mono.Android), not Java.Interop.JavaObject. Remaining peers are - // ordered by ordinal managed type name for deterministic proxy naming. if (peersForName.Count > 1) { - peersForName.Sort ((a, b) => { - bool aMonoAndroid = string.Equals (a.AssemblyName, "Mono.Android", StringComparison.Ordinal); - bool bMonoAndroid = string.Equals (b.AssemblyName, "Mono.Android", StringComparison.Ordinal); - if (aMonoAndroid != bMonoAndroid) { - return aMonoAndroid ? -1 : 1; - } - return StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName); - }); + peersForName.Sort (CompareAliasesForRuntimeResolution); } EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); @@ -150,6 +136,19 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri return model; } + static int CompareAliasesForRuntimeResolution (JavaPeerInfo a, JavaPeerInfo b) + { + // Keep alias [0] aligned with the native java→managed map, which processes Mono.Android first. + bool aMonoAndroid = string.Equals (a.AssemblyName, "Mono.Android", StringComparison.Ordinal); + bool bMonoAndroid = string.Equals (b.AssemblyName, "Mono.Android", StringComparison.Ordinal); + if (aMonoAndroid != bMonoAndroid) { + return aMonoAndroid ? -1 : 1; + } + + int result = StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName); + return result != 0 ? result : StringComparer.Ordinal.Compare (a.AssemblyName, b.AssemblyName); + } + static void EmitPeers (TypeMapAssemblyData model, string jniName, List peersForName, string assemblyName, HashSet usedProxyNames) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 84ccd57a4ad..1910183d692 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -109,11 +110,7 @@ public void WritePE (Stream stream) new MetadataRootBuilder (Metadata), ILBuilder, mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null, - // Derive the PE content id (and thus the header TimeDateStamp/debug id) from a hash of the - // image so the bytes are fully deterministic. Without this, ManagedPEBuilder falls back to a - // time-based id, so every regeneration produces different bytes even for identical input; that - // churns the generated typemap assemblies and breaks incremental packaging (a no-op rebuild - // re-touches the .dll, forcing repackage + re-sign). The MVID is already deterministic. + // ManagedPEBuilder otherwise uses a time-based id, changing bytes on every regeneration. deterministicIdProvider: DeterministicContentId); var peBlob = new BlobBuilder (); peBuilder.Serialize (peBlob); @@ -125,7 +122,14 @@ static BlobContentId DeterministicContentId (IEnumerable content) using var hash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256); foreach (var blob in content) { var segment = blob.GetBytes (); - hash.AppendData (segment.Array!, segment.Offset, segment.Count); + if (segment.Count == 0) { + continue; + } + Debug.Assert (segment.Array is not null); + if (segment.Array is null) { + throw new InvalidOperationException ("PE content blob segment has no backing array."); + } + hash.AppendData (segment.Array, segment.Offset, segment.Count); } return BlobContentId.FromHash (hash.GetHashAndReset ()); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 35736008d50..f239f509f54 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -274,10 +274,7 @@ static void ForceUnconditionalIfPresent (Dictionary<(string ManagedName, string } } - // Managed types that must never be emitted into the trimmable type map, keyed by - // (managed full name, assembly simple name). These are reflection-based ([RequiresUnreferencedCode]) - // helpers that the trimmable runtime never activates via the type map; emitting proxies for them - // only produces IL2026 trim warnings. + // ManagedPeer depends on reflection-based registration; the trimmable path uses IAndroidCallableWrapper. static bool IsUnsupportedByTrimmableTypeMap (string managedFullName, string assemblyName) => managedFullName == "Java.Interop.ManagedPeer" && assemblyName == "Java.Interop"; @@ -293,12 +290,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); - // Java.Interop.ManagedPeer is a reflection-based helper (marked - // [RequiresUnreferencedCode]) that is not supported by the trimmable type map: on the - // trimmable path native registration goes through IAndroidCallableWrapper.RegisterNatives - // and ManagedPeerNativeRegistration is disabled, so ManagedPeer is never activated via the - // type map. Emitting a proxy for it only produces IL2026 trim warnings (its constructors - // use reflection), so exclude it here. if (IsUnsupportedByTrimmableTypeMap (fullName, index.AssemblyName)) { continue; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index d1442c2c1b7..661b0fd52d4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -105,6 +105,25 @@ public void Placeholders_AllValid_DoesNotWarn () Assert.Equal ("val1", (string?) doc.Root?.Element ("application")?.Attribute (AndroidNs + "label")); } + [Fact] + public void Placeholders_EmptyKey_ReplacesEmptyTokenWithoutWarning () + { + var gen = CreateDefaultGenerator (); + var warnings = new List (); + gen.WarnInvalidPlaceholder = warnings.Add; + gen.ManifestPlaceholders = "=val1"; + var template = ParseTemplate (""" + + + + + + """); + var doc = GenerateAndLoad (gen, template: template); + Assert.Empty (warnings); + Assert.Equal ("val1", (string?) doc.Root?.Element ("application")?.Attribute (AndroidNs + "label")); + } + [Fact] public void Package_PlaceholderToken_ReplacedWithResolvedPackageName () { @@ -385,6 +404,24 @@ public void Activity_LayoutAttributeElement () Assert.Equal ("400dp", (string?)layout?.Attribute (AndroidNs + "minHeight")); } + [Fact] + public void Service_LayoutAttributeElement_Ignored () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyService", new ComponentInfo { + Kind = ComponentKind.Service, + LayoutProperties = new Dictionary { + ["DefaultWidth"] = "500dp", + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var service = doc.Root?.Element ("application")?.Element ("service"); + + Assert.NotNull (service); + Assert.Null (service?.Element ("layout")); + } + [Fact] public void Activity_AllExtendedProperties () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 9398f15ff2d..59fa2caa2d2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -116,12 +116,7 @@ public void Build_ThreeWayAlias_CreatesCorrectIndexedEntries () [Fact] public void Build_AliasGroup_MonoAndroidPeerSortsFirst () { - // Mirror the native runtime's java→managed selection (clr_typemap_java_to_managed / - // monovm_typemap_java_to_managed): NativeTypeMappingData builds that map by processing the - // Mono.Android module first and keeping the first managed type to claim a Java name, so - // java/lang/Object resolves to Java.Lang.Object (Mono.Android), not Java.Interop.JavaObject - // (Java.Interop). GetTypeForSimpleReference returns alias index [0], so the Mono.Android peer - // must sort first. Declare them in the "wrong" order to prove the sort reorders them. + // Java.Interop first proves Mono.Android still becomes alias [0], matching runtime lookup. var peers = new List { MakeMcwPeer ("java/lang/Object", "Java.Interop.JavaObject", "Java.Interop") with { IsFromJniTypeSignature = true }, MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"), @@ -129,7 +124,6 @@ public void Build_AliasGroup_MonoAndroidPeerSortsFirst () var model = BuildModel (peers, "MonoAndroid"); - // The Mono.Android peer (Java.Lang.Object) must be alias index [0]. Assert.Equal ("java/lang/Object[0]", model.Entries [0].MapKey); Assert.Contains ("Java.Lang.Object", model.Entries [0].ProxyTypeReference); Assert.Equal ("java/lang/Object[1]", model.Entries [1].MapKey); From 21d1f4629a87786e40147218cae85047bb9af49c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 7 Jul 2026 12:41:13 +0200 Subject: [PATCH 7/7] Simplify deterministic PE blob assertion Keep Jon's requested Debug.Assert without the redundant runtime null guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/PEAssemblyBuilder.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 1910183d692..bb28663cc61 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -126,9 +126,6 @@ static BlobContentId DeterministicContentId (IEnumerable content) continue; } Debug.Assert (segment.Array is not null); - if (segment.Array is null) { - throw new InvalidOperationException ("PE content blob segment has no backing array."); - } hash.AppendData (segment.Array, segment.Offset, segment.Count); } return BlobContentId.FromHash (hash.GetHashAndReset ());