From d88489ccd542e39d5a1e6a115bcd1185dd70eb53 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 14 Aug 2026 14:17:45 -0500 Subject: [PATCH] [build] Stabilize Android define constant ordering Append Android define constants at a fixed point after the .NET SDK implicit defines so equivalent project configurations share the same compile dependency hash. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47bf5092-15e1-4b15-9e69-e6d60fa06b85 --- .../GenerateNativeApplicationConfigSources.cs | 8 ++- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 5 +- .../IncrementalBuildTest.cs | 68 +++++++++++++++++++ .../Xamarin.Android.Common.targets | 13 ++-- 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index aeb51c46044..53124157928 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -67,6 +67,7 @@ public class GenerateNativeApplicationConfigSources : AndroidTask public bool EmitLlvmIrComments { get; set; } public bool AndroidEnableAssemblyStoreDecompressionCache { get; set; } + public bool SkipJniAddNativeMethodRegistrationAttributeScan { get; set; } public string? RuntimeConfigBinFilePath { get; set; } public string ProjectRuntimeConfigFilePath { get; set; } = String.Empty; public string? ProjectRuntimeConfigDevFilePath { get; set; } @@ -271,6 +272,9 @@ static bool ShouldSkipAssembly (ITaskItem assembly) } bool haveRuntimeConfigBlob = !String.IsNullOrEmpty (RuntimeConfigBinFilePath) && File.Exists (RuntimeConfigBinFilePath); + bool jniAddNativeMethodRegistrationAttributePresent = + SkipJniAddNativeMethodRegistrationAttributeScan || + NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent; var jniRemappingNativeCodeInfo = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal (ProjectSpecificTaskObjectKey (GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfoKey), RegisteredTaskObjectLifetime.Build); LLVMIR.LlvmIrComposer appConfigAsmGen; @@ -280,7 +284,7 @@ static bool ShouldSkipAssembly (ITaskItem assembly) UsesAssemblyPreload = envBuilder.Parser.UsesAssemblyPreload, AndroidPackageName = AndroidPackageName, PackageNamingPolicy = pnp, - JniAddNativeMethodRegistrationAttributePresent = NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent, + JniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent, NumberOfAssembliesInApk = assemblyCount, BundledAssemblyNameWidth = assemblyNameWidth, NativeLibraries = uniqueNativeLibraries, @@ -307,7 +311,7 @@ static bool ShouldSkipAssembly (ITaskItem assembly) BrokenExceptionTransitions = envBuilder.Parser.BrokenExceptionTransitions, PackageNamingPolicy = pnp, BoundExceptionType = boundExceptionType, - JniAddNativeMethodRegistrationAttributePresent = NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent, + JniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent, HaveRuntimeConfigBlob = haveRuntimeConfigBlob, NumberOfAssembliesInApk = assemblyCount, BundledAssemblyNameWidth = assemblyNameWidth, diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 15a4f675fa1..91cda246795 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -967,7 +967,9 @@ module Xamarin.Android.Tests } [Test] - public void DesignTimeBuildHasAndroidDefines ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) + public void DesignTimeBuildHasAndroidDefines ( + [Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime, + [Values (false, true)] bool disableImplicitFrameworkDefines) { bool isRelease = runtime == AndroidRuntime.NativeAOT; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { @@ -977,6 +979,7 @@ public void DesignTimeBuildHasAndroidDefines ([Values (AndroidRuntime.CoreCLR, A IsRelease = isRelease, }; proj.SetRuntime (runtime); + proj.SetProperty ("DisableImplicitFrameworkDefines", disableImplicitFrameworkDefines.ToString ()); var androidDefines = new List (); for (int i = 1; i <= XABuildConfig.AndroidDefaultTargetDotnetApiLevel.Major; ++i) { androidDefines.Add ($"!__ANDROID_{i}__"); 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 acb5deb214f..43720ce195e 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 @@ -154,6 +154,37 @@ 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."); } + [Test] + public void JniAddNativeMethodRegistrationAttributePresentIsPreserved () + { + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetRuntimeIdentifiers (new [] { "arm64-v8a" }); + proj.SetProperty ("_SkipJniAddNativeMethodRegistrationAttributeScan", "true"); + + using (var builder = CreateApkBuilder ()) { + Assert.IsTrue (builder.Build (proj), "first build should have succeeded."); + AssertJniAddNativeMethodRegistrationAttributePresent (proj, builder); + + string stamp = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "stamp", "_GeneratePackageManagerJava.stamp"); + FileAssert.Exists (stamp); + File.Delete (stamp); + + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true), "second build should have succeeded."); + builder.Output.AssertTargetIsSkipped ("_GenerateJavaStubs"); + builder.Output.AssertTargetIsNotSkipped ("_GeneratePackageManagerJava"); + AssertJniAddNativeMethodRegistrationAttributePresent (proj, builder); + } + } + + void AssertJniAddNativeMethodRegistrationAttributePresent (XamarinAndroidApplicationProject proj, ProjectBuilder builder) + { + string objDirPath = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath); + var envFiles = EnvironmentHelper.GatherEnvironmentFiles (objDirPath, string.Join (";", proj.GetRuntimeIdentifiersAsAbis ()), required: true, runtime: AndroidRuntime.CoreCLR); + var appConfig = (EnvironmentHelper.ApplicationConfig_CoreCLR) EnvironmentHelper.ReadApplicationConfig (envFiles, AndroidRuntime.CoreCLR); + Assert.IsTrue (appConfig.jni_add_native_method_registration_attribute_present, "JNI native method registration should remain enabled."); + } + Dictionary GetJniRemappingSourceTimestamps (XamarinAndroidApplicationProject proj, ProjectBuilder builder) { string objDirPath = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "android"); @@ -793,6 +824,43 @@ public void ManifestMergerIncremental ([Values (AndroidRuntime.CoreCLR, AndroidR } } + [Test] + public void AndroidDefineConstantsAreOrderIndependent () + { + var path = Path.Combine ("temp", TestName); + var lib = new XamarinAndroidLibraryProject { + ProjectName = "Library", + }; + lib.SetProperty ("DisableImplicitFrameworkDefines", "true"); + lib.Imports.Add (new Import ("DefineConstants.targets") { + TextContent = () => """ + + + + $(DefineConstants);TEST_DEFINE + + + + + + +""" + }); + + using (var builder = CreateDllBuilder (Path.Combine (path, lib.ProjectName))) { + builder.Target = "_ResolveMonoAndroidSdks,_AddTestDefineConstant,Compile,_WriteTestDefineConstants"; + Assert.IsTrue (builder.Build (lib), "first library build should have succeeded."); + var firstDefineConstants = builder.Output.GetIntermediaryAsText ("define-constants.txt"); + + builder.Target = "_AddTestDefineConstant,_ResolveMonoAndroidSdks,Compile,_WriteTestDefineConstants"; + Assert.IsTrue (builder.Build (lib, doNotCleanupOnUpdate: true, saveProject: false), "second library build should have succeeded."); + Assert.AreEqual (firstDefineConstants, builder.Output.GetIntermediaryAsText ("define-constants.txt"), + "DefineConstants should not depend on target execution order."); + Assert.IsFalse (builder.LastBuildOutput.Any (line => line.Contains ("Building target \"CoreCompile\" completely.")), + "CoreCompile should not run when define constants are reordered."); + } + } + [Test] public void ProduceReferenceAssembly ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] 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 6a180259a39..2dabfa297ad 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -810,10 +810,6 @@ because xbuild doesn't support framework reference assemblies. - - $(DefineConstants);@(AndroidDefineConstants) - - android.support.multidex.MultiDexApplication @@ -822,6 +818,14 @@ because xbuild doesn't support framework reference assemblies. + + + $(DefineConstants);@(AndroidDefineConstants) + + +