From 1dc27ed853d44fef7419b976cabcf72a18f1d053 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 19 Aug 2026 17:27:25 +0200 Subject: [PATCH 1/3] [build] Avoid staging unchanged framework assemblies Package non-Android framework assemblies directly from runtime packs instead of opening and copying them into project intermediates. Keep project-local zero-byte scan markers for downstream typemap and JCW consumers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6469f820-c19a-4450-8bb9-de70387d899c --- ...oft.Android.Sdk.AssemblyResolution.targets | 11 +++++--- .../Tasks/AssemblyModifierPipeline.cs | 5 ++++ .../Tasks/GenerateACWMap.cs | 2 +- .../Tasks/GenerateJavaCallableWrappers.cs | 2 +- .../Tasks/LinkAssembliesNoShrink.cs | 17 ++++++++++++ .../Tasks/ProcessAssemblies.cs | 13 +++++++-- .../Utilities/JavaObjectsXmlFile.cs | 7 +++++ .../Utilities/TypeMapGenerator.cs | 2 +- .../Utilities/TypeMapObjectsXmlFile.cs | 7 +++++ .../Xamarin.Android.Common.targets | 27 ++++++++++++++++++- 10 files changed, 83 insertions(+), 10 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets index 2b056f02870..8d21f4d71ce 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets @@ -264,10 +264,13 @@ _ResolveAssemblies MSBuild target. - <_ResolvedAssemblies Include="@(ResolvedAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " /> - <_ResolvedUserAssemblies Include="@(ResolvedUserAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " /> - <_ResolvedFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " /> - <_ResolvedSymbols Include="@(ResolvedSymbols->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " /> + <_ResolvedAssemblies Include="@(_AndroidAssembliesToLinkNoShrink->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " /> + <_ResolvedAssemblies Include="@(_AndroidAssembliesToPackageWithoutLinkNoShrink)" Condition=" '%(DestinationSubPath)' != '' " /> + <_ResolvedUserAssemblies Include="@(ResolvedUserAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " /> + <_ResolvedFrameworkAssemblies Include="@(_AndroidAssembliesToLinkNoShrink->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(FrameworkAssembly)' == 'True' And '%(DestinationSubPath)' != '' " /> + <_ResolvedFrameworkAssemblies Include="@(_AndroidAssembliesToPackageWithoutLinkNoShrink)" Condition=" '%(DestinationSubPath)' != '' " /> + <_ResolvedSymbols Include="@(_AndroidSymbolsToLinkNoShrink->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " /> + <_ResolvedSymbols Include="@(_AndroidSymbolsToPackageWithoutLinkNoShrink)" Condition=" '%(DestinationSubPath)' != '' " /> <_ShrunkAssemblies Include="@(_ResolvedAssemblies)" /> <_ShrunkUserAssemblies Include="@(_ResolvedUserAssemblies)" /> <_ShrunkFrameworkAssemblies Include="@(_ResolvedFrameworkAssemblies)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs index a6e5abbc0ac..212009784dc 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs @@ -95,6 +95,9 @@ public override bool RunTask () throw new InvalidOperationException ($"Internal error: assembly '{sourceArch}' targets architecture '{sourceArch}', while destination assembly '{destination}' targets '{destinationArch}' instead"); } + if (TryProcessWithoutPipeline (source, destination)) + continue; + // Each architecture must have a different set of context classes, or otherwise only the first instance of the assembly may be rewritten. if (currentArch != sourceArch) { currentArch = sourceArch; @@ -128,6 +131,8 @@ public override bool RunTask () return !Log.HasLoggedErrors; } + protected virtual bool TryProcessWithoutPipeline (ITaskItem source, ITaskItem destination) => false; + protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context) { // FindJavaObjectsStep diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateACWMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateACWMap.cs index 7d289d2663d..bfb161f7619 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateACWMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateACWMap.cs @@ -38,7 +38,7 @@ void GenerateMap () var entries = new List (); foreach (var assembly in singleArchAssemblies) { - var wrappersPath = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (assembly.ItemSpec); + var wrappersPath = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (assembly); if (!File.Exists (wrappersPath)) { Log.LogError ($"'{wrappersPath}' not found."); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaCallableWrappers.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaCallableWrappers.cs index ca59e949238..0d47b346c49 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaCallableWrappers.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaCallableWrappers.cs @@ -62,7 +62,7 @@ void GenerateWrappers (List assemblies) var sw = Stopwatch.StartNew (); foreach (var assembly in assemblies) { - var wrappersPath = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (assembly.ItemSpec); + var wrappersPath = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (assembly); if (!File.Exists (wrappersPath)) { Log.LogError ($"'{wrappersPath}' not found."); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs index cde9d9bc8f9..147d1e2fe18 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs @@ -1,6 +1,8 @@ #nullable enable using System; +using System.IO; +using Microsoft.Build.Framework; using MonoDroid.Tuner; namespace Xamarin.Android.Tasks @@ -17,6 +19,21 @@ public class LinkAssembliesNoShrink : AssemblyModifierPipeline public bool UseDesignerAssembly { get; set; } + protected override bool TryProcessWithoutPipeline (ITaskItem source, ITaskItem destination) + { + if (!bool.TryParse (source.GetMetadata ("AndroidSkipAssemblyModification"), out bool skipAssemblyModification) || !skipAssemblyModification) + return false; + + // Downstream scanners treat a zero-byte file as an assembly that did not need scanning. + var marker = Path.ChangeExtension (destination.ItemSpec, ".scan.empty"); + var markerDirectory = Path.GetDirectoryName (marker); + if (markerDirectory.IsNullOrEmpty ()) + throw new InvalidOperationException ($"Could not determine the output directory for '{marker}'."); + Directory.CreateDirectory (markerDirectory); + JavaObjectsXmlFile.WriteEmptyFile (marker, Log); + return true; + } + protected override void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context) { // FixAbstractMethodsStep diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ProcessAssemblies.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ProcessAssemblies.cs index 5c166e75935..c48e6f55836 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ProcessAssemblies.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ProcessAssemblies.cs @@ -18,6 +18,7 @@ namespace Xamarin.Android.Tasks /// Also sets some metadata: /// * %(FrameworkAssembly)=True to determine if framework or user assembly /// * %(HasMonoAndroidReference)=True for incremental build performance + /// * %(AndroidSkipAssemblyModification)=True for framework assemblies with no Android types /// * Modify %(DestinationSubDirectory) and %(DestinationSubPath) if an assembly has an architecture-specific version /// public class ProcessAssemblies : AndroidTask @@ -138,11 +139,19 @@ void SetMetadataForAssemblies (List output, Dictionary Path.ChangeExtension (assemblyPath, ".jlo.xml"); + public static string GetJavaObjectsXmlFilePath (ITaskItem assembly) + { + var path = assembly.GetMetadata ("JavaObjectsXmlFile"); + return path.IsNullOrEmpty () ? GetJavaObjectsXmlFilePath (assembly.ItemSpec) : path; + } + public static JavaObjectsXmlFile Import (string filename, JavaObjectsXmlFileReadType readType) { // If the file has zero length, then the assembly wasn't scanned because it couldn't contain JLOs. diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs index ece5fe1eba2..19180b34778 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs @@ -338,7 +338,7 @@ public ReleaseGenerationState GetReleaseGenerationState () var adapter = new TypeMapObjectsFileAdapter (targetArch); foreach (var assembly in assemblies) { - var typeMapPath = TypeMapObjectsXmlFile.GetTypeMapObjectsXmlFilePath (assembly.ItemSpec); + var typeMapPath = TypeMapObjectsXmlFile.GetTypeMapObjectsXmlFilePath (assembly); if (!File.Exists (typeMapPath)) { log.LogError ($"'{typeMapPath}' not found."); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs index 0d38817c825..5a610af49d1 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs @@ -4,6 +4,7 @@ using System.IO; using System.Xml; using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using ModuleReleaseData = Xamarin.Android.Tasks.TypeMapGenerator.ModuleReleaseData; @@ -165,6 +166,12 @@ void ExportTypeMapReleaseEntry (XmlWriter xml, TypeMapReleaseEntry entry, string public static string GetTypeMapObjectsXmlFilePath (string assemblyPath) => Path.ChangeExtension (assemblyPath, ".typemap.xml"); + public static string GetTypeMapObjectsXmlFilePath (ITaskItem assembly) + { + var path = assembly.GetMetadata ("TypeMapObjectsXmlFile"); + return path.IsNullOrEmpty () ? GetTypeMapObjectsXmlFilePath (assembly.ItemSpec) : path; + } + public static TypeMapObjectsXmlFile Import (string filename) { // If the file has zero length, then the assembly wasn't scanned because it couldn't contain JLOs. diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index f043a72abc5..45b63ecbe65 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1434,6 +1434,31 @@ because xbuild doesn't support framework reference assemblies. <_AllResolvedAssemblies Include="@(ResolvedAssemblies)" /> + + <_AndroidLinkNoShrinkInput Include="@(ResolvedAssemblies)"> + $(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty + $(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath) + + <_AndroidAssembliesToLinkNoShrink + Include="@(ResolvedAssemblies)" + Condition=" '%(ResolvedAssemblies.AndroidSkipAssemblyModification)' != 'True' " /> + <_AndroidAssembliesToPackageWithoutLinkNoShrink + Include="@(ResolvedAssemblies)" + Condition=" '%(ResolvedAssemblies.AndroidSkipAssemblyModification)' == 'True' "> + $(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty + $(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty + + <_AndroidSymbolsToLinkNoShrink + Include="@(ResolvedSymbols)" + Condition=" '%(ResolvedSymbols.AndroidSkipAssemblyModification)' != 'True' " /> + <_AndroidSymbolsToPackageWithoutLinkNoShrink + Include="@(ResolvedSymbols)" + Condition=" '%(ResolvedSymbols.AndroidSkipAssemblyModification)' == 'True' " /> @@ -1441,7 +1466,7 @@ because xbuild doesn't support framework reference assemblies. DependsOnTargets="_LinkAssembliesNoShrinkInputs" Condition="'$(PublishTrimmed)' != 'true'" Inputs="@(ResolvedAssemblies);$(_AndroidBuildPropertiesCache)" - Outputs="@(ResolvedAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')"> + Outputs="@(_AndroidLinkNoShrinkInput->'%(OutputFile)')"> Date: Thu, 20 Aug 2026 14:00:29 +0200 Subject: [PATCH 2/3] [build] Preserve LinkAssembliesNoShrink batching Keep conditional output metadata on the original ResolvedAssemblies items so MSBuild can correlate inputs and outputs during partial builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 986b59ba-93bf-46dc-a6c0-4f01dd7e2b3f --- .../Xamarin.Android.Common.targets | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 45b63ecbe65..572bab7b0b8 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1436,14 +1436,14 @@ because xbuild doesn't support framework reference assemblies. <_AllResolvedAssemblies Include="@(ResolvedAssemblies)" /> - <_AndroidLinkNoShrinkInput Include="@(ResolvedAssemblies)"> - + <_AndroidLinkNoShrinkOutput Condition=" '%(ResolvedAssemblies.AndroidSkipAssemblyModification)' == 'True' " - >$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty - $(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty + <_AndroidLinkNoShrinkOutput Condition=" '%(ResolvedAssemblies.AndroidSkipAssemblyModification)' != 'True' " - >$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath) - + >$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath) + <_AndroidAssembliesToLinkNoShrink Include="@(ResolvedAssemblies)" Condition=" '%(ResolvedAssemblies.AndroidSkipAssemblyModification)' != 'True' " /> @@ -1466,7 +1466,7 @@ because xbuild doesn't support framework reference assemblies. DependsOnTargets="_LinkAssembliesNoShrinkInputs" Condition="'$(PublishTrimmed)' != 'true'" Inputs="@(ResolvedAssemblies);$(_AndroidBuildPropertiesCache)" - Outputs="@(_AndroidLinkNoShrinkInput->'%(OutputFile)')"> + Outputs="@(ResolvedAssemblies->'%(_AndroidLinkNoShrinkOutput)')"> Date: Fri, 21 Aug 2026 11:09:39 +0200 Subject: [PATCH 3/3] [tests] Cover unstaged framework assemblies Verify runtime-pack packaging, shared scan markers, Android framework staging, and no-op incremental behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../IncrementalBuildTest.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs index 6b52f8825a5..70f3d0d1bb8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs @@ -15,6 +15,90 @@ namespace Xamarin.Android.Build.Tests [Parallelizable (ParallelScope.Children)] public class IncrementalBuildTest : BaseTest { + [Test] + public void FrameworkAssembliesArePackagedWithoutStaging () + { + const string runtimeIdentifier = "android-arm64"; + const string abi = "arm64-v8a"; + + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var captureFrameworkAssemblies = new Import (() => "CaptureFrameworkAssemblies.targets") { + TextContent = () => +""" + + + + + +""" + }; + var proj = new XamarinAndroidApplicationProject { + EmbedAssembliesIntoApk = true, + Imports = { captureFrameworkAssemblies }, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty (KnownProperties.RuntimeIdentifier, runtimeIdentifier); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "first build should succeed"); + builder.Output.AssertTargetIsNotSkipped ("_LinkAssembliesNoShrink"); + + string [] GetFrameworkAssemblyMetadata (string assemblyName) + { + string prefix = $"FrameworkAssembly={assemblyName}|"; + string line = builder.LastBuildOutput.Single (line => line.Contains (prefix, StringComparison.Ordinal)); + int start = line.IndexOf (prefix, StringComparison.Ordinal); + var metadata = line.Substring (start + "FrameworkAssembly=".Length).Split ('|'); + Assert.AreEqual (5, metadata.Length, $"Unexpected framework assembly metadata: {line}"); + return metadata; + } + + var projectDirectory = Path.Combine (Root, builder.ProjectDirectory); + string ResolveProjectPath (string path) => Path.GetFullPath (Path.Combine (projectDirectory, path)); + + var assemblyDirectory = Path.Combine (projectDirectory, proj.IntermediateOutputPath, runtimeIdentifier, "android", "assets", abi); + var stagedMicrosoftCSharp = Path.Combine (assemblyDirectory, "Microsoft.CSharp.dll"); + var marker = Path.Combine (assemblyDirectory, "Microsoft.CSharp.scan.empty"); + var microsoftCSharp = GetFrameworkAssemblyMetadata ("Microsoft.CSharp"); + + FileAssert.Exists (ResolveProjectPath (microsoftCSharp [1])); + Assert.That (microsoftCSharp [2], Does.StartWith ("Microsoft.NETCore.App.Runtime."), + $"Microsoft.CSharp.dll should come from a runtime pack, but its package ID was '{microsoftCSharp [2]}'."); + Assert.AreNotEqual (Path.GetFullPath (stagedMicrosoftCSharp), ResolveProjectPath (microsoftCSharp [1]), + "Microsoft.CSharp.dll should be packaged directly from its runtime pack."); + FileAssert.DoesNotExist (stagedMicrosoftCSharp); + FileAssert.Exists (marker); + Assert.AreEqual (0, new FileInfo (marker).Length, $"{marker} should be empty."); + Assert.AreEqual (Path.GetFullPath (marker), ResolveProjectPath (microsoftCSharp [3]), + "JavaObjectsXmlFile should point to the shared scan marker."); + Assert.AreEqual (Path.GetFullPath (marker), ResolveProjectPath (microsoftCSharp [4]), + "TypeMapObjectsXmlFile should point to the shared scan marker."); + + var monoAndroid = GetFrameworkAssemblyMetadata ("Mono.Android"); + var stagedMonoAndroid = Path.Combine (assemblyDirectory, "Mono.Android.dll"); + Assert.AreEqual (Path.GetFullPath (stagedMonoAndroid), ResolveProjectPath (monoAndroid [1]), + "Mono.Android.dll should continue to use the staged assembly."); + FileAssert.Exists (stagedMonoAndroid); + FileAssert.Exists (Path.ChangeExtension (stagedMonoAndroid, ".jlo.xml")); + FileAssert.Exists (Path.ChangeExtension (stagedMonoAndroid, ".typemap.xml")); + FileAssert.DoesNotExist (Path.Combine (assemblyDirectory, "Mono.Android.scan.empty")); + + var outputDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath); + var apk = Directory.GetFiles (outputDirectory, "*-Signed.apk", SearchOption.AllDirectories).Single (); + var archive = new ArchiveAssemblyHelper (apk, useAssemblyStores: true); + Assert.IsTrue (archive.Exists ($"assemblies/{abi}/Microsoft.CSharp.dll"), + $"Microsoft.CSharp.dll should be packaged in {apk}."); + + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "second build should succeed"); + builder.Output.AssertTargetIsSkipped ("_LinkAssembliesNoShrink"); + } + [Test] [Ignore ("Flaky timing-based test. Disabled while investigating incremental build regressions. See: https://github.com/dotnet/android/issues/11792")] public void BasicApplicationRepetitiveBuild ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime)