From 3da11a9fe60d457ccbcd8b3c043f4fe80f8ac8b3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 12:23:26 +0100 Subject: [PATCH 01/11] PR 4: Build pipeline changes for trimmable typemap - Update TrimmableTypeMapGenerator.Execute() with manifest generation, assembly manifest scanning, acw-map writing, and new optional parameters - Add ManifestConfig record to TrimmableTypeMapTypes.cs - Update TrimmableTypeMapResult with AdditionalProviderSources - Update GenerateTrimmableTypeMap MSBuild task with manifest/config properties - Create GenerateEmptyTypemapStub task for LLVM IR native typemap stubs - Create ApplicationRegistration.Trimmable.java (empty registerApplications) - Create Trimmable.CoreCLR.xml preserve list for JNIEnvInit.Initialize - Rewrite Trimmable.targets with full build pipeline (separate generation and _GenerateJavaStubs targets, native stub generation, manifest handling) - Rewrite Trimmable.CoreCLR.targets with ILLink integration and per-ABI assembly store support - Add TrimmableTypeMap=false feature switch to MonoVM and NativeAOT targets - Handle trimmed JNIEnvInit tokens in GenerateNativeApplicationConfigSources Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 74 ++++++- .../TrimmableTypeMapTypes.cs | 21 +- .../PreserveLists/Trimmable.CoreCLR.xml | 9 + .../ApplicationRegistration.Trimmable.java | 13 ++ .../Microsoft.Android.Sdk.MonoVM.targets | 4 + .../Microsoft.Android.Sdk.NativeAOT.targets | 4 + ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 70 +++++- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 203 +++++++++++++----- .../Tasks/GenerateEmptyTypemapStub.cs | 97 +++++++++ .../GenerateNativeApplicationConfigSources.cs | 16 +- .../Tasks/GenerateTrimmableTypeMap.cs | 51 ++++- 11 files changed, 486 insertions(+), 76 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml create mode 100644 src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 1a2b0691057..c602e12611b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -15,16 +15,24 @@ public TrimmableTypeMapGenerator (Action log) this.log = log ?? throw new ArgumentNullException (nameof (log)); } + /// + /// Runs the full generation pipeline: scan assemblies, generate typemap + /// assemblies, generate JCW Java sources, optionally generate manifest and acw-map. + /// public TrimmableTypeMapResult Execute ( IReadOnlyList<(string Name, PEReader Reader)> assemblies, Version systemRuntimeVersion, - HashSet frameworkAssemblyNames) + HashSet frameworkAssemblyNames, + ManifestConfig? manifestConfig = null, + string? manifestTemplatePath = null, + string? mergedManifestOutputPath = null, + string? acwMapOutputPath = null) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); _ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames)); - var allPeers = ScanAssemblies (assemblies); + var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies); if (allPeers.Count == 0) { log ("No Java peer types found, skipping typemap generation."); return new TrimmableTypeMapResult ([], [], allPeers); @@ -36,15 +44,71 @@ public TrimmableTypeMapResult Execute ( || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total)."); var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); - return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers); + + string[]? additionalProviderSources = null; + + // Generate merged AndroidManifest.xml if requested + if (!mergedManifestOutputPath.IsNullOrEmpty () && manifestConfig is not null) { + var providerSources = GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplatePath, mergedManifestOutputPath); + if (providerSources.Count > 0) { + additionalProviderSources = providerSources.ToArray (); + } + } + + // Write merged acw-map.txt if requested + if (!acwMapOutputPath.IsNullOrEmpty ()) { + Directory.CreateDirectory (Path.GetDirectoryName (acwMapOutputPath)); + using (var writer = new StreamWriter (acwMapOutputPath)) { + AcwMapWriter.Write (writer, allPeers); + } + log ($"Wrote merged acw-map.txt with {allPeers.Count} types to {acwMapOutputPath}."); + } + + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, additionalProviderSources); + } + + IList GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo, + ManifestConfig config, string? manifestTemplatePath, string mergedManifestOutputPath) + { + string minSdk = "21"; + if (!config.SupportedOSPlatformVersion.IsNullOrEmpty () && Version.TryParse (config.SupportedOSPlatformVersion, out var sopv)) { + minSdk = sopv.Major.ToString (System.Globalization.CultureInfo.InvariantCulture); + } + + string targetSdk = config.AndroidApiLevel ?? "36"; + if (Version.TryParse (targetSdk, out var apiVersion)) { + targetSdk = apiVersion.Major.ToString (System.Globalization.CultureInfo.InvariantCulture); + } + + bool forceDebuggable = !config.CheckedBuild.IsNullOrEmpty (); + + var generator = new ManifestGenerator { + PackageName = config.PackageName, + ApplicationLabel = config.ApplicationLabel ?? config.PackageName, + VersionCode = config.VersionCode ?? "", + VersionName = config.VersionName ?? "", + MinSdkVersion = minSdk, + TargetSdkVersion = targetSdk, + AndroidRuntime = config.AndroidRuntime ?? "coreclr", + Debug = config.Debug, + NeedsInternet = config.NeedsInternet, + EmbedAssemblies = config.EmbedAssemblies, + ForceDebuggable = forceDebuggable, + ForceExtractNativeLibs = forceDebuggable, + ManifestPlaceholders = config.ManifestPlaceholders, + ApplicationJavaClass = config.ApplicationJavaClass, + }; + + return generator.Generate (manifestTemplatePath, allPeers, assemblyManifestInfo, mergedManifestOutputPath); } - List ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies) + (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies) { using var scanner = new JavaPeerScanner (); var peers = scanner.Scan (assemblies); + var manifestInfo = scanner.ScanAssemblyManifestInfo (); log ($"Scanned {assemblies.Count} assemblies, found {peers.Count} Java peer types."); - return peers; + return (peers, manifestInfo); } List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index c10a2482bd2..e4172195744 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -6,8 +6,27 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; public record TrimmableTypeMapResult ( IReadOnlyList GeneratedAssemblies, IReadOnlyList GeneratedJavaSources, - IReadOnlyList AllPeers); + IReadOnlyList AllPeers, + string[]? AdditionalProviderSources = null); public record GeneratedAssembly (string Name, MemoryStream Content); public record GeneratedJavaSource (string RelativePath, string Content); + +/// +/// Configuration values for manifest generation. Passed from MSBuild properties. +/// +public record ManifestConfig ( + string PackageName, + string? ApplicationLabel = null, + string? VersionCode = null, + string? VersionName = null, + string? AndroidApiLevel = null, + string? SupportedOSPlatformVersion = null, + string? AndroidRuntime = null, + bool Debug = false, + bool NeedsInternet = false, + bool EmbedAssemblies = false, + string? ManifestPlaceholders = null, + string? CheckedBuild = null, + string? ApplicationJavaClass = null); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml new file mode 100644 index 00000000000..8c62546764a --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java new file mode 100644 index 00000000000..d03cc3b3c7e --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java @@ -0,0 +1,13 @@ +package net.dot.android; + +public class ApplicationRegistration { + + public static android.content.Context Context; + + // In the trimmable typemap path, Application/Instrumentation types are activated + // via Runtime.registerNatives() + UCO wrappers, not Runtime.register(), so this + // method is intentionally empty. + public static void registerApplications () + { + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets index 8b49df77580..9a5e479837c 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets @@ -17,6 +17,10 @@ This file contains the MonoVM-specific MSBuild logic for .NET for Android. Value="false" Trim="true" /> + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index b4ecd0f5fe6..7119b475f1d 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -44,6 +44,10 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. Value="false" Trim="true" /> + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index a06b771e122..8897c697ac1 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -1,15 +1,67 @@ - + - - + + - <_ResolvedAssemblies Include="@(_GeneratedTypeMapAssemblies)" /> + + %(Filename)%(Extension) + + + + + + + + <_ExtraTrimmerArgs>--typemap-entry-assembly $(_TypeMapAssemblyName) $(_ExtraTrimmerArgs) + + + + + + + <_CurrentAbi>%(_BuildTargetAbis.Identity) + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'arm64-v8a' ">android-arm64 + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'armeabi-v7a' ">android-arm + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'x86_64' ">android-x64 + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'x86' ">android-x86 + + + + <_CurrentLinkedTypeMapDlls Include="$(IntermediateOutputPath)$(_CurrentRid)/linked/_*.TypeMap.dll;$(IntermediateOutputPath)$(_CurrentRid)/linked/_Microsoft.Android.TypeMap*.dll" /> + + + <_BuildApkResolvedUserAssemblies Include="@(_CurrentLinkedTypeMapDlls)"> + $(_CurrentAbi) + $(_CurrentRid) + $(_CurrentAbi)/%(_CurrentLinkedTypeMapDlls.Filename)%(_CurrentLinkedTypeMapDlls.Extension) + $(_CurrentAbi)/ + + + + + <_CurrentTypeMapDlls Include="$(_TypeMapOutputDirectory)*.dll" /> + + + <_BuildApkResolvedUserAssemblies Include="@(_CurrentTypeMapDlls)"> + $(_CurrentAbi) + $(_CurrentRid) + $(_CurrentAbi)/%(_CurrentTypeMapDlls.Filename)%(_CurrentTypeMapDlls.Extension) + $(_CurrentAbi)/ + + + + <_CurrentLinkedTypeMapDlls Remove="@(_CurrentLinkedTypeMapDlls)" /> + <_CurrentTypeMapDlls Remove="@(_CurrentTypeMapDlls)" /> 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 b056316d7db..317b2b66d7f 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 @@ -1,9 +1,8 @@ - + + @@ -12,94 +11,180 @@ <_TypeMapAssemblyName>_Microsoft.Android.TypeMaps - <_TypeMapOutputDirectory>$(IntermediateOutputPath)typemap\ - <_TypeMapJavaOutputDirectory>$(IntermediateOutputPath)typemap\java - <_PerAssemblyAcwMapDirectory>$(IntermediateOutputPath)acw-maps\ + <_TypeMapBaseOutputDir>$(IntermediateOutputPath) + <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) + <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ + <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java - + + + - + Condition=" '$(_AndroidRuntime)' != 'CoreCLR' And '$(_AndroidRuntime)' != 'NativeAOT' " + BeforeTargets="Build"> + - - + + + + + <_TypeMapInputAssemblies Include="@(ReferencePath)" /> + <_TypeMapInputAssemblies Include="@(ResolvedAssemblies)" /> + <_TypeMapInputAssemblies Include="@(ResolvedFrameworkAssemblies)" /> + <_TypeMapInputAssemblies Include="$(IntermediateOutputPath)$(TargetFileName)" + Condition="Exists('$(IntermediateOutputPath)$(TargetFileName)')" /> + + AcwMapDirectory="$(_TypeMapBaseOutputDir)acw-maps/" + TargetFrameworkVersion="$(TargetFrameworkVersion)" + ManifestTemplate="$(_AndroidManifestAbs)" + MergedAndroidManifestOutput="$(_TypeMapBaseOutputDir)AndroidManifest.xml" + PackageName="$(_AndroidPackage)" + ApplicationLabel="$(_ApplicationLabel)" + VersionCode="$(_AndroidVersionCode)" + VersionName="$(_AndroidVersionName)" + AndroidApiLevel="$(_AndroidApiLevel)" + SupportedOSPlatformVersion="$(SupportedOSPlatformVersion)" + AndroidRuntime="$(_AndroidRuntime)" + Debug="$(AndroidIncludeDebugSymbols)" + NeedsInternet="$(AndroidNeedsInternetPermission)" + EmbedAssemblies="$(EmbedAssembliesIntoApk)" + ManifestPlaceholders="$(AndroidManifestPlaceholders)" + CheckedBuild="$(_AndroidCheckedBuild)" + ApplicationJavaClass="$(AndroidApplicationJavaClass)" + AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt"> - + - + + - - - - + + - <_PerAssemblyAcwMapFiles Remove="@(_PerAssemblyAcwMapFiles)" /> - <_PerAssemblyAcwMapFiles Include="$(_PerAssemblyAcwMapDirectory)*.txt" /> + <_TypeMapJavaFiles Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> - + - - - - - - - - + + + + + + + <_TypeMapFirstAbi Condition=" '$(AndroidSupportedAbis)' != '' ">$([System.String]::Copy('$(AndroidSupportedAbis)').Split(';')[0]) + <_TypeMapFirstAbi Condition=" '$(_TypeMapFirstAbi)' == '' ">arm64-v8a + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'arm64-v8a' ">android-arm64 + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'armeabi-v7a' ">android-arm + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86_64' ">android-x64 + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86' ">android-x86 + + + + + <_ResolvedAssemblies Include="$(_TypeMapOutputDirectory)*.dll"> + $(_TypeMapFirstAbi) + $(_TypeMapFirstRid) + $(_TypeMapFirstAbi)/%(Filename)%(Extension) + $(_TypeMapFirstAbi)/ + + + + + + + + + + + + + + + + + + + + + + + + + + <_TypeMapStubAbis Include="@(_BuildTargetAbis)" /> + + + + + - + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs new file mode 100644 index 00000000000..47d16267cda --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs @@ -0,0 +1,97 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks; + +/// +/// Generates empty native typemap LLVM IR stub files (typemap.{abi}.ll) for the trimmable typemap path. +/// These are compiled by the native toolchain to provide the type_map and related symbols that libmonodroid.so expects. +/// +public class GenerateEmptyTypemapStub : AndroidTask +{ + public override string TaskPrefix => "GETS"; + + [Required] + public string OutputDirectory { get; set; } = ""; + + [Required] + public ITaskItem [] Abis { get; set; } = []; + + public bool Debug { get; set; } + + [Output] + public ITaskItem []? Sources { get; set; } + + public override bool RunTask () + { + Directory.CreateDirectory (OutputDirectory); + var sources = new List (); + + foreach (var abi in Abis) { + string abiName = abi.ItemSpec; + string stubPath = Path.Combine (OutputDirectory, $"typemap.{abiName}.ll"); + Files.CopyIfStringChanged (GenerateStubLlvmIr (abiName), stubPath); + var item = new TaskItem (stubPath); + item.SetMetadata ("abi", abiName); + sources.Add (item); + } + + Sources = sources.ToArray (); + return !Log.HasLoggedErrors; + } + + string GenerateStubLlvmIr (string abi) + { + var (triple, datalayout) = abi switch { + "arm64-v8a" => ("aarch64-unknown-linux-android21", "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128"), + "x86_64" => ("x86_64-unknown-linux-android21", "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"), + "armeabi-v7a" => ("armv7-unknown-linux-androideabi21", "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64"), + "x86" => ("i686-unknown-linux-android21", "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-f64:32:64-f80:32-n8:16:32-S128"), + _ => throw new NotSupportedException ($"Unsupported ABI: {abi}"), + }; + + string header = $$""" +; ModuleID = 'typemap.{{abi}}.ll' +source_filename = "typemap.{{abi}}.ll" +target datalayout = "{{datalayout}}" +target triple = "{{triple}}" + +"""; + + if (Debug) { + return header + """ +%struct.TypeMap = type { i32, i32, ptr, ptr } +%struct.TypeMapManagedTypeInfo = type { i64, i32, i32 } +%struct.TypeMapAssembly = type { i64 } + +@type_map = dso_local constant %struct.TypeMap zeroinitializer, align 8 +@typemap_use_hashes = dso_local constant i8 1, align 1 +@type_map_managed_type_info = dso_local constant [0 x %struct.TypeMapManagedTypeInfo] zeroinitializer, align 8 +@type_map_unique_assemblies = dso_local constant [0 x %struct.TypeMapAssembly] zeroinitializer, align 8 +@type_map_assembly_names = dso_local constant [1 x i8] zeroinitializer, align 1 +@type_map_managed_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 +@type_map_java_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 +"""; + } + + return header + """ +@managed_to_java_map_module_count = dso_local constant i32 0, align 4 +@managed_to_java_map = dso_local constant [0 x i8] zeroinitializer, align 8 +@java_to_managed_map = dso_local constant [0 x i8] zeroinitializer, align 8 +@java_to_managed_hashes = dso_local constant [0 x i64] zeroinitializer, align 8 +@modules_map_data = dso_local constant [0 x i8] zeroinitializer, align 8 +@modules_duplicates_data = dso_local constant [0 x i8] zeroinitializer, align 8 +@java_type_count = dso_local constant i32 0, align 4 +@java_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 +@java_type_names_size = dso_local constant i64 0, align 8 +@managed_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 +@managed_assembly_names = dso_local constant [1 x i8] zeroinitializer, align 1 +"""; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index a17945d8cee..7b01f6f08e5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -334,7 +334,21 @@ void GetRequiredTokens (string assemblyFilePath, out int android_runtime_jnienv_ } if (android_runtime_jnienv_class_token == -1 || jnienv_initialize_method_token == -1 || jnienv_registerjninatives_method_token == -1) { - throw new InvalidOperationException ($"Unable to find the required Android.Runtime.JNIEnvInit method tokens for {assemblyFilePath}"); + if (!TargetsCLR) { + throw new InvalidOperationException ($"Required JNIEnvInit tokens not found in '{assemblyFilePath}' (class={android_runtime_jnienv_class_token}, init={jnienv_initialize_method_token}, register={jnienv_registerjninatives_method_token})."); + } + + // In the trimmable typemap path (CoreCLR), some JNIEnvInit methods may be trimmed. + // Use token 0 for missing tokens — native code will skip them. + if (jnienv_registerjninatives_method_token == -1) { + jnienv_registerjninatives_method_token = 0; + } + if (jnienv_initialize_method_token == -1) { + jnienv_initialize_method_token = 0; + } + if (android_runtime_jnienv_class_token == -1) { + android_runtime_jnienv_class_token = 0; + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 4d621afd2a9..c81068ec9f5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -27,12 +27,35 @@ public class GenerateTrimmableTypeMap : AndroidTask public string AcwMapDirectory { get; set; } = ""; [Required] public string TargetFrameworkVersion { get; set; } = ""; + +public string? AcwMapOutputFile { get; set; } + +public string? ManifestTemplate { get; set; } + +public string? MergedAndroidManifestOutput { get; set; } + +public string? PackageName { get; set; } +public string? ApplicationLabel { get; set; } +public string? VersionCode { get; set; } +public string? VersionName { get; set; } +public string? AndroidApiLevel { get; set; } +public string? SupportedOSPlatformVersion { get; set; } +public string? AndroidRuntime { get; set; } +public bool Debug { get; set; } +public bool NeedsInternet { get; set; } +public bool EmbedAssemblies { get; set; } +public string? ManifestPlaceholders { get; set; } +public string? CheckedBuild { get; set; } +public string? ApplicationJavaClass { get; set; } + [Output] public ITaskItem [] GeneratedAssemblies { get; set; } = []; [Output] public ITaskItem [] GeneratedJavaFiles { get; set; } = []; [Output] public ITaskItem []? PerAssemblyAcwMapFiles { get; set; } +[Output] +public string[]? AdditionalProviderSources { get; set; } public override bool RunTask () { @@ -56,12 +79,38 @@ public override bool RunTask () assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader)); } +ManifestConfig? manifestConfig = null; +if (!string.IsNullOrEmpty (MergedAndroidManifestOutput) && !string.IsNullOrEmpty (PackageName)) { +manifestConfig = new ManifestConfig ( +PackageName: PackageName, +ApplicationLabel: ApplicationLabel, +VersionCode: VersionCode, +VersionName: VersionName, +AndroidApiLevel: AndroidApiLevel, +SupportedOSPlatformVersion: SupportedOSPlatformVersion, +AndroidRuntime: AndroidRuntime, +Debug: Debug, +NeedsInternet: NeedsInternet, +EmbedAssemblies: EmbedAssemblies, +ManifestPlaceholders: ManifestPlaceholders, +CheckedBuild: CheckedBuild, +ApplicationJavaClass: ApplicationJavaClass); +} + var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); -result = generator.Execute (assemblies, systemRuntimeVersion, frameworkAssemblyNames); +result = generator.Execute ( +assemblies, +systemRuntimeVersion, +frameworkAssemblyNames, +manifestConfig, +ManifestTemplate, +MergedAndroidManifestOutput, +AcwMapOutputFile); GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers); +AdditionalProviderSources = result.AdditionalProviderSources; } finally { if (result is not null) { foreach (var assembly in result.GeneratedAssemblies) { From 9d7ac8353f7df767137a5cea996dcafd79a86db6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 13:21:08 +0100 Subject: [PATCH 02/11] Address review: remove #nullable enable, use IsNullOrEmpty extension - Remove redundant #nullable enable from 6 Generator files and task file - Convert string.IsNullOrEmpty to IsNullOrEmpty extension in task Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/AcwMapWriter.cs | 1 - .../Generator/AndroidEnumConverter.cs | 1 - .../Generator/AssemblyLevelElementBuilder.cs | 1 - .../Generator/ComponentElementBuilder.cs | 1 - .../Generator/ManifestConstants.cs | 1 - .../Generator/PropertyMapper.cs | 1 - .../Tasks/GenerateTrimmableTypeMap.cs | 4 +--- 7 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs index 8d4005d6390..55e3864cd7c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.IO; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs index b25a1407a4f..cddbd3eedfe 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs index 8ba00724f87..837c0cc2c0f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs index 9f6f8e7f516..2d185a783d6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Globalization; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs index 4ed62e9baa6..b2e051314a6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Xml.Linq; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs index 32b07671600..ed5e68a208e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index c81068ec9f5..1fe170294b8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.IO; @@ -80,7 +78,7 @@ public override bool RunTask () } ManifestConfig? manifestConfig = null; -if (!string.IsNullOrEmpty (MergedAndroidManifestOutput) && !string.IsNullOrEmpty (PackageName)) { +if (!MergedAndroidManifestOutput.IsNullOrEmpty () && !PackageName.IsNullOrEmpty ()) { manifestConfig = new ManifestConfig ( PackageName: PackageName, ApplicationLabel: ApplicationLabel, From 2a245bbefd3038b7093f8155942d3c17ffc22c8c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 31 Mar 2026 18:16:49 +0200 Subject: [PATCH 03/11] =?UTF-8?q?Fix=20=5FPrepareNativeAssemblySources=20O?= =?UTF-8?q?utputs=20typo=20(typemaps=20=E2=86=92=20typemap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The target declared Outputs as typemaps.{abi}.ll but GenerateEmptyTypemapStub writes typemap.{abi}.ll. The mismatch caused MSBuild to always rerun the target. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 317b2b66d7f..078ad81b511 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 @@ -164,7 +164,7 @@ + Outputs="@(_BuildTargetAbis->'$(_NativeAssemblySourceDir)typemap.%(Identity).ll')"> <_TypeMapStubAbis Include="@(_BuildTargetAbis)" /> From ed8ea811ecf82778626e8704dcf31fc8d0b4b36b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 1 Apr 2026 17:20:50 +0200 Subject: [PATCH 04/11] Fix CS1503: load XDocument from manifest path, guard GetDirectoryName null - Load manifestTemplatePath into XDocument before passing to ManifestGenerator.Generate() which expects XDocument? (not string) - Guard Path.GetDirectoryName() result with IsNullOrEmpty check before passing to Directory.CreateDirectory() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapGenerator.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index c602e12611b..d24dff0466b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Reflection.PortableExecutable; +using System.Xml.Linq; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -57,7 +58,10 @@ public TrimmableTypeMapResult Execute ( // Write merged acw-map.txt if requested if (!acwMapOutputPath.IsNullOrEmpty ()) { - Directory.CreateDirectory (Path.GetDirectoryName (acwMapOutputPath)); + var acwDirectory = Path.GetDirectoryName (acwMapOutputPath); + if (!acwDirectory.IsNullOrEmpty ()) { + Directory.CreateDirectory (acwDirectory); + } using (var writer = new StreamWriter (acwMapOutputPath)) { AcwMapWriter.Write (writer, allPeers); } @@ -99,7 +103,12 @@ IList GenerateManifest (List allPeers, AssemblyManifestInf ApplicationJavaClass = config.ApplicationJavaClass, }; - return generator.Generate (manifestTemplatePath, allPeers, assemblyManifestInfo, mergedManifestOutputPath); + XDocument? manifestTemplateDoc = null; + if (!manifestTemplatePath.IsNullOrEmpty () && File.Exists (manifestTemplatePath)) { + manifestTemplateDoc = XDocument.Load (manifestTemplatePath); + } + + return generator.Generate (manifestTemplateDoc, allPeers, assemblyManifestInfo, mergedManifestOutputPath); } (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies) From 9b8cbf6271a869729350ce82fb90ebc2bd3d70f7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 1 Apr 2026 17:33:14 +0200 Subject: [PATCH 05/11] Move all IO from TrimmableTypeMapGenerator to MSBuild task The previous PR (#11034) established the pattern that TTMG has no IO operations. The build pipeline PR regressed this by adding acw-map writing, manifest template loading, and manifest file saving into the generator. Move all IO back to GenerateTrimmableTypeMap (MSBuild task): - Load manifest template XDocument from file path - Write merged manifest to disk via XDocument.Save() - Write merged acw-map.txt via Files.CopyIfStreamChanged() Refactor ManifestGenerator.Generate to return (XDocument, IList) instead of writing to disk. Add GeneratedManifest record to carry the in-memory result. Update ManifestGeneratorTests to work entirely in-memory (no temp dirs, no IDisposable). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ManifestGenerator.cs | 15 ++---- .../TrimmableTypeMapGenerator.cs | 42 ++++----------- .../TrimmableTypeMapTypes.cs | 9 +++- .../Tasks/GenerateTrimmableTypeMap.cs | 43 ++++++++++++--- .../Generator/ManifestGeneratorTests.cs | 54 +++++-------------- 5 files changed, 70 insertions(+), 93 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 1bea7928e30..501130f03a3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Xml.Linq; @@ -38,11 +37,10 @@ class ManifestGenerator /// Generates the merged manifest from an optional pre-loaded template and writes it to . /// Returns the list of additional content provider names (for ApplicationRegistration.java). /// - public IList Generate ( + public (XDocument Document, IList ProviderNames) Generate ( XDocument? manifestTemplate, IReadOnlyList allPeers, - AssemblyManifestInfo assemblyInfo, - string outputPath) + AssemblyManifestInfo assemblyInfo) { var doc = manifestTemplate ?? CreateDefaultManifest (); var manifest = doc.Root; @@ -122,14 +120,7 @@ public IList Generate ( ApplyPlaceholders (doc, placeholders); } - // Write output - var outputDir = Path.GetDirectoryName (outputPath); - if (outputDir is not null) { - Directory.CreateDirectory (outputDir); - } - doc.Save (outputPath); - - return providerNames; + return (doc, providerNames); } XDocument CreateDefaultManifest () diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index d24dff0466b..26c9727adef 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -18,16 +18,15 @@ public TrimmableTypeMapGenerator (Action log) /// /// Runs the full generation pipeline: scan assemblies, generate typemap - /// assemblies, generate JCW Java sources, optionally generate manifest and acw-map. + /// assemblies, generate JCW Java sources, and optionally generate a merged manifest. + /// No file IO is performed — all results are returned in memory. /// public TrimmableTypeMapResult Execute ( IReadOnlyList<(string Name, PEReader Reader)> assemblies, Version systemRuntimeVersion, HashSet frameworkAssemblyNames, ManifestConfig? manifestConfig = null, - string? manifestTemplatePath = null, - string? mergedManifestOutputPath = null, - string? acwMapOutputPath = null) + XDocument? manifestTemplate = null) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -46,33 +45,18 @@ public TrimmableTypeMapResult Execute ( log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total)."); var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); - string[]? additionalProviderSources = null; + GeneratedManifest? manifest = null; // Generate merged AndroidManifest.xml if requested - if (!mergedManifestOutputPath.IsNullOrEmpty () && manifestConfig is not null) { - var providerSources = GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplatePath, mergedManifestOutputPath); - if (providerSources.Count > 0) { - additionalProviderSources = providerSources.ToArray (); - } + if (manifestConfig is not null) { + manifest = GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate); } - // Write merged acw-map.txt if requested - if (!acwMapOutputPath.IsNullOrEmpty ()) { - var acwDirectory = Path.GetDirectoryName (acwMapOutputPath); - if (!acwDirectory.IsNullOrEmpty ()) { - Directory.CreateDirectory (acwDirectory); - } - using (var writer = new StreamWriter (acwMapOutputPath)) { - AcwMapWriter.Write (writer, allPeers); - } - log ($"Wrote merged acw-map.txt with {allPeers.Count} types to {acwMapOutputPath}."); - } - - return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, additionalProviderSources); + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest); } - IList GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo, - ManifestConfig config, string? manifestTemplatePath, string mergedManifestOutputPath) + GeneratedManifest GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo, + ManifestConfig config, XDocument? manifestTemplate) { string minSdk = "21"; if (!config.SupportedOSPlatformVersion.IsNullOrEmpty () && Version.TryParse (config.SupportedOSPlatformVersion, out var sopv)) { @@ -103,12 +87,8 @@ IList GenerateManifest (List allPeers, AssemblyManifestInf ApplicationJavaClass = config.ApplicationJavaClass, }; - XDocument? manifestTemplateDoc = null; - if (!manifestTemplatePath.IsNullOrEmpty () && File.Exists (manifestTemplatePath)) { - manifestTemplateDoc = XDocument.Load (manifestTemplatePath); - } - - return generator.Generate (manifestTemplateDoc, allPeers, assemblyManifestInfo, mergedManifestOutputPath); + var (doc, providerNames) = generator.Generate (manifestTemplate, allPeers, assemblyManifestInfo); + return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index e4172195744..4c51f375048 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Xml.Linq; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -7,12 +8,18 @@ public record TrimmableTypeMapResult ( IReadOnlyList GeneratedAssemblies, IReadOnlyList GeneratedJavaSources, IReadOnlyList AllPeers, - string[]? AdditionalProviderSources = null); + GeneratedManifest? Manifest = null); public record GeneratedAssembly (string Name, MemoryStream Content); public record GeneratedJavaSource (string RelativePath, string Content); +/// +/// The in-memory result of manifest generation: the merged document and +/// any additional content provider class names for ApplicationRegistration.java. +/// +public record GeneratedManifest (XDocument Document, string[] AdditionalProviderSources); + /// /// Configuration values for manifest generation. Passed from MSBuild properties. /// diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 1fe170294b8..7e198698d8b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -96,19 +96,46 @@ public override bool RunTask () } var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); + +XDocument? manifestTemplate = null; +if (!ManifestTemplate.IsNullOrEmpty () && File.Exists (ManifestTemplate)) { + manifestTemplate = System.Xml.Linq.XDocument.Load (ManifestTemplate); +} + result = generator.Execute ( -assemblies, -systemRuntimeVersion, -frameworkAssemblyNames, -manifestConfig, -ManifestTemplate, -MergedAndroidManifestOutput, -AcwMapOutputFile); + assemblies, + systemRuntimeVersion, + frameworkAssemblyNames, + manifestConfig, + manifestTemplate); GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers); -AdditionalProviderSources = result.AdditionalProviderSources; + +// Write manifest to disk if generated +if (result.Manifest is not null && !MergedAndroidManifestOutput.IsNullOrEmpty ()) { + var manifestDir = Path.GetDirectoryName (MergedAndroidManifestOutput); + if (!manifestDir.IsNullOrEmpty ()) { + Directory.CreateDirectory (manifestDir); + } + result.Manifest.Document.Save (MergedAndroidManifestOutput); + AdditionalProviderSources = result.Manifest.AdditionalProviderSources; +} + +// Write merged acw-map.txt if requested +if (!AcwMapOutputFile.IsNullOrEmpty ()) { + var acwDirectory = Path.GetDirectoryName (AcwMapOutputFile); + if (!acwDirectory.IsNullOrEmpty ()) { + Directory.CreateDirectory (acwDirectory); + } + using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { + AcwMapWriter.Write (sw, result.AllPeers); + sw.Flush (); + Files.CopyIfStreamChanged (sw.BaseStream, AcwMapOutputFile); + } + Log.LogDebugMessage ($"Wrote merged acw-map.txt with {result.AllPeers.Count} types to {AcwMapOutputFile}."); +} } finally { if (result is not null) { foreach (var assembly in result.GeneratedAssemblies) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 2653286fd20..907ee048b1a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Xml.Linq; using Microsoft.Android.Sdk.TrimmableTypeMap; @@ -9,26 +7,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; -public class ManifestGeneratorTests : IDisposable +public class ManifestGeneratorTests { static readonly XNamespace AndroidNs = "http://schemas.android.com/apk/res/android"; static readonly XName AttName = AndroidNs + "name"; - string tempDir; - - public ManifestGeneratorTests () - { - tempDir = Path.Combine (Path.GetTempPath (), "ManifestGeneratorTests_" + Guid.NewGuid ().ToString ("N")); - Directory.CreateDirectory (tempDir); - } - - public void Dispose () - { - if (Directory.Exists (tempDir)) { - Directory.Delete (tempDir, recursive: true); - } - } - ManifestGenerator CreateDefaultGenerator () => new ManifestGenerator { PackageName = "com.example.app", ApplicationLabel = "My App", @@ -39,14 +22,7 @@ public void Dispose () AndroidRuntime = "coreclr", }; - string OutputPath => Path.Combine (tempDir, "AndroidManifest.xml"); - - string WriteTemplate (string xml) - { - var path = Path.Combine (tempDir, "template.xml"); - File.WriteAllText (path, xml); - return path; - } + static XDocument ParseTemplate (string xml) => XDocument.Parse (xml); static JavaPeerInfo CreatePeer ( string javaName, @@ -70,16 +46,12 @@ XDocument GenerateAndLoad ( ManifestGenerator gen, IReadOnlyList? peers = null, AssemblyManifestInfo? assemblyInfo = null, - string? templatePath = null) + XDocument? template = null) { peers ??= []; assemblyInfo ??= new AssemblyManifestInfo (); - XDocument? template = null; - if (!string.IsNullOrEmpty (templatePath) && File.Exists (templatePath)) { - template = XDocument.Load (templatePath); - } - gen.Generate (template, peers, assemblyInfo, OutputPath); - return XDocument.Load (OutputPath); + var (doc, _) = gen.Generate (template, peers, assemblyInfo); + return doc; } [Fact] @@ -305,7 +277,7 @@ public void RuntimeProvider_Added () public void TemplateManifest_Preserved () { var gen = CreateDefaultGenerator (); - var template = WriteTemplate ( + var template = ParseTemplate ( """ @@ -314,7 +286,7 @@ public void TemplateManifest_Preserved () """); - var doc = GenerateAndLoad (gen, templatePath: template); + var doc = GenerateAndLoad (gen, template: template); var app = doc.Root?.Element ("application"); Assert.Equal ("false", (string?)app?.Attribute (AndroidNs + "allowBackup")); @@ -384,7 +356,7 @@ public void ManifestPlaceholders_Replaced () var gen = CreateDefaultGenerator (); gen.ManifestPlaceholders = "myAuthority=com.example.auth;myKey=12345"; - var template = WriteTemplate ( + var template = ParseTemplate ( """ @@ -395,7 +367,7 @@ public void ManifestPlaceholders_Replaced () """); - var doc = GenerateAndLoad (gen, templatePath: template); + var doc = GenerateAndLoad (gen, template: template); var provider = doc.Root?.Element ("application")?.Elements ("provider") .FirstOrDefault (p => (string?)p.Attribute (AndroidNs + "name") == "com.example.MyProvider"); Assert.Equal ("com.example.auth", (string?)provider?.Attribute (AndroidNs + "authorities")); @@ -434,7 +406,7 @@ public void AbstractTypes_Skipped () public void ExistingType_NotDuplicated () { var gen = CreateDefaultGenerator (); - var template = WriteTemplate ( + var template = ParseTemplate ( """ @@ -449,7 +421,7 @@ public void ExistingType_NotDuplicated () Properties = new Dictionary { ["Label"] = "New Label" }, }); - var doc = GenerateAndLoad (gen, [peer], templatePath: template); + var doc = GenerateAndLoad (gen, [peer], template: template); var activities = doc.Root?.Element ("application")?.Elements ("activity") .Where (a => (string?)a.Attribute (AttName) == "com.example.app.ExistingActivity") .ToList (); @@ -556,7 +528,7 @@ public void AssemblyLevel_Application () public void AssemblyLevel_Deduplication () { var gen = CreateDefaultGenerator (); - var template = WriteTemplate ( + var template = ParseTemplate ( """ @@ -573,7 +545,7 @@ public void AssemblyLevel_Deduplication () info.UsesLibraries.Add (new UsesLibraryInfo { Name = "org.apache.http.legacy" }); info.MetaData.Add (new MetaDataInfo { Name = "existing.key", Value = "new_value" }); - var doc = GenerateAndLoad (gen, assemblyInfo: info, templatePath: template); + var doc = GenerateAndLoad (gen, assemblyInfo: info, template: template); var cameraPerms = doc.Root?.Elements ("uses-permission") .Where (p => (string?)p.Attribute (AttName) == "android.permission.CAMERA") From c4a36c37188099b93063cfbb43d463258736e9e8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 1 Apr 2026 20:09:19 +0200 Subject: [PATCH 06/11] Generate ApplicationRegistration.java with registerNatives for App/Instrumentation types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Application and Instrumentation types skip the JCW static initializer block (CannotRegisterInStaticCtor) because libmonodroid.so isn't loaded when these classes are first instantiated by the Android framework. The legacy path deferred their registration to the generated ApplicationRegistration.registerApplications() method. The trimmable path incorrectly used an empty static file, meaning these types would never get their native methods registered → UnsatisfiedLinkError. Fix: dynamically generate ApplicationRegistration.java in the GenerateTrimmableTypeMap MSBuild task with mono.android.Runtime.registerNatives(MyApp.class); for each Application/Instrumentation type. This triggers the trimmable TrimmableTypeMap.OnRegisterNatives handler which dispatches to the UCO-generated IAndroidCallableWrapper.RegisterNatives. Call graph: MonoPackageManager.LoadApplication() → Runtime.initInternal() → ApplicationRegistration.registerApplications() → Runtime.registerNatives(MyApp.class) [generated] → OnRegisterNatives (JNI native) [TrimmableTypeMap.cs] → IAndroidCallableWrapper.RegisterNatives() [UCO IL] → Application.onCreate() → n_onCreate() [now registered] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGenerator.cs | 3 +- .../TrimmableTypeMapGenerator.cs | 11 ++++++- .../TrimmableTypeMapTypes.cs | 12 ++++++- .../ApplicationRegistration.Trimmable.java | 13 -------- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 13 ++------ .../Tasks/GenerateTrimmableTypeMap.cs | 32 +++++++++++++++++++ 6 files changed, 58 insertions(+), 26 deletions(-) delete mode 100644 src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 96bd0f729ab..7e41fc570d7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -124,7 +124,8 @@ static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer) { // Application and Instrumentation types cannot call registerNatives in their // static initializer — the native library isn't loaded yet at that point. - // Their registration is deferred to ApplicationRegistration.registerApplications(). + // Their registerNatives call is emitted in the generated + // ApplicationRegistration.registerApplications() method instead. if (type.CannotRegisterInStaticConstructor) { return; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 26c9727adef..09e969e8c87 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -45,6 +45,15 @@ public TrimmableTypeMapResult Execute ( log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total)."); var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); + // Collect Application/Instrumentation types that need deferred registerNatives + var appRegTypes = allPeers + .Where (p => p.CannotRegisterInStaticConstructor && !p.IsAbstract) + .Select (p => p.JavaName.Replace ('/', '.')) + .ToList (); + if (appRegTypes.Count > 0) { + log ($"Found {appRegTypes.Count} Application/Instrumentation types for deferred registration."); + } + GeneratedManifest? manifest = null; // Generate merged AndroidManifest.xml if requested @@ -52,7 +61,7 @@ public TrimmableTypeMapResult Execute ( manifest = GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate); } - return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest); + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); } GeneratedManifest GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index 4c51f375048..0ecf922fbcf 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -8,7 +8,17 @@ public record TrimmableTypeMapResult ( IReadOnlyList GeneratedAssemblies, IReadOnlyList GeneratedJavaSources, IReadOnlyList AllPeers, - GeneratedManifest? Manifest = null); + GeneratedManifest? Manifest = null, + IReadOnlyList ApplicationRegistrationTypes = null!) +{ + /// + /// Java class names (dot-separated) of Application/Instrumentation types + /// that need deferred Runtime.registerNatives() calls in + /// ApplicationRegistration.registerApplications(). + /// + public IReadOnlyList ApplicationRegistrationTypes { get; init; } = + ApplicationRegistrationTypes ?? []; +} public record GeneratedAssembly (string Name, MemoryStream Content); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java deleted file mode 100644 index d03cc3b3c7e..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.dot.android; - -public class ApplicationRegistration { - - public static android.content.Context Context; - - // In the trimmable typemap path, Application/Instrumentation types are activated - // via Runtime.registerNatives() + UCO wrappers, not Runtime.register(), so this - // method is intentionally empty. - public static void registerApplications () - { - } -} 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 078ad81b511..15e37e84f02 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 @@ -73,7 +73,8 @@ ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" - AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt"> + AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt" + ApplicationRegistrationOutputFile="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java"> @@ -84,6 +85,7 @@ + @@ -141,15 +143,6 @@ - - - - diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 7e198698d8b..96390fabaf8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -28,6 +28,8 @@ public class GenerateTrimmableTypeMap : AndroidTask public string? AcwMapOutputFile { get; set; } +public string? ApplicationRegistrationOutputFile { get; set; } + public string? ManifestTemplate { get; set; } public string? MergedAndroidManifestOutput { get; set; } @@ -136,6 +138,17 @@ public override bool RunTask () } Log.LogDebugMessage ($"Wrote merged acw-map.txt with {result.AllPeers.Count} types to {AcwMapOutputFile}."); } + +// Generate ApplicationRegistration.java with registerNatives calls for +// Application/Instrumentation types whose static initializers were skipped. +if (!ApplicationRegistrationOutputFile.IsNullOrEmpty ()) { + var appRegDir = Path.GetDirectoryName (ApplicationRegistrationOutputFile); + if (!appRegDir.IsNullOrEmpty ()) { + Directory.CreateDirectory (appRegDir); + } + Files.CopyIfStringChanged (GenerateApplicationRegistrationJava (result.ApplicationRegistrationTypes), ApplicationRegistrationOutputFile); + Log.LogDebugMessage ($"Generated ApplicationRegistration.java with {result.ApplicationRegistrationTypes.Count} deferred registration(s)."); +} } finally { if (result is not null) { foreach (var assembly in result.GeneratedAssemblies) { @@ -262,4 +275,23 @@ static Version ParseTargetFrameworkVersion (string tfv) } throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version."); } + +static string GenerateApplicationRegistrationJava (IReadOnlyList registrationTypes) +{ + var sb = new System.Text.StringBuilder (); + sb.AppendLine ("package net.dot.android;"); + sb.AppendLine (); + sb.AppendLine ("public class ApplicationRegistration {"); + sb.AppendLine (); + sb.AppendLine ("\tpublic static android.content.Context Context;"); + sb.AppendLine (); + sb.AppendLine ("\tpublic static void registerApplications ()"); + sb.AppendLine ("\t{"); + foreach (var javaClassName in registrationTypes) { + sb.AppendLine ($"\t\tmono.android.Runtime.registerNatives ({javaClassName}.class);"); + } + sb.AppendLine ("\t}"); + sb.AppendLine ("}"); + return sb.ToString (); +} } From 5f7addb317a357cbdc2f53a63eea68e6847d15b7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 2 Apr 2026 12:24:23 +0200 Subject: [PATCH 07/11] Address trimmable typemap review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/AcwMapWriter.cs | 4 +-- .../Generator/AndroidEnumConverter.cs | 1 - .../Generator/AssemblyLevelElementBuilder.cs | 1 - .../Generator/ComponentElementBuilder.cs | 5 ++- .../Generator/ManifestConstants.cs | 1 - .../Generator/ManifestGenerator.cs | 31 ++++++++++++------- .../Generator/PropertyMapper.cs | 1 - .../TrimmableTypeMapGenerator.cs | 19 +++++------- .../TrimmableTypeMapTypes.cs | 2 +- .../Microsoft.Android.Sdk.MonoVM.targets | 4 --- .../Microsoft.Android.Sdk.NativeAOT.targets | 16 ---------- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 7 +++++ ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 4 +++ ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 4 +++ ...soft.Android.Sdk.TypeMap.Trimmable.targets | 2 +- .../Tasks/GenerateTrimmableTypeMap.cs | 4 +-- .../Generator/ManifestGeneratorTests.cs | 2 +- 17 files changed, 52 insertions(+), 56 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs index 55e3864cd7c..4314a2acba8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs @@ -28,10 +28,10 @@ public static class AcwMapWriter public static void Write (TextWriter writer, IEnumerable peers) { foreach (var peer in peers.OrderBy (p => p.ManagedTypeName, StringComparer.Ordinal)) { - string javaKey = peer.JavaName.Replace ('/', '.'); + string javaKey = JniSignatureHelper.JniNameToJavaName (peer.JavaName); string managedKey = peer.ManagedTypeName; string partialAsmQualifiedName = $"{managedKey}, {peer.AssemblyName}"; - string compatJniName = peer.CompatJniName.Replace ('/', '.'); + string compatJniName = JniSignatureHelper.JniNameToJavaName (peer.CompatJniName); // Line 1: PartialAssemblyQualifiedName;JavaKey writer.Write (partialAsmQualifiedName); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs index cddbd3eedfe..6797e32d6d0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs @@ -1,4 +1,3 @@ - using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs index 837c0cc2c0f..0e3f0ad9021 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Generic; using System.Globalization; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs index 2d185a783d6..e382556c748 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -1,4 +1,3 @@ - using System; using System.Globalization; using System.Linq; @@ -156,7 +155,7 @@ internal static XElement CreateMetaDataElement (MetaDataInfo meta) internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer) { - string jniName = peer.JavaName.Replace ('/', '.'); + string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); app.SetAttributeValue (AttName, jniName); var component = peer.ComponentAttribute; @@ -168,7 +167,7 @@ internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer) internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer) { - string jniName = peer.JavaName.Replace ('/', '.'); + string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); var element = new XElement ("instrumentation", new XAttribute (AttName, jniName)); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs index b2e051314a6..191637babaa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs @@ -1,4 +1,3 @@ - using System.Xml.Linq; namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 501130f03a3..f5604f97abf 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -22,9 +22,9 @@ class ManifestGenerator public string ApplicationLabel { get; set; } = ""; public string VersionCode { get; set; } = ""; public string VersionName { get; set; } = ""; - public string MinSdkVersion { get; set; } = "21"; - public string TargetSdkVersion { get; set; } = "36"; - public string AndroidRuntime { get; set; } = "coreclr"; + public string MinSdkVersion { get; set; } = ""; + public string TargetSdkVersion { get; set; } = ""; + public string RuntimeProviderJavaName { get; set; } = ""; public bool Debug { get; set; } public bool NeedsInternet { get; set; } public bool EmbedAssemblies { get; set; } @@ -76,7 +76,7 @@ class ManifestGenerator continue; } - string jniName = peer.JavaName.Replace ('/', '.'); + string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); if (existingTypes.Contains (jniName)) { continue; } @@ -152,6 +152,12 @@ void EnsureManifestAttributes (XElement manifest) // Add if (!manifest.Elements ("uses-sdk").Any ()) { + if (MinSdkVersion.IsNullOrEmpty ()) { + throw new InvalidOperationException ("MinSdkVersion must be provided by MSBuild."); + } + if (TargetSdkVersion.IsNullOrEmpty ()) { + throw new InvalidOperationException ("TargetSdkVersion must be provided by MSBuild."); + } manifest.AddFirst (new XElement ("uses-sdk", new XAttribute (AndroidNs + "minSdkVersion", MinSdkVersion), new XAttribute (AndroidNs + "targetSdkVersion", TargetSdkVersion))); @@ -175,16 +181,19 @@ XElement EnsureApplicationElement (XElement manifest) IList AddRuntimeProviders (XElement app) { - string packageName = "mono"; - string className = "MonoRuntimeProvider"; - - if (string.Equals (AndroidRuntime, "nativeaot", StringComparison.OrdinalIgnoreCase)) { - packageName = "net.dot.jni.nativeaot"; - className = "NativeAotRuntimeProvider"; + if (RuntimeProviderJavaName.IsNullOrEmpty ()) { + throw new InvalidOperationException ("RuntimeProviderJavaName must be provided by MSBuild."); } + int lastDot = RuntimeProviderJavaName.LastIndexOf ('.'); + if (lastDot < 0 || lastDot == RuntimeProviderJavaName.Length - 1) { + throw new InvalidOperationException ($"RuntimeProviderJavaName must be a fully-qualified Java type name: '{RuntimeProviderJavaName}'."); + } + + string packageName = RuntimeProviderJavaName.Substring (0, lastDot); + string className = RuntimeProviderJavaName.Substring (lastDot + 1); // Check if runtime provider already exists in template - string runtimeProviderName = $"{packageName}.{className}"; + string runtimeProviderName = RuntimeProviderJavaName; if (!app.Elements ("provider").Any (p => { var name = (string?)p.Attribute (ManifestConstants.AttName); return name == runtimeProviderName || diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs index ed5e68a208e..9610941d829 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Generic; using System.Globalization; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 09e969e8c87..ed9ea04384f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -48,18 +48,15 @@ public TrimmableTypeMapResult Execute ( // Collect Application/Instrumentation types that need deferred registerNatives var appRegTypes = allPeers .Where (p => p.CannotRegisterInStaticConstructor && !p.IsAbstract) - .Select (p => p.JavaName.Replace ('/', '.')) + .Select (p => JniSignatureHelper.JniNameToJavaName (p.JavaName)) .ToList (); if (appRegTypes.Count > 0) { log ($"Found {appRegTypes.Count} Application/Instrumentation types for deferred registration."); } - GeneratedManifest? manifest = null; - - // Generate merged AndroidManifest.xml if requested - if (manifestConfig is not null) { - manifest = GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate); - } + var manifest = manifestConfig is not null + ? GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate) + : null; return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); } @@ -67,12 +64,12 @@ public TrimmableTypeMapResult Execute ( GeneratedManifest GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo, ManifestConfig config, XDocument? manifestTemplate) { - string minSdk = "21"; - if (!config.SupportedOSPlatformVersion.IsNullOrEmpty () && Version.TryParse (config.SupportedOSPlatformVersion, out var sopv)) { + string minSdk = config.SupportedOSPlatformVersion ?? throw new InvalidOperationException ("SupportedOSPlatformVersion must be provided by MSBuild."); + if (Version.TryParse (minSdk, out var sopv)) { minSdk = sopv.Major.ToString (System.Globalization.CultureInfo.InvariantCulture); } - string targetSdk = config.AndroidApiLevel ?? "36"; + string targetSdk = config.AndroidApiLevel ?? throw new InvalidOperationException ("AndroidApiLevel must be provided by MSBuild."); if (Version.TryParse (targetSdk, out var apiVersion)) { targetSdk = apiVersion.Major.ToString (System.Globalization.CultureInfo.InvariantCulture); } @@ -86,7 +83,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes VersionName = config.VersionName ?? "", MinSdkVersion = minSdk, TargetSdkVersion = targetSdk, - AndroidRuntime = config.AndroidRuntime ?? "coreclr", + RuntimeProviderJavaName = config.RuntimeProviderJavaName ?? throw new InvalidOperationException ("RuntimeProviderJavaName must be provided by MSBuild."), Debug = config.Debug, NeedsInternet = config.NeedsInternet, EmbedAssemblies = config.EmbedAssemblies, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index 0ecf922fbcf..183e6005142 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -40,7 +40,7 @@ public record ManifestConfig ( string? VersionName = null, string? AndroidApiLevel = null, string? SupportedOSPlatformVersion = null, - string? AndroidRuntime = null, + string? RuntimeProviderJavaName = null, bool Debug = false, bool NeedsInternet = false, bool EmbedAssemblies = false, diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets index 9a5e479837c..8b49df77580 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets @@ -17,10 +17,6 @@ This file contains the MonoVM-specific MSBuild logic for .NET for Android. Value="false" Trim="true" /> - diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index 7119b475f1d..ba13aa93cdd 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -34,22 +34,6 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. false - - - - - - - diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index aa145ccb839..699b9baf1e3 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -17,6 +17,13 @@ <_BeforeCompileToDalvik>$(_BeforeCompileToDalvik);_GetMonoPlatformJarPath + + + + <_GenerateJavaStubsDependsOnTargets> _SetLatestTargetFrameworkVersion; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 8897c697ac1..d30ab44e74b 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -1,6 +1,10 @@ + + <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider + + + + <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider + + 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 15e37e84f02..c2a944dd6eb 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 @@ -66,7 +66,7 @@ VersionName="$(_AndroidVersionName)" AndroidApiLevel="$(_AndroidApiLevel)" SupportedOSPlatformVersion="$(SupportedOSPlatformVersion)" - AndroidRuntime="$(_AndroidRuntime)" + RuntimeProviderJavaName="$(_TrimmableRuntimeProviderJavaName)" Debug="$(AndroidIncludeDebugSymbols)" NeedsInternet="$(AndroidNeedsInternetPermission)" EmbedAssemblies="$(EmbedAssembliesIntoApk)" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 96390fabaf8..b7f1f21106f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -40,7 +40,7 @@ public class GenerateTrimmableTypeMap : AndroidTask public string? VersionName { get; set; } public string? AndroidApiLevel { get; set; } public string? SupportedOSPlatformVersion { get; set; } -public string? AndroidRuntime { get; set; } +public string? RuntimeProviderJavaName { get; set; } public bool Debug { get; set; } public bool NeedsInternet { get; set; } public bool EmbedAssemblies { get; set; } @@ -88,7 +88,7 @@ public override bool RunTask () VersionName: VersionName, AndroidApiLevel: AndroidApiLevel, SupportedOSPlatformVersion: SupportedOSPlatformVersion, -AndroidRuntime: AndroidRuntime, +RuntimeProviderJavaName: RuntimeProviderJavaName, Debug: Debug, NeedsInternet: NeedsInternet, EmbedAssemblies: EmbedAssemblies, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 907ee048b1a..d5ee42f8560 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -19,7 +19,7 @@ public class ManifestGeneratorTests VersionName = "1.0", MinSdkVersion = "21", TargetSdkVersion = "36", - AndroidRuntime = "coreclr", + RuntimeProviderJavaName = "mono.MonoRuntimeProvider", }; static XDocument ParseTemplate (string xml) => XDocument.Parse (xml); From 95a846dacbc84892a85d8c4a77878cd6262b76bf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 2 Apr 2026 13:14:54 +0200 Subject: [PATCH 08/11] Fix CS0246/CS1503: add missing 'using System.Xml.Linq' to GenerateTrimmableTypeMap.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMap.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index b7f1f21106f..816d2b1b768 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; +using System.Xml.Linq; using Microsoft.Android.Build.Tasks; using Microsoft.Android.Sdk.TrimmableTypeMap; using Microsoft.Build.Framework; @@ -101,7 +102,7 @@ public override bool RunTask () XDocument? manifestTemplate = null; if (!ManifestTemplate.IsNullOrEmpty () && File.Exists (ManifestTemplate)) { - manifestTemplate = System.Xml.Linq.XDocument.Load (ManifestTemplate); + manifestTemplate = XDocument.Load (ManifestTemplate); } result = generator.Execute ( From 4aacf1c8d8c2960ea7b39b0e2c53ba3d9710bf41 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 2 Apr 2026 13:16:43 +0200 Subject: [PATCH 09/11] Restore pre-existing feature switches in NativeAOT.targets; fix null! in TrimmableTypeMapTypes - Restore IsMonoRuntime=false and IsCoreClrRuntime=false RuntimeHostConfigurationOption items accidentally removed from NativeAOT.targets - Change ApplicationRegistrationTypes parameter from null! to nullable (null?) to comply with the no null-forgiving operator rule Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapTypes.cs | 2 +- .../targets/Microsoft.Android.Sdk.NativeAOT.targets | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index 183e6005142..2e162d0d61b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -9,7 +9,7 @@ public record TrimmableTypeMapResult ( IReadOnlyList GeneratedJavaSources, IReadOnlyList AllPeers, GeneratedManifest? Manifest = null, - IReadOnlyList ApplicationRegistrationTypes = null!) + IReadOnlyList? ApplicationRegistrationTypes = null) { /// /// Java class names (dot-separated) of Application/Instrumentation types diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index ba13aa93cdd..b4ecd0f5fe6 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -34,6 +34,18 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. false + + + + + + From 2362be9cd6022c8d03f09c2a0208cecdffb6aa58 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 2 Apr 2026 13:42:05 +0200 Subject: [PATCH 10/11] Address review: fix manifest save, remove dead PerAssemblyAcwMaps, style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use Files.CopyIfStringChanged instead of XDocument.Save() to preserve manifest file timestamp when content is unchanged (fixes incremental builds) - Remove dead PerAssemblyAcwMapFiles [Output] property, GeneratePerAssemblyAcwMaps() method, and AcwMapDirectory [Required] property — the only consumer (_MergeAcwMaps) was removed in this PR; nothing reads the per-assembly acw-map files anymore - Add 'using System.Text' and use short StringBuilder form Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 - .../Tasks/GenerateTrimmableTypeMap.cs | 32 ++----------------- .../Tasks/GenerateTrimmableTypeMapTests.cs | 2 -- 3 files changed, 3 insertions(+), 32 deletions(-) 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 c2a944dd6eb..d38aad44fb9 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 @@ -56,7 +56,6 @@ ResolvedAssemblies="@(_TypeMapInputAssemblies)" OutputDirectory="$(_TypeMapOutputDirectory)" JavaSourceOutputDirectory="$(_TypeMapJavaOutputDirectory)" - AcwMapDirectory="$(_TypeMapBaseOutputDir)acw-maps/" TargetFrameworkVersion="$(TargetFrameworkVersion)" ManifestTemplate="$(_AndroidManifestAbs)" MergedAndroidManifestOutput="$(_TypeMapBaseOutputDir)AndroidManifest.xml" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 816d2b1b768..0759d2648d5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; +using System.Text; using System.Xml.Linq; using Microsoft.Android.Build.Tasks; using Microsoft.Android.Sdk.TrimmableTypeMap; @@ -23,8 +24,6 @@ public class GenerateTrimmableTypeMap : AndroidTask [Required] public string JavaSourceOutputDirectory { get; set; } = ""; [Required] -public string AcwMapDirectory { get; set; } = ""; -[Required] public string TargetFrameworkVersion { get; set; } = ""; public string? AcwMapOutputFile { get; set; } @@ -54,8 +53,6 @@ public class GenerateTrimmableTypeMap : AndroidTask [Output] public ITaskItem [] GeneratedJavaFiles { get; set; } = []; [Output] -public ITaskItem []? PerAssemblyAcwMapFiles { get; set; } -[Output] public string[]? AdditionalProviderSources { get; set; } public override bool RunTask () @@ -67,7 +64,6 @@ public override bool RunTask () Directory.CreateDirectory (OutputDirectory); Directory.CreateDirectory (JavaSourceOutputDirectory); -Directory.CreateDirectory (AcwMapDirectory); var peReaders = new List (); var assemblies = new List<(string Name, PEReader Reader)> (); @@ -114,7 +110,6 @@ public override bool RunTask () GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); -PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers); // Write manifest to disk if generated if (result.Manifest is not null && !MergedAndroidManifestOutput.IsNullOrEmpty ()) { @@ -122,7 +117,7 @@ public override bool RunTask () if (!manifestDir.IsNullOrEmpty ()) { Directory.CreateDirectory (manifestDir); } - result.Manifest.Document.Save (MergedAndroidManifestOutput); + Files.CopyIfStringChanged (result.Manifest.Document.ToString (), MergedAndroidManifestOutput); AdditionalProviderSources = result.Manifest.AdditionalProviderSources; } @@ -245,27 +240,6 @@ ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSour return items.ToArray (); } -ITaskItem [] GeneratePerAssemblyAcwMaps (IReadOnlyList allPeers) -{ -var peersByAssembly = allPeers -.GroupBy (p => p.AssemblyName, StringComparer.Ordinal) -.OrderBy (g => g.Key, StringComparer.Ordinal); -var outputFiles = new List (); -foreach (var group in peersByAssembly) { -var peers = group.ToList (); -string outputFile = Path.Combine (AcwMapDirectory, $"acw-map.{group.Key}.txt"); -using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { -AcwMapWriter.Write (sw, peers); -sw.Flush (); -Files.CopyIfStreamChanged (sw.BaseStream, outputFile); -} -var item = new TaskItem (outputFile); -item.SetMetadata ("AssemblyName", group.Key); -outputFiles.Add (item); -} -return outputFiles.ToArray (); -} - static Version ParseTargetFrameworkVersion (string tfv) { if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) { @@ -279,7 +253,7 @@ static Version ParseTargetFrameworkVersion (string tfv) static string GenerateApplicationRegistrationJava (IReadOnlyList registrationTypes) { - var sb = new System.Text.StringBuilder (); + var sb = new StringBuilder (); sb.AppendLine ("package net.dot.android;"); sb.AppendLine (); sb.AppendLine ("public class ApplicationRegistration {"); 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 25ab24fa9a1..f76d95f246b 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 @@ -40,7 +40,6 @@ public void Execute_InvalidTargetFrameworkVersion_Fails () ResolvedAssemblies = [], OutputDirectory = outputDir, JavaSourceOutputDirectory = javaDir, - AcwMapDirectory = Path.Combine (Root, path, "acw-maps"), TargetFrameworkVersion = "not-a-version", }; @@ -132,7 +131,6 @@ GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, ResolvedAssemblies = assemblies, OutputDirectory = outputDir, JavaSourceOutputDirectory = javaDir, - AcwMapDirectory = Path.Combine (outputDir, "..", "acw-maps"), TargetFrameworkVersion = tfv, }; } From 285860095e52147500ae67b83e2957f3d65be657 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 2 Apr 2026 14:08:16 +0200 Subject: [PATCH 11/11] Fix: use MemoryStream to preserve XML declaration in manifest output XDocument.ToString() drops the declaration. Use XDocument.Save(Stream) + Files.CopyIfStreamChanged to preserve both the XML declaration and incremental-build safety. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateTrimmableTypeMap.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 0759d2648d5..b603317ef0f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -117,7 +117,11 @@ public override bool RunTask () if (!manifestDir.IsNullOrEmpty ()) { Directory.CreateDirectory (manifestDir); } - Files.CopyIfStringChanged (result.Manifest.Document.ToString (), MergedAndroidManifestOutput); + using (var ms = new MemoryStream ()) { + result.Manifest.Document.Save (ms); + ms.Position = 0; + Files.CopyIfStreamChanged (ms, MergedAndroidManifestOutput); + } AdditionalProviderSources = result.Manifest.AdditionalProviderSources; }