From 7782f6a075adf28be4478d4535b86648c31e8cea Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 16 May 2026 10:27:47 +0200 Subject: [PATCH 1/3] Fix JNI remapping counts lost on incremental builds GenerateJniRemappingNativeCode registers JNI remapping counts (type and method replacement counts) as an in-memory MSBuild task object. On incremental builds where the remap target is skipped (outputs up to date) but _GeneratePackageManagerJava re-runs (e.g. due to assembly changes), GenerateNativeApplicationConfigSources finds no registered task object and writes zero counts into environment.ll. This silently disables JNI method remapping at runtime (jniRemappingInUse = false). Fix by persisting the counts to a file (jni_remapping_info.txt) alongside the generated jni_remap.ll sources. GenerateNativeApplicationConfigSources falls back to reading this file when the task object is not available. The info file is also added to both the remap targets' Outputs and the _GeneratePackageManagerJava target's Inputs, ensuring proper incremental build invalidation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateJniRemappingNativeCode.cs | 46 ++++++++++++++++++- .../GenerateNativeApplicationConfigSources.cs | 8 ++++ .../Xamarin.Android.Common.targets | 12 ++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs index 7da71df76e3..9ecabe6a8d4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Globalization; using System.IO; using System.Collections.Generic; using System.Xml; @@ -37,6 +38,9 @@ public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMeth [Required] public string [] SupportedAbis { get; set; } = []; + [Required] + public string JniRemappingInfoFilePath { get; set; } = ""; + public bool GenerateEmptyCode { get; set; } public override bool RunTask () @@ -94,11 +98,51 @@ void Generate (JniRemappingAssemblyGenerator jniRemappingComposer, int typeRepla } } + int methodIndexEntryCount = jniRemappingComposer.ReplacementMethodIndexEntryCount; + BuildEngine4.RegisterTaskObjectAssemblyLocal ( ProjectSpecificTaskObjectKey (JniRemappingNativeCodeInfoKey), - new JniRemappingNativeCodeInfo (typeReplacementsCount, jniRemappingComposer.ReplacementMethodIndexEntryCount), + new JniRemappingNativeCodeInfo (typeReplacementsCount, methodIndexEntryCount), RegisteredTaskObjectLifetime.Build ); + + WriteInfoFile (typeReplacementsCount, methodIndexEntryCount); + } + + void WriteInfoFile (int typeReplacementsCount, int methodIndexEntryCount) + { + string contents = string.Format ( + CultureInfo.InvariantCulture, + "version=1\nreplacement_type_count={0}\nreplacement_method_index_entry_count={1}\n", + typeReplacementsCount, + methodIndexEntryCount); + Files.CopyIfStringChanged (contents, JniRemappingInfoFilePath); + } + + internal static JniRemappingNativeCodeInfo? ReadInfoFile (string path, TaskLoggingHelper log) + { + if (!File.Exists (path)) { + log.LogError ($"JNI remapping info file '{path}' not found. A clean rebuild may be required."); + return null; + } + + int typeCount = -1; + int methodCount = -1; + + foreach (string line in File.ReadLines (path)) { + if (line.StartsWith ("replacement_type_count=", StringComparison.Ordinal)) { + typeCount = int.Parse (line.Substring ("replacement_type_count=".Length), NumberStyles.None, CultureInfo.InvariantCulture); + } else if (line.StartsWith ("replacement_method_index_entry_count=", StringComparison.Ordinal)) { + methodCount = int.Parse (line.Substring ("replacement_method_index_entry_count=".Length), NumberStyles.None, CultureInfo.InvariantCulture); + } + } + + if (typeCount < 0 || methodCount < 0) { + log.LogError ($"JNI remapping info file '{path}' is malformed."); + return null; + } + + return new JniRemappingNativeCodeInfo (typeCount, methodCount); } void ReadXml (XmlReader reader, List typeReplacements, List methodReplacements, string remappingXmlFilePath) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index 92aab238757..81938b6d8e2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -74,6 +74,7 @@ public class GenerateNativeApplicationConfigSources : AndroidTask public string? AndroidSequencePointsMode { get; set; } public bool EnableSGenConcurrent { get; set; } public string? CustomBundleConfigFile { get; set; } + public string? JniRemappingInfoFilePath { get; set; } bool _Debug { get { @@ -252,6 +253,13 @@ public override bool RunTask () bool haveRuntimeConfigBlob = !String.IsNullOrEmpty (RuntimeConfigBinFilePath) && File.Exists (RuntimeConfigBinFilePath); var jniRemappingNativeCodeInfo = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal (ProjectSpecificTaskObjectKey (GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfoKey), RegisteredTaskObjectLifetime.Build); + + // When the JNI remapping target is skipped (incremental build), the registered task object + // will be null. Fall back to reading the persisted info file written by a previous build. + if (jniRemappingNativeCodeInfo == null && !JniRemappingInfoFilePath.IsNullOrEmpty ()) { + jniRemappingNativeCodeInfo = GenerateJniRemappingNativeCode.ReadInfoFile (JniRemappingInfoFilePath, Log); + } + LLVMIR.LlvmIrComposer appConfigAsmGen; if (TargetsCLR) { diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 5674bb72ff2..aee9b00cc1d 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1619,6 +1619,10 @@ because xbuild doesn't support framework reference assemblies. + + <_JniRemappingInfoFilePath>$(IntermediateOutputPath)android\jni_remapping_info.txt + + + Outputs="@(_AndroidRemapAssemblySource);$(_JniRemappingInfoFilePath)"> @@ -1654,11 +1659,12 @@ because xbuild doesn't support framework reference assemblies. DependsOnTargets="$(_GenerateAndroidRemapNativeCodeDependsOn)" Condition=" '@(_AndroidRemapMembers->Count())' != '0' " Inputs="$(_AndroidBuildPropertiesCache);@(_AndroidMSBuildAllProjects);$(_XARemapMembersFilePath)" - Outputs="@(_AndroidRemapAssemblySource)"> + Outputs="@(_AndroidRemapAssemblySource);$(_JniRemappingInfoFilePath)"> @@ -1696,6 +1702,7 @@ because xbuild doesn't support framework reference assemblies. <_GeneratePackageManagerJavaInputs Include="@(_GenerateJavaStubsInputs)" /> + <_GeneratePackageManagerJavaInputs Include="$(_JniRemappingInfoFilePath)" Condition=" Exists('$(_JniRemappingInfoFilePath)') " /> @@ -1768,6 +1775,7 @@ because xbuild doesn't support framework reference assemblies. TargetsCLR="$(_AndroidUseCLR)" AndroidRuntime="$(_AndroidRuntime)" ProjectRuntimeConfigFilePath="$(ProjectRuntimeConfigFilePath)" + JniRemappingInfoFilePath="$(_JniRemappingInfoFilePath)" > From 82e82576bf2caef58cf9341da6ca9e207b27a62a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 16 May 2026 15:48:15 +0200 Subject: [PATCH 2/3] Add test for JNI remapping info file round-trip Verifies that building a project with JNI remapping produces a jni_remapping_info.txt file with the correct type and method replacement counts, and that these counts match the values in the generated environment.ll. Without the fix in the previous commit, this file would not be created and the test would fail at the file existence assertion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest3.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs index 957b506b6ce..16bea7c0582 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs @@ -283,4 +283,45 @@ void NativeLibraryJniPreload_VerifyLibs (List? al return EnvironmentHelper.ReadJniPreloads (envFiles, numberOfDsoCacheEntries, runtime); } + + [Test] + public void JniRemappingInfoFileRoundTrip () + { + var remapXml = @" + + +"; + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + OtherBuildItems = { + new AndroidItem._AndroidRemapMembers ("Remap.xml") { + Encoding = System.Text.Encoding.UTF8, + TextContent = () => remapXml, + }, + }, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + // Verify the jni_remapping_info.txt file was written with correct counts + var infoFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "jni_remapping_info.txt")); + Assert.IsTrue (File.Exists (infoFile), $"jni_remapping_info.txt should exist at {infoFile}"); + + var contents = File.ReadAllText (infoFile); + StringAssert.Contains ("replacement_type_count=1", contents, "Should have 1 type replacement."); + StringAssert.Contains ("replacement_method_index_entry_count=1", contents, "Should have 1 method replacement entry."); + + // Verify environment.ll has the matching non-zero counts + var envFiles = EnvironmentHelper.GatherEnvironmentFiles (builder.Output.GetIntermediaryPath (""), "arm64-v8a", required: true, AndroidRuntime.CoreCLR); + var appConfig = (EnvironmentHelper.ApplicationConfig_CoreCLR) EnvironmentHelper.ReadApplicationConfig (envFiles, AndroidRuntime.CoreCLR); + Assert.AreEqual (1u, appConfig.jni_remapping_replacement_type_count, "jni_remapping_replacement_type_count should be 1."); + Assert.AreEqual (1u, appConfig.jni_remapping_replacement_method_index_entry_count, "jni_remapping_replacement_method_index_entry_count should be 1."); + } } From 89fe8fa455bf6de1ef3189d7021e27239d5f3ace Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 17 May 2026 00:25:45 +0200 Subject: [PATCH 3/3] Simplify: remove Inputs/Outputs from remap targets instead of file-based fallback The remap targets are very fast (small XML parse + LL file write) and already use CopyIfStreamChanged, so always running them has negligible cost. This ensures the registered task object is always available for GenerateNativeApplicationConfigSources, eliminating the incremental build bug without any new files or fallback logic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateJniRemappingNativeCode.cs | 46 +------------------ .../GenerateNativeApplicationConfigSources.cs | 8 ---- .../Xamarin.Android.Build.Tests/BuildTest3.cs | 41 ----------------- .../Xamarin.Android.Common.targets | 16 +------ 4 files changed, 3 insertions(+), 108 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs index 9ecabe6a8d4..7da71df76e3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJniRemappingNativeCode.cs @@ -1,7 +1,6 @@ #nullable enable using System; -using System.Globalization; using System.IO; using System.Collections.Generic; using System.Xml; @@ -38,9 +37,6 @@ public JniRemappingNativeCodeInfo (int replacementTypeCount, int replacementMeth [Required] public string [] SupportedAbis { get; set; } = []; - [Required] - public string JniRemappingInfoFilePath { get; set; } = ""; - public bool GenerateEmptyCode { get; set; } public override bool RunTask () @@ -98,51 +94,11 @@ void Generate (JniRemappingAssemblyGenerator jniRemappingComposer, int typeRepla } } - int methodIndexEntryCount = jniRemappingComposer.ReplacementMethodIndexEntryCount; - BuildEngine4.RegisterTaskObjectAssemblyLocal ( ProjectSpecificTaskObjectKey (JniRemappingNativeCodeInfoKey), - new JniRemappingNativeCodeInfo (typeReplacementsCount, methodIndexEntryCount), + new JniRemappingNativeCodeInfo (typeReplacementsCount, jniRemappingComposer.ReplacementMethodIndexEntryCount), RegisteredTaskObjectLifetime.Build ); - - WriteInfoFile (typeReplacementsCount, methodIndexEntryCount); - } - - void WriteInfoFile (int typeReplacementsCount, int methodIndexEntryCount) - { - string contents = string.Format ( - CultureInfo.InvariantCulture, - "version=1\nreplacement_type_count={0}\nreplacement_method_index_entry_count={1}\n", - typeReplacementsCount, - methodIndexEntryCount); - Files.CopyIfStringChanged (contents, JniRemappingInfoFilePath); - } - - internal static JniRemappingNativeCodeInfo? ReadInfoFile (string path, TaskLoggingHelper log) - { - if (!File.Exists (path)) { - log.LogError ($"JNI remapping info file '{path}' not found. A clean rebuild may be required."); - return null; - } - - int typeCount = -1; - int methodCount = -1; - - foreach (string line in File.ReadLines (path)) { - if (line.StartsWith ("replacement_type_count=", StringComparison.Ordinal)) { - typeCount = int.Parse (line.Substring ("replacement_type_count=".Length), NumberStyles.None, CultureInfo.InvariantCulture); - } else if (line.StartsWith ("replacement_method_index_entry_count=", StringComparison.Ordinal)) { - methodCount = int.Parse (line.Substring ("replacement_method_index_entry_count=".Length), NumberStyles.None, CultureInfo.InvariantCulture); - } - } - - if (typeCount < 0 || methodCount < 0) { - log.LogError ($"JNI remapping info file '{path}' is malformed."); - return null; - } - - return new JniRemappingNativeCodeInfo (typeCount, methodCount); } void ReadXml (XmlReader reader, List typeReplacements, List methodReplacements, string remappingXmlFilePath) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index 81938b6d8e2..92aab238757 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -74,7 +74,6 @@ public class GenerateNativeApplicationConfigSources : AndroidTask public string? AndroidSequencePointsMode { get; set; } public bool EnableSGenConcurrent { get; set; } public string? CustomBundleConfigFile { get; set; } - public string? JniRemappingInfoFilePath { get; set; } bool _Debug { get { @@ -253,13 +252,6 @@ public override bool RunTask () bool haveRuntimeConfigBlob = !String.IsNullOrEmpty (RuntimeConfigBinFilePath) && File.Exists (RuntimeConfigBinFilePath); var jniRemappingNativeCodeInfo = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal (ProjectSpecificTaskObjectKey (GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfoKey), RegisteredTaskObjectLifetime.Build); - - // When the JNI remapping target is skipped (incremental build), the registered task object - // will be null. Fall back to reading the persisted info file written by a previous build. - if (jniRemappingNativeCodeInfo == null && !JniRemappingInfoFilePath.IsNullOrEmpty ()) { - jniRemappingNativeCodeInfo = GenerateJniRemappingNativeCode.ReadInfoFile (JniRemappingInfoFilePath, Log); - } - LLVMIR.LlvmIrComposer appConfigAsmGen; if (TargetsCLR) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs index 16bea7c0582..957b506b6ce 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs @@ -283,45 +283,4 @@ void NativeLibraryJniPreload_VerifyLibs (List? al return EnvironmentHelper.ReadJniPreloads (envFiles, numberOfDsoCacheEntries, runtime); } - - [Test] - public void JniRemappingInfoFileRoundTrip () - { - var remapXml = @" - - -"; - - var proj = new XamarinAndroidApplicationProject { - IsRelease = true, - OtherBuildItems = { - new AndroidItem._AndroidRemapMembers ("Remap.xml") { - Encoding = System.Text.Encoding.UTF8, - TextContent = () => remapXml, - }, - }, - }; - proj.SetRuntime (AndroidRuntime.CoreCLR); - - using var builder = CreateApkBuilder (); - Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); - - // Verify the jni_remapping_info.txt file was written with correct counts - var infoFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "jni_remapping_info.txt")); - Assert.IsTrue (File.Exists (infoFile), $"jni_remapping_info.txt should exist at {infoFile}"); - - var contents = File.ReadAllText (infoFile); - StringAssert.Contains ("replacement_type_count=1", contents, "Should have 1 type replacement."); - StringAssert.Contains ("replacement_method_index_entry_count=1", contents, "Should have 1 method replacement entry."); - - // Verify environment.ll has the matching non-zero counts - var envFiles = EnvironmentHelper.GatherEnvironmentFiles (builder.Output.GetIntermediaryPath (""), "arm64-v8a", required: true, AndroidRuntime.CoreCLR); - var appConfig = (EnvironmentHelper.ApplicationConfig_CoreCLR) EnvironmentHelper.ReadApplicationConfig (envFiles, AndroidRuntime.CoreCLR); - Assert.AreEqual (1u, appConfig.jni_remapping_replacement_type_count, "jni_remapping_replacement_type_count should be 1."); - Assert.AreEqual (1u, appConfig.jni_remapping_replacement_method_index_entry_count, "jni_remapping_replacement_method_index_entry_count should be 1."); - } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index aee9b00cc1d..b741609b216 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1619,10 +1619,6 @@ because xbuild doesn't support framework reference assemblies. - - <_JniRemappingInfoFilePath>$(IntermediateOutputPath)android\jni_remapping_info.txt - - + Condition=" '@(_AndroidRemapMembers->Count())' == '0' "> @@ -1657,14 +1650,11 @@ because xbuild doesn't support framework reference assemblies. + Condition=" '@(_AndroidRemapMembers->Count())' != '0' "> @@ -1702,7 +1692,6 @@ because xbuild doesn't support framework reference assemblies. <_GeneratePackageManagerJavaInputs Include="@(_GenerateJavaStubsInputs)" /> - <_GeneratePackageManagerJavaInputs Include="$(_JniRemappingInfoFilePath)" Condition=" Exists('$(_JniRemappingInfoFilePath)') " /> @@ -1775,7 +1764,6 @@ because xbuild doesn't support framework reference assemblies. TargetsCLR="$(_AndroidUseCLR)" AndroidRuntime="$(_AndroidRuntime)" ProjectRuntimeConfigFilePath="$(ProjectRuntimeConfigFilePath)" - JniRemappingInfoFilePath="$(_JniRemappingInfoFilePath)" >