diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs index 8d4005d6390..4314a2acba8 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; @@ -29,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 b25a1407a4f..6797e32d6d0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs @@ -1,5 +1,3 @@ -#nullable enable - 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 8ba00724f87..0e3f0ad9021 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs @@ -1,5 +1,3 @@ -#nullable enable - 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 9f6f8e7f516..e382556c748 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Globalization; using System.Linq; @@ -157,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; @@ -169,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/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/Generator/ManifestConstants.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs index 4ed62e9baa6..191637babaa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs @@ -1,5 +1,3 @@ -#nullable enable - 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 1bea7928e30..f5604f97abf 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; @@ -23,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; } @@ -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; @@ -78,7 +76,7 @@ public IList Generate ( continue; } - string jniName = peer.JavaName.Replace ('/', '.'); + string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); if (existingTypes.Contains (jniName)) { continue; } @@ -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 () @@ -161,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))); @@ -184,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 32b07671600..9610941d829 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs @@ -1,5 +1,3 @@ -#nullable enable - 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 1a2b0691057..ed9ea04384f 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; @@ -15,16 +16,23 @@ 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, 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) + HashSet frameworkAssemblyNames, + ManifestConfig? manifestConfig = null, + XDocument? manifestTemplate = 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,66 @@ 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); + + // Collect Application/Instrumentation types that need deferred registerNatives + var appRegTypes = allPeers + .Where (p => p.CannotRegisterInStaticConstructor && !p.IsAbstract) + .Select (p => JniSignatureHelper.JniNameToJavaName (p.JavaName)) + .ToList (); + if (appRegTypes.Count > 0) { + log ($"Found {appRegTypes.Count} Application/Instrumentation types for deferred registration."); + } + + var manifest = manifestConfig is not null + ? GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate) + : null; + + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); + } + + GeneratedManifest GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo, + ManifestConfig config, XDocument? manifestTemplate) + { + 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 ?? throw new InvalidOperationException ("AndroidApiLevel must be provided by MSBuild."); + 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, + RuntimeProviderJavaName = config.RuntimeProviderJavaName ?? throw new InvalidOperationException ("RuntimeProviderJavaName must be provided by MSBuild."), + Debug = config.Debug, + NeedsInternet = config.NeedsInternet, + EmbedAssemblies = config.EmbedAssemblies, + ForceDebuggable = forceDebuggable, + ForceExtractNativeLibs = forceDebuggable, + ManifestPlaceholders = config.ManifestPlaceholders, + ApplicationJavaClass = config.ApplicationJavaClass, + }; + + var (doc, providerNames) = generator.Generate (manifestTemplate, allPeers, assemblyManifestInfo); + return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } - 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..2e162d0d61b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -1,13 +1,49 @@ using System.Collections.Generic; using System.IO; +using System.Xml.Linq; namespace Microsoft.Android.Sdk.TrimmableTypeMap; public record TrimmableTypeMapResult ( IReadOnlyList GeneratedAssemblies, IReadOnlyList GeneratedJavaSources, - IReadOnlyList AllPeers); + IReadOnlyList AllPeers, + 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); 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. +/// +public record ManifestConfig ( + string PackageName, + string? ApplicationLabel = null, + string? VersionCode = null, + string? VersionName = null, + string? AndroidApiLevel = null, + string? SupportedOSPlatformVersion = null, + string? RuntimeProviderJavaName = 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/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 a06b771e122..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,15 +1,71 @@ - + - - + + <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider + + + + + + + %(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)/ + + - <_ResolvedAssemblies Include="@(_GeneratedTypeMapAssemblies)" /> + <_CurrentLinkedTypeMapDlls Remove="@(_CurrentLinkedTypeMapDlls)" /> + <_CurrentTypeMapDlls Remove="@(_CurrentTypeMapDlls)" /> 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 eabaffb2745..cc96c228bc7 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,6 +2,10 @@ Adds generated typemap assemblies to ILC inputs. --> + + <_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 b056316d7db..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 @@ -1,9 +1,8 @@ - + + @@ -12,94 +11,172 @@ <_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)')" /> + + TargetFrameworkVersion="$(TargetFrameworkVersion)" + ManifestTemplate="$(_AndroidManifestAbs)" + MergedAndroidManifestOutput="$(_TypeMapBaseOutputDir)AndroidManifest.xml" + PackageName="$(_AndroidPackage)" + ApplicationLabel="$(_ApplicationLabel)" + VersionCode="$(_AndroidVersionCode)" + VersionName="$(_AndroidVersionName)" + AndroidApiLevel="$(_AndroidApiLevel)" + SupportedOSPlatformVersion="$(SupportedOSPlatformVersion)" + RuntimeProviderJavaName="$(_TrimmableRuntimeProviderJavaName)" + Debug="$(AndroidIncludeDebugSymbols)" + NeedsInternet="$(AndroidNeedsInternetPermission)" + EmbedAssemblies="$(EmbedAssembliesIntoApk)" + ManifestPlaceholders="$(AndroidManifestPlaceholders)" + CheckedBuild="$(_AndroidCheckedBuild)" + ApplicationJavaClass="$(AndroidApplicationJavaClass)" + AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt" + ApplicationRegistrationOutputFile="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java"> - + - + + + - - - - + + - <_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..5e420bd0251 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -1,11 +1,11 @@ -#nullable enable - using System; using System.Collections.Generic; using System.IO; 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; using Microsoft.Build.Framework; @@ -15,177 +15,262 @@ namespace Xamarin.Android.Tasks; public class GenerateTrimmableTypeMap : AndroidTask { -public override string TaskPrefix => "GTT"; - -[Required] -public ITaskItem [] ResolvedAssemblies { get; set; } = []; -[Required] -public string OutputDirectory { get; set; } = ""; -[Required] -public string JavaSourceOutputDirectory { get; set; } = ""; -[Required] -public string AcwMapDirectory { get; set; } = ""; -[Required] -public string TargetFrameworkVersion { get; set; } = ""; -[Output] -public ITaskItem [] GeneratedAssemblies { get; set; } = []; -[Output] -public ITaskItem [] GeneratedJavaFiles { get; set; } = []; -[Output] -public ITaskItem []? PerAssemblyAcwMapFiles { get; set; } - -public override bool RunTask () -{ -var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion); -var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); -// TODO(#10792): populate with framework assembly names to skip JCW generation for pre-compiled framework types -var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); - -Directory.CreateDirectory (OutputDirectory); -Directory.CreateDirectory (JavaSourceOutputDirectory); -Directory.CreateDirectory (AcwMapDirectory); - -var peReaders = new List (); -var assemblies = new List<(string Name, PEReader Reader)> (); -TrimmableTypeMapResult? result = null; -try { -foreach (var path in assemblyPaths) { -var peReader = new PEReader (File.OpenRead (path)); -peReaders.Add (peReader); -var mdReader = peReader.GetMetadataReader (); -assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader)); -} + public override string TaskPrefix => "GTT"; -var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); -result = generator.Execute (assemblies, systemRuntimeVersion, frameworkAssemblyNames); + [Required] + public ITaskItem [] ResolvedAssemblies { get; set; } = []; + [Required] + public string OutputDirectory { get; set; } = ""; + [Required] + public string JavaSourceOutputDirectory { get; set; } = ""; + [Required] + public string TargetFrameworkVersion { get; set; } = ""; -GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); -GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); -PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers); -} finally { -if (result is not null) { -foreach (var assembly in result.GeneratedAssemblies) { -assembly.Content.Dispose (); -} -} -foreach (var peReader in peReaders) { -peReader.Dispose (); -} -} + public string? AcwMapOutputFile { get; set; } -return !Log.HasLoggedErrors; -} + public string? ApplicationRegistrationOutputFile { get; set; } -ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyPaths) -{ -// Build a map from assembly name -> source path for timestamp comparison -var sourcePathByName = new Dictionary (StringComparer.Ordinal); -foreach (var path in assemblyPaths) { -var name = Path.GetFileNameWithoutExtension (path); -sourcePathByName [name] = path; -} + public string? ManifestTemplate { get; set; } -var items = new List (); -bool anyRegenerated = false; + public string? MergedAndroidManifestOutput { get; set; } -foreach (var assembly in assemblies) { -if (assembly.Name == "_Microsoft.Android.TypeMaps") { -continue; // Handle root assembly separately below -} + 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? RuntimeProviderJavaName { 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; } -string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll"); -// Extract the original assembly name from the typemap name (e.g., "_Foo.TypeMap" -> "Foo") -string originalName = assembly.Name; -if (originalName.StartsWith ("_", StringComparison.Ordinal) && originalName.EndsWith (".TypeMap", StringComparison.Ordinal)) { -originalName = originalName.Substring (1, originalName.Length - ".TypeMap".Length - 1); -} + [Output] + public ITaskItem [] GeneratedAssemblies { get; set; } = []; + [Output] + public ITaskItem [] GeneratedJavaFiles { get; set; } = []; + [Output] + public string[]? AdditionalProviderSources { get; set; } -if (IsUpToDate (outputPath, originalName, sourcePathByName)) { -Log.LogDebugMessage ($" {assembly.Name}: up to date, skipping"); -} else { -Files.CopyIfStreamChanged (assembly.Content, outputPath); -anyRegenerated = true; -Log.LogDebugMessage ($" {assembly.Name}: written"); -} + public override bool RunTask () + { + var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion); + var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); + // TODO(#10792): populate with framework assembly names to skip JCW generation for pre-compiled framework types + var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); -items.Add (new TaskItem (outputPath)); -} + Directory.CreateDirectory (OutputDirectory); + Directory.CreateDirectory (JavaSourceOutputDirectory); -// Root assembly — regenerate if any per-assembly typemap changed -var rootAssembly = assemblies.FirstOrDefault (a => a.Name == "_Microsoft.Android.TypeMaps"); -if (rootAssembly is not null) { -string rootOutputPath = Path.Combine (OutputDirectory, rootAssembly.Name + ".dll"); -if (anyRegenerated || !File.Exists (rootOutputPath)) { -Files.CopyIfStreamChanged (rootAssembly.Content, rootOutputPath); -Log.LogDebugMessage ($" Root: written"); -} else { -Log.LogDebugMessage ($" Root: up to date, skipping"); -} -items.Add (new TaskItem (rootOutputPath)); -} + var peReaders = new List (); + var assemblies = new List<(string Name, PEReader Reader)> (); + TrimmableTypeMapResult? result = null; + try { + foreach (var path in assemblyPaths) { + var peReader = new PEReader (File.OpenRead (path)); + peReaders.Add (peReader); + var mdReader = peReader.GetMetadataReader (); + assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader)); + } -return items.ToArray (); -} + ManifestConfig? manifestConfig = null; + if (!MergedAndroidManifestOutput.IsNullOrEmpty () && !PackageName.IsNullOrEmpty ()) { + manifestConfig = new ManifestConfig ( + PackageName: PackageName, + ApplicationLabel: ApplicationLabel, + VersionCode: VersionCode, + VersionName: VersionName, + AndroidApiLevel: AndroidApiLevel, + SupportedOSPlatformVersion: SupportedOSPlatformVersion, + RuntimeProviderJavaName: RuntimeProviderJavaName, + Debug: Debug, + NeedsInternet: NeedsInternet, + EmbedAssemblies: EmbedAssemblies, + ManifestPlaceholders: ManifestPlaceholders, + CheckedBuild: CheckedBuild, + ApplicationJavaClass: ApplicationJavaClass); + } -static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName) -{ -if (!File.Exists (outputPath)) { -return false; -} -if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) { -return false; -} -return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath); -} + var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); -ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSources) -{ -var items = new List (); -foreach (var source in javaSources) { -string outputPath = Path.Combine (JavaSourceOutputDirectory, source.RelativePath); -string? dir = Path.GetDirectoryName (outputPath); -if (!string.IsNullOrEmpty (dir)) { -Directory.CreateDirectory (dir); -} -using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { -sw.Write (source.Content); -sw.Flush (); -Files.CopyIfStreamChanged (sw.BaseStream, outputPath); -} -items.Add (new TaskItem (outputPath)); -} -return items.ToArray (); -} + XDocument? manifestTemplate = null; + if (!ManifestTemplate.IsNullOrEmpty () && File.Exists (ManifestTemplate)) { + manifestTemplate = XDocument.Load (ManifestTemplate); + } -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 (); -} + result = generator.Execute ( + assemblies, + systemRuntimeVersion, + frameworkAssemblyNames, + manifestConfig, + manifestTemplate); -static Version ParseTargetFrameworkVersion (string tfv) -{ -if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) { -tfv = tfv.Substring (1); -} -if (Version.TryParse (tfv, out var version)) { -return version; -} -throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version."); -} + GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); + GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); + + // 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); + } + using (var ms = new MemoryStream ()) { + result.Manifest.Document.Save (ms); + ms.Position = 0; + Files.CopyIfStreamChanged (ms, 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}."); + } + + // 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) { + assembly.Content.Dispose (); + } + } + foreach (var peReader in peReaders) { + peReader.Dispose (); + } + } + + return !Log.HasLoggedErrors; + } + + ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyPaths) + { + // Build a map from assembly name -> source path for timestamp comparison + var sourcePathByName = new Dictionary (StringComparer.Ordinal); + foreach (var path in assemblyPaths) { + var name = Path.GetFileNameWithoutExtension (path); + sourcePathByName [name] = path; + } + + var items = new List (); + bool anyRegenerated = false; + + foreach (var assembly in assemblies) { + if (assembly.Name == "_Microsoft.Android.TypeMaps") { + continue; // Handle root assembly separately below + } + + string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll"); + // Extract the original assembly name from the typemap name (e.g., "_Foo.TypeMap" -> "Foo") + string originalName = assembly.Name; + if (originalName.StartsWith ("_", StringComparison.Ordinal) && originalName.EndsWith (".TypeMap", StringComparison.Ordinal)) { + originalName = originalName.Substring (1, originalName.Length - ".TypeMap".Length - 1); + } + + if (IsUpToDate (outputPath, originalName, sourcePathByName)) { + Log.LogDebugMessage ($" {assembly.Name}: up to date, skipping"); + } else { + Files.CopyIfStreamChanged (assembly.Content, outputPath); + anyRegenerated = true; + Log.LogDebugMessage ($" {assembly.Name}: written"); + } + + items.Add (new TaskItem (outputPath)); + } + + // Root assembly — regenerate if any per-assembly typemap changed + var rootAssembly = assemblies.FirstOrDefault (a => a.Name == "_Microsoft.Android.TypeMaps"); + if (rootAssembly is not null) { + string rootOutputPath = Path.Combine (OutputDirectory, rootAssembly.Name + ".dll"); + if (anyRegenerated || !File.Exists (rootOutputPath)) { + Files.CopyIfStreamChanged (rootAssembly.Content, rootOutputPath); + Log.LogDebugMessage ($" Root: written"); + } else { + Log.LogDebugMessage ($" Root: up to date, skipping"); + } + items.Add (new TaskItem (rootOutputPath)); + } + + return items.ToArray (); + } + + static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName) + { + if (!File.Exists (outputPath)) { + return false; + } + if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) { + return false; + } + return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath); + } + + ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSources) + { + var items = new List (); + foreach (var source in javaSources) { + string outputPath = Path.Combine (JavaSourceOutputDirectory, source.RelativePath); + string? dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { + sw.Write (source.Content); + sw.Flush (); + Files.CopyIfStreamChanged (sw.BaseStream, outputPath); + } + items.Add (new TaskItem (outputPath)); + } + return items.ToArray (); + } + + static Version ParseTargetFrameworkVersion (string tfv) + { + if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) { + tfv = tfv.Substring (1); + } + if (Version.TryParse (tfv, out var version)) { + return version; + } + throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version."); + } + + static string GenerateApplicationRegistrationJava (IReadOnlyList registrationTypes) + { + var sb = new 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 (); + } } 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, }; } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc index f25e2da1a1d..81aafc33724 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc @@ -11,25 +11,25 @@ "Size": 18288 }, "lib/arm64-v8a/lib_Java.Interop.dll.so": { - "Size": 88504 + "Size": 88040 }, "lib/arm64-v8a/lib_Mono.Android.dll.so": { - "Size": 125280 + "Size": 118024 }, "lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": { - "Size": 26520 + "Size": 26544 }, "lib/arm64-v8a/lib_System.Console.dll.so": { - "Size": 24424 + "Size": 24432 }, "lib/arm64-v8a/lib_System.Linq.dll.so": { - "Size": 25504 + "Size": 25512 }, "lib/arm64-v8a/lib_System.Private.CoreLib.dll.so": { - "Size": 692016 + "Size": 635608 }, "lib/arm64-v8a/lib_System.Runtime.dll.so": { - "Size": 20288 + "Size": 20296 }, "lib/arm64-v8a/lib_System.Runtime.InteropServices.dll.so": { "Size": 21632 @@ -38,7 +38,7 @@ "Size": 20032 }, "lib/arm64-v8a/libarc.bin.so": { - "Size": 19112 + "Size": 19176 }, "lib/arm64-v8a/libmono-component-marshal-ilgen.so": { "Size": 36616 @@ -47,7 +47,7 @@ "Size": 1386512 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3124304 + "Size": 3124368 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 71936 @@ -56,16 +56,16 @@ "Size": 1281696 }, "lib/arm64-v8a/libSystem.Native.so": { - "Size": 107024 + "Size": 107904 }, "lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so": { "Size": 165536 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 19640 + "Size": 19840 }, "META-INF/BNDLTOOL.RSA": { - "Size": 1223 + "Size": 1221 }, "META-INF/BNDLTOOL.SF": { "Size": 3266 @@ -98,5 +98,5 @@ "Size": 1904 } }, - "PackageSize": 3324437 + "PackageSize": 3267093 } \ No newline at end of file diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 2653286fd20..d5ee42f8560 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", @@ -36,17 +19,10 @@ public void Dispose () VersionName = "1.0", MinSdkVersion = "21", TargetSdkVersion = "36", - AndroidRuntime = "coreclr", + RuntimeProviderJavaName = "mono.MonoRuntimeProvider", }; - 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")