diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index b12893314a5..1e0909c2b53 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -238,6 +238,9 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4316](xa4316.md): Specified input file '{file}' does not exist. Ignoring. + [XA4317](xa4317.md): Input file '{file}' does not start with ``. Skipping. + [XA4318](xa4318.md): Input file '{file}' could not be read: {message}. Skipping. ++ [XA4319](xa4319.md): No NativeAOT DGML files were provided. ++ [XA4320](xa4320.md): ACW map file '{file}' was not found. ++ [XA4321](xa4321.md): NativeAOT DGML file '{file}' was not found. ## XA5xxx: GCC and toolchain diff --git a/Documentation/docs-mobile/messages/xa4319.md b/Documentation/docs-mobile/messages/xa4319.md new file mode 100644 index 00000000000..636c8b9dcd8 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4319.md @@ -0,0 +1,26 @@ +--- +title: .NET for Android error XA4319 +description: XA4319 error code +ms.date: 05/27/2026 +f1_keywords: + - "XA4319" +--- + +# .NET for Android error XA4319 + +## Example messages + +``` +error XA4319: No NativeAOT DGML files were provided. +``` + +## Issue + +The .NET for Android build could not find any NativeAOT DGML files to use when +generating trimmable type map ProGuard configuration. + +## Solution + +This error is expected only for internal build state inconsistencies. Ensure the +project is building with the intended Runtime Identifier settings and that the +NativeAOT compile produced its DGML scan file. diff --git a/Documentation/docs-mobile/messages/xa4320.md b/Documentation/docs-mobile/messages/xa4320.md new file mode 100644 index 00000000000..cb70fac90bf --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4320.md @@ -0,0 +1,25 @@ +--- +title: .NET for Android error XA4320 +description: XA4320 error code +ms.date: 05/27/2026 +f1_keywords: + - "XA4320" +--- + +# .NET for Android error XA4320 + +## Example messages + +``` +error XA4320: ACW map file '{file}' was not found. +``` + +## Issue + +The .NET for Android build could not find the Android Callable Wrapper (ACW) map +file needed to generate trimmable type map ProGuard configuration. + +## Solution + +This error is expected only for internal build state inconsistencies. Rebuild +the project from a clean state so the ACW map is regenerated. diff --git a/Documentation/docs-mobile/messages/xa4321.md b/Documentation/docs-mobile/messages/xa4321.md new file mode 100644 index 00000000000..f082e1e7157 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4321.md @@ -0,0 +1,26 @@ +--- +title: .NET for Android error XA4321 +description: XA4321 error code +ms.date: 05/27/2026 +f1_keywords: + - "XA4321" +--- + +# .NET for Android error XA4321 + +## Example messages + +``` +error XA4321: NativeAOT DGML file '{file}' was not found. +``` + +## Issue + +The .NET for Android build could not find a NativeAOT DGML scan file needed to +generate trimmable type map ProGuard configuration. + +## Solution + +This error is expected only for internal build state inconsistencies. Rebuild +the project from a clean state and verify the NativeAOT compile produced the +DGML scan file for the selected Runtime Identifier. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 10c030c7f69..08b7b6c66bc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -121,9 +121,9 @@ void MarkFrameworkArrayEntryPeers (IEnumerable peers) if (frameworkAssemblyNames.Contains (index.AssemblyName)) { continue; } - foreach (var frameworkAssemblyName in frameworkAssemblyNames) { - if (index.ReferencedTypeNamesByAssembly.TryGetValue (frameworkAssemblyName, out var typeNames)) { - referencedFrameworkTypes.UnionWith (typeNames); + foreach (var referencedTypeNames in index.ReferencedTypeNamesByAssembly) { + if (frameworkAssemblyNames.Contains (referencedTypeNames.Key)) { + referencedFrameworkTypes.UnionWith (referencedTypeNames.Value); } } } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index 3ab8c4f6494..6ef82b37f11 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -2,8 +2,17 @@ Adds generated typemap assemblies to ILC inputs. --> + + <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider + r8 + d8 + True + True + true + <_UseTrimmableNativeAotProguardConfiguration Condition=" '$(_UseTrimmableNativeAotProguardConfiguration)' == '' ">true + <_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_GenerateTrimmableTypeMapProguardConfiguration - <_TrimmableTypeMapUnmanagedEntryPointAssemblies Include="@(_TrimmableTypeMapIlcAssemblies)" /> - <_TrimmableTypeMapUnmanagedEntryPointAssemblies Remove="$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll" /> - <_TrimmableTypeMapUnmanagedEntryPointAssemblies Remove="$(_TypeMapOutputDirectory)_Java.Interop.TypeMap.dll;$(_TypeMapOutputDirectory)_Mono.Android.TypeMap.dll" /> - <_TrimmableTypeMapUnmanagedEntryPointAssemblies Remove="@(_TrimmableTypeMapFrameworkIlcAssemblies)" /> + <_TrimmableTypeMapUnmanagedEntryPointAssemblyNames Include="@(_TrimmableTypeMapIlcAssemblies->'%(Filename)')" /> + <_TrimmableTypeMapUnmanagedEntryPointAssemblyNames Remove="$(_TypeMapAssemblyName)" /> + <_TrimmableTypeMapUnmanagedEntryPointAssemblyNames Remove="_Java.Interop.TypeMap;_Mono.Android.TypeMap" /> + <_TrimmableTypeMapUnmanagedEntryPointAssemblyNames Remove="@(_TrimmableTypeMapFrameworkIlcAssemblyNames)" /> - + + + + + + + <_TrimmableNativeAotRuntimeIdentifiers Remove="@(_TrimmableNativeAotRuntimeIdentifiers)" /> + <_TrimmableNativeAotDgmlFiles Remove="@(_TrimmableNativeAotDgmlFiles)" /> + <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifier)" Condition=" '$(RuntimeIdentifier)' != '' " /> + <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifiers)" Condition=" '$(RuntimeIdentifier)' == '' and '$(RuntimeIdentifiers)' != '' " /> + + <_TrimmableNativeAotDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' == '' " /> + <_TrimmableNativeAotDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" Condition=" '$(RuntimeIdentifier)' != '' " /> + <_TrimmableNativeAotDgmlFiles Include="$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).scan.dgml.xml" Condition=" '$(RuntimeIdentifier)' == '' and '@(_TrimmableNativeAotRuntimeIdentifiers)' != '' " /> + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index aab2ec4f46c..632ea9d1a32 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -131,6 +131,13 @@ + + diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 8a12db1c544..6d32f29e314 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1759,6 +1759,33 @@ public static string XA4318 { return ResourceManager.GetString("XA4318", resourceCulture); } } + + /// + /// Looks up a localized string similar to No NativeAOT DGML files were provided.. + /// + public static string XA4319 { + get { + return ResourceManager.GetString("XA4319", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ACW map file '{0}' was not found.. + /// + public static string XA4320 { + get { + return ResourceManager.GetString("XA4320", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to NativeAOT DGML file '{0}' was not found.. + /// + public static string XA4321 { + get { + return ResourceManager.GetString("XA4321", resourceCulture); + } + } /// /// Looks up a localized string similar to Missing Android NDK toolchains directory '{0}'. Please install the Android NDK.. diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index 5b8186898e1..2fd54e571e5 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -837,6 +837,20 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins {0} - The path to the file. {1} - The exception message. + + + No NativeAOT DGML files were provided. + The following are literal names and should not be translated: NativeAOT, DGML + + + ACW map file '{0}' was not found. + The following are literal names and should not be translated: ACW +{0} - The path to the ACW map file. + + + NativeAOT DGML file '{0}' was not found. + The following are literal names and should not be translated: NativeAOT, DGML +{0} - The path to the NativeAOT DGML file. Missing Android NDK toolchains directory '{0}'. Please install the Android NDK. diff --git a/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg b/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg new file mode 100644 index 00000000000..6d54d809ae2 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg @@ -0,0 +1,15 @@ +# Xamarin.Android NativeAOT trimmable typemap configuration. + +-dontobfuscate + +-keep class net.dot.jni.** { *; (...); } +-keep class net.dot.android.crypto.** { *; (...); } + +-keepclassmembers class * extends android.view.View { + *** set*(...); +} + +-keepclassmembers class * extends android.view.View { + (android.content.Context,android.util.AttributeSet); + (android.content.Context,android.util.AttributeSet,int); +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs new file mode 100644 index 00000000000..44ae925d4f4 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs @@ -0,0 +1,114 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Android.Build.Tasks; + +namespace Xamarin.Android.Tasks; + +public class GenerateNativeAotProguardConfiguration : AndroidTask +{ + const string TypeMetadataPrefix = "Type metadata: ["; + + public override string TaskPrefix => "GNAPC"; + + [Required] + public ITaskItem [] NativeAotDgmlFiles { get; set; } = []; + + [Required] + public string AcwMapFile { get; set; } = ""; + + [Required] + public string OutputFile { get; set; } = ""; + + public override bool RunTask () + { + var dir = Path.GetDirectoryName (OutputFile); + if (!dir.IsNullOrEmpty () && !Directory.Exists (dir)) { + Directory.CreateDirectory (dir); + } + + if (NativeAotDgmlFiles.Length == 0) { + Log.LogCodedError ("XA4319", Properties.Resources.XA4319); + return !Log.HasLoggedErrors; + } + if (!File.Exists (AcwMapFile)) { + Log.LogCodedError ("XA4320", Properties.Resources.XA4320, AcwMapFile); + return !Log.HasLoggedErrors; + } + foreach (var dgmlFile in NativeAotDgmlFiles) { + if (!File.Exists (dgmlFile.ItemSpec)) { + Log.LogCodedError ("XA4321", Properties.Resources.XA4321, dgmlFile.ItemSpec); + return !Log.HasLoggedErrors; + } + } + + var retainedTypeKeys = LoadRetainedTypeKeysFromDgml (); + var javaTypes = LoadJavaTypesFromAcwMap (retainedTypeKeys); + + using var writer = new StringWriter (); + writer.WriteLine ("# ACWs retained by NativeAOT ILC"); + foreach (var javaTypeName in javaTypes) { + writer.WriteLine ($"-keep class {javaTypeName} {{ *; }}"); + } + Files.CopyIfStringChanged (writer.ToString (), OutputFile); + + Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT trimmable typemap ProGuard rules from {1} DGML file(s).", javaTypes.Count, NativeAotDgmlFiles.Length); + return !Log.HasLoggedErrors; + } + + List LoadJavaTypesFromAcwMap (HashSet retainedTypeKeys) + { + var javaTypes = new List (retainedTypeKeys.Count); + var seenJavaTypes = new HashSet (StringComparer.Ordinal); + foreach (var line in File.ReadLines (AcwMapFile)) { + var separator = line.IndexOf (";", StringComparison.Ordinal); + if (separator <= 0 || separator == line.Length - 1) { + continue; + } + var managedTypeName = line.Substring (0, separator); + var javaTypeName = line.Substring (separator + 1); + if (retainedTypeKeys.Contains (managedTypeName) && seenJavaTypes.Add (javaTypeName)) { + javaTypes.Add (javaTypeName); + } + } + return javaTypes; + } + + HashSet LoadRetainedTypeKeysFromDgml () + { + var typeKeys = new HashSet (StringComparer.Ordinal); + foreach (var dgmlFile in NativeAotDgmlFiles) { + using var reader = XmlReader.Create (dgmlFile.ItemSpec, new XmlReaderSettings { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + }); + + while (reader.Read ()) { + if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "Node") { + continue; + } + + var label = reader.GetAttribute ("Label"); + if (label.IsNullOrEmpty () || !label.StartsWith (TypeMetadataPrefix, StringComparison.Ordinal)) { + continue; + } + + var assemblyStart = TypeMetadataPrefix.Length; + var assemblyEnd = label.IndexOf (']', assemblyStart); + if (assemblyEnd < 0 || assemblyEnd == label.Length - 1) { + continue; + } + + var assemblyName = label.Substring (assemblyStart, assemblyEnd - assemblyStart); + var managedTypeName = label.Substring (assemblyEnd + 1); + typeKeys.Add ($"{managedTypeName}, {assemblyName}"); + } + } + + return typeKeys; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index fbf66e7bf48..89370b1d12c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -35,6 +35,7 @@ public class R8 : D8 public string? ProguardCommonXamarinConfiguration { get; set; } public string? ProguardMappingFileOutput { get; set; } public string []? ProguardConfigurationFiles { get; set; } + public bool UseTrimmableNativeAotProguardConfiguration { get; set; } protected override string MainClass => "com.android.tools.r8.R8"; @@ -95,7 +96,9 @@ protected override string CreateResponseFile () } if (EnableShrinking) { - if (!AcwMapFile.IsNullOrEmpty ()) { + if (UseTrimmableNativeAotProguardConfiguration && !ProguardGeneratedApplicationConfiguration.IsNullOrEmpty ()) { + File.WriteAllText (ProguardGeneratedApplicationConfiguration, "# ACW keep rules are generated from NativeAOT ILC metadata.\n"); + } else if (!AcwMapFile.IsNullOrEmpty ()) { var acwMap = MonoAndroidHelper.LoadMapFile (BuildEngine4, Path.GetFullPath (AcwMapFile), StringComparer.OrdinalIgnoreCase); var javaTypes = new List (acwMap.Values.Count); foreach (var v in acwMap.Values) { @@ -110,7 +113,13 @@ protected override string CreateResponseFile () } if (!ProguardCommonXamarinConfiguration.IsNullOrWhiteSpace ()) { using (var xamcfg = File.CreateText (ProguardCommonXamarinConfiguration)) { - GetType ().Assembly.GetManifestResourceStream ("proguard_xamarin.cfg").CopyTo (xamcfg.BaseStream); + if (UseTrimmableNativeAotProguardConfiguration) { + using var stream = GetEmbeddedResourceStream ("proguard_trimmable_nativeaot.cfg"); + stream.CopyTo (xamcfg.BaseStream); + } else { + using var stream = GetEmbeddedResourceStream ("proguard_xamarin.cfg"); + stream.CopyTo (xamcfg.BaseStream); + } if (IgnoreWarnings) { xamcfg.WriteLine ("-ignorewarnings"); } @@ -159,6 +168,15 @@ protected override string CreateResponseFile () return responseFile; } + Stream GetEmbeddedResourceStream (string resourceName) + { + var stream = GetType ().Assembly.GetManifestResourceStream (resourceName); + if (stream == null) { + throw new InvalidOperationException ($"Missing embedded resource '{resourceName}'."); + } + return stream; + } + // Note: We do not want to call the base.LogEventsFromTextOutput as it will incorrectly identify // Warnings and Info messages as errors. protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index b93d314e922..7a6f121832d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -239,6 +239,49 @@ public void Execute_ManifestPlaceholdersAreResolvedForRooting () Assert.IsFalse (warnings.Any (w => w.Code == "XA4250"), "Resolved placeholder-based manifest references should not log XA4250."); } + [Test] + public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata () + { + var path = Path.Combine (Root, "temp", TestName); + var dgmlFile = Path.Combine (path, "app.scan.dgml.xml"); + var acwMapFile = Path.Combine (path, "acw-map.txt"); + var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); + Directory.CreateDirectory (path); + File.WriteAllText (dgmlFile, """ + + + + + + + + + + """); + File.WriteAllText (acwMapFile, """ + UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity + Android.App.Activity, Mono.Android;android.app.Activity + Duplicate.Type, My.Assembly;my.app.Duplicate + Duplicate.Type;wrong.Duplicate + Other.Type;other.Type + """); + + var task = new GenerateNativeAotProguardConfiguration { + BuildEngine = new MockBuildEngine (TestContext.Out), + NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) }, + AcwMapFile = acwMapFile, + OutputFile = outputFile, + }; + + Assert.IsTrue (task.Execute (), "Task should succeed."); + var proguard = File.ReadAllText (outputFile); + StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); + StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); + StringAssert.Contains ("-keep class my.app.Duplicate { *; }", proguard); + StringAssert.DoesNotContain ("wrong.Duplicate", proguard); + StringAssert.DoesNotContain ("other.Type", proguard); + } + GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir, IList? messages = null, IList? warnings = null, string tfv = "v11.0") { diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index 0796e933155..40b45798c35 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -74,6 +74,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. ProguardGeneratedApplicationConfiguration="$(IntermediateOutputPath)proguard\proguard_project_primary.cfg" ProguardMappingFileOutput="$(AndroidProguardMappingFile)" ProguardConfigurationFiles="@(_ProguardConfiguration)" + UseTrimmableNativeAotProguardConfiguration="$(_UseTrimmableNativeAotProguardConfiguration)" EnableShrinking="$(_R8EnableShrinking)" EnableMultiDex="$(AndroidEnableMultiDex)" MultiDexMainDexListFile="$(_AndroidMainDexListFile)"