From 7782f6a075adf28be4478d4535b86648c31e8cea Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 16 May 2026 10:27:47 +0200 Subject: [PATCH 1/8] 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/8] 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/8] 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)" > From 21dd7ac91532d2bbcc55817294dfd10747812b23 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 00:35:32 +0200 Subject: [PATCH 4/8] Add JNI remapping incremental build test Verify JNI remapping counts survive a C#-only incremental rebuild so the remap native code target continues to populate the build-scoped task object before environment.ll is regenerated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../IncrementalBuildTest.cs | 51 +++++++++++++++++++ 1 file changed, 51 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 86ffb5bf8a4..94d44d7d9a0 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 @@ -102,6 +102,57 @@ public class Foo { } } + [Test] + public void JniRemappingCountsSurviveIncrementalBuild () + { + const AndroidRuntime runtime = AndroidRuntime.CoreCLR; + const bool isRelease = true; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + OtherBuildItems = { + new AndroidItem._AndroidRemapMembers ("Remap.xml") { + Encoding = Encoding.UTF8, + TextContent = () => """ + + + + +""", + }, + }, + }; + proj.SetRuntime (runtime); + + using (var b = CreateApkBuilder ()) { + Assert.IsTrue (b.Build (proj), "first build failed"); + AssertJniRemappingCounts (proj, b, expectedTypeCount: 1, expectedMethodCount: 1); + + proj.MainActivity += Environment.NewLine + "// Force an incremental C# rebuild."; + proj.Touch ("MainActivity.cs"); + Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "second build failed"); + b.Output.AssertTargetIsNotSkipped ("_GenerateAndroidRemapNativeCode"); + AssertJniRemappingCounts (proj, b, expectedTypeCount: 1, expectedMethodCount: 1); + } + } + + void AssertJniRemappingCounts (XamarinAndroidApplicationProject proj, ProjectBuilder builder, uint expectedTypeCount, uint expectedMethodCount) + { + string objDirPath = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath); + var envFiles = EnvironmentHelper.GatherEnvironmentFiles (objDirPath, "arm64-v8a;x86_64", required: true, runtime: AndroidRuntime.CoreCLR); + var appConfig = (EnvironmentHelper.ApplicationConfig_CoreCLR) EnvironmentHelper.ReadApplicationConfig (envFiles, AndroidRuntime.CoreCLR); + Assert.AreEqual (expectedTypeCount, appConfig.jni_remapping_replacement_type_count, "jni_remapping_replacement_type_count should be preserved."); + Assert.AreEqual (expectedMethodCount, appConfig.jni_remapping_replacement_method_index_entry_count, "jni_remapping_replacement_method_index_entry_count should be preserved."); + } + [Test] public void CheckNothingIsDeletedByIncrementalClean ([Values] bool enableMultiDex, [Values] AndroidRuntime runtime) { From df9f24b9de3228813e8f5132957034ad4dd0c5ff Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 06:42:29 +0200 Subject: [PATCH 5/8] Fix JNI remapping regression test assertion Assert the observable generated environment counts instead of requiring a specific target execution detail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs | 1 - 1 file changed, 1 deletion(-) 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 94d44d7d9a0..a5184bfe5b1 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 @@ -139,7 +139,6 @@ public void JniRemappingCountsSurviveIncrementalBuild () proj.MainActivity += Environment.NewLine + "// Force an incremental C# rebuild."; proj.Touch ("MainActivity.cs"); Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "second build failed"); - b.Output.AssertTargetIsNotSkipped ("_GenerateAndroidRemapNativeCode"); AssertJniRemappingCounts (proj, b, expectedTypeCount: 1, expectedMethodCount: 1); } } From 734eab39f0a6bc20438d37fbfaa5a43a71a2f3be Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 21 May 2026 07:02:06 +0200 Subject: [PATCH 6/8] Keep empty JNI remap target incremental Only the non-empty remapping target needs to run every build to register non-zero JNI remapping counts. The empty target may remain incremental because skipped task data still correctly results in zero generated environment counts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Common.targets | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index a093e2e2336..817bd682004 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1631,7 +1631,9 @@ because xbuild doesn't support framework reference assemblies. + Condition=" '@(_AndroidRemapMembers->Count())' == '0' " + Inputs="$(_AndroidBuildPropertiesCache);@(_AndroidMSBuildAllProjects)" + Outputs="@(_AndroidRemapAssemblySource)"> Date: Thu, 21 May 2026 09:31:35 +0200 Subject: [PATCH 7/8] Avoid touching unchanged JNI remap sources Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../IncrementalBuildTest.cs | 25 +++++++++++++++++++ .../Xamarin.Android.Common.targets | 1 - 2 files changed, 25 insertions(+), 1 deletion(-) 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 a5184bfe5b1..f96dc8783cd 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 @@ -135,11 +135,13 @@ public void JniRemappingCountsSurviveIncrementalBuild () using (var b = CreateApkBuilder ()) { Assert.IsTrue (b.Build (proj), "first build failed"); AssertJniRemappingCounts (proj, b, expectedTypeCount: 1, expectedMethodCount: 1); + var remapSourceTimestamps = GetJniRemappingSourceTimestamps (proj, b); proj.MainActivity += Environment.NewLine + "// Force an incremental C# rebuild."; proj.Touch ("MainActivity.cs"); Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "second build failed"); AssertJniRemappingCounts (proj, b, expectedTypeCount: 1, expectedMethodCount: 1); + AssertJniRemappingSourceTimestamps (remapSourceTimestamps); } } @@ -152,6 +154,29 @@ void AssertJniRemappingCounts (XamarinAndroidApplicationProject proj, ProjectBui Assert.AreEqual (expectedMethodCount, appConfig.jni_remapping_replacement_method_index_entry_count, "jni_remapping_replacement_method_index_entry_count should be preserved."); } + Dictionary GetJniRemappingSourceTimestamps (XamarinAndroidApplicationProject proj, ProjectBuilder builder) + { + string objDirPath = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "android"); + var timestamps = new Dictionary (StringComparer.Ordinal); + foreach (string abi in new [] { "arm64-v8a", "x86_64" }) { + string path = Path.Combine (objDirPath, $"jni_remap.{abi}.ll"); + FileAssert.Exists (path); + timestamps.Add (path, File.GetLastWriteTimeUtc (path)); + } + return timestamps; + } + + void AssertJniRemappingSourceTimestamps (Dictionary expectedTimestamps) + { + foreach (var expectedTimestamp in expectedTimestamps) { + Assert.AreEqual ( + expectedTimestamp.Value, + File.GetLastWriteTimeUtc (expectedTimestamp.Key), + $"{expectedTimestamp.Key} should not be touched when regenerated with unchanged contents." + ); + } + } + [Test] public void CheckNothingIsDeletedByIncrementalClean ([Values] bool enableMultiDex, [Values] AndroidRuntime runtime) { diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 817bd682004..603c7243705 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1658,7 +1658,6 @@ because xbuild doesn't support framework reference assemblies. RemappingXmlFilePath="$(_XARemapMembersFilePath)" SupportedAbis="@(_BuildTargetAbis)" /> - Date: Fri, 22 May 2026 12:26:03 +0200 Subject: [PATCH 8/8] Align JNI remap native code inputs Run the JNI remap native-code target when the package-manager native config inputs change, and stamp target execution separately from the generated LLVM IR files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Common.targets | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 603c7243705..fd444a89512 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1646,18 +1646,22 @@ because xbuild doesn't support framework reference assemblies. <_GenerateAndroidRemapNativeCodeDependsOn> _ConvertAndroidMamMappingFileToXml; _CollectAndroidRemapMembers; - _PrepareAndroidRemapNativeAssemblySources + _PrepareAndroidRemapNativeAssemblySources; + _GetGeneratePackageManagerJavaInputs + Condition=" '@(_AndroidRemapMembers->Count())' != '0' " + Inputs="@(_GeneratePackageManagerJavaInputs);$(_XARemapMembersFilePath)" + Outputs="$(_AndroidStampDirectory)_GenerateAndroidRemapNativeCode.stamp"> + <_GeneratePackageManagerJavaInputs Include="@(_GenerateJavaStubsInputs)" /> + <_GeneratePackageManagerJavaInputs Include="$(_XARemapMembersFilePath)" Condition=" '@(_AndroidRemapMembers->Count())' != '0' " />