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 a718d313950..6d60a0f5a99 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -98,9 +98,8 @@ 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 if (peersForName.Count > 1) { - peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); + peersForName.Sort (CompareAliasesForRuntimeResolution); } EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); @@ -137,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 c6f60a7d592..bb28663cc61 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Security.Cryptography; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -107,12 +109,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, + // ManagedPEBuilder otherwise uses a time-based id, changing bytes on every regeneration. + 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 (); + if (segment.Count == 0) { + continue; + } + Debug.Assert (segment.Array is not null); + hash.AppendData (segment.Array, segment.Offset, segment.Count); + } + return BlobContentId.FromHash (hash.GetHashAndReset ()); + } + /// /// Adds (or retrieves from cache) an assembly reference. /// 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/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 7eff64e8b8f..f239f509f54 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -274,6 +274,10 @@ static void ForceUnconditionalIfPresent (Dictionary<(string ManagedName, string } } + // 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"; + void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results) { foreach (var typeHandle in index.Reader.TypeDefinitions) { @@ -286,6 +290,10 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + if (IsUnsupportedByTrimmableTypeMap (fullName, index.AssemblyName)) { + continue; + } + // Temporarily allow [JniAddNativeMethodRegistrationAttribute] while we investigate // which scenarios fail later in the trimmable typemap pipeline. // if (index.MayUseJniAddNativeMethodRegistrationAttribute && @@ -2528,6 +2536,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, 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/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/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, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index f527953d850..59fa2caa2d2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -113,6 +113,23 @@ public void Build_ThreeWayAlias_CreatesCorrectIndexedEntries () Assert.Equal (3, model.AliasHolders [0].AliasKeys.Count); } + [Fact] + public void Build_AliasGroup_MonoAndroidPeerSortsFirst () + { + // 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"), + }; + + var model = BuildModel (peers, "MonoAndroid"); + + 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 () {