From 0ad72d230e536d7949a858de6969b93ff0ee854f 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 b59ad3c3a4277586445663b1b7f68b5675248391 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 993cb949a1c..636b22e25b0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -13,6 +13,7 @@ public interface ITrimmableTypeMapLogger void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName); void LogManifestReferencedTypeNotFoundWarning (string javaTypeName); void LogLibraryManifestMergeWarning (string message); + 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 f59e99e0334..fee0811ebc4 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -155,6 +155,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes // Other codes (e.g. unresolvable type properties) are not yet assigned XA codes // and are intentionally not surfaced here. }, + WarnInvalidPlaceholder = placeholders => logger.LogInvalidManifestPlaceholderWarning (placeholders), LibraryManifests = config.LibraryManifests ?? [], }; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 6c7d18b5823..b1dcd144167 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -46,6 +46,8 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName); public void LogLibraryManifestMergeWarning (string message) => log.LogCodedWarning ("XA4302", Properties.Resources.XA4302, message); + 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 a17715f8d68..fb9829ca648 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -38,6 +38,8 @@ 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 LogLibraryManifestMergeWarning (string message) => warnings?.Add (message); + public void LogInvalidManifestPlaceholderWarning (string placeholders) => + warnings?.Add ($"Invalid $(AndroidManifestPlaceholders) '{placeholders}'."); public void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, From 5ccfc1a987694706b067c3a706a37fca570619f5 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 eeb667e4131318fddfda70155a3be0f81f2d5e5e 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 c372463c6b6..52e4d6e3928 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -102,9 +102,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 0cf84e0146d..9f24e60db2f 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 b344a0817c3a4c2b22e398fc818f98cad3fa464c 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 1a4fb259ded..e092327366e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -516,7 +516,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 52e4d6e3928..b8c8d767bcd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -102,22 +102,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); @@ -154,6 +140,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 56c1b2fa3f6..ef0224aaecb 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 9f24e60db2f..7bca6e7f0a3 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 20fed8bf203d6b0b5901a3f4166059ea744d9752 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 ());