From 7f71334b937ba318f3b5bf9a022d65b115f664a7 Mon Sep 17 00:00:00 2001 From: David Nguyen <87228593+davidnguyen-tech@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:19:01 +0200 Subject: [PATCH 1/6] [Build] Add per-file up-to-date check in CompileNativeAssembly When _CompileNativeAssemblySources runs, it recompiles ALL .ll files even if only some have changed. Add a per-file timestamp check in RunAssembler() so that if the output .o is newer than the input .ll, that file is skipped. This complements the upcoming target split by ensuring unchanged type map files are not recompiled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/CompileNativeAssembly.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/CompileNativeAssembly.cs b/src/Xamarin.Android.Build.Tasks/Tasks/CompileNativeAssembly.cs index 49402865722..6bc20afbf9f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/CompileNativeAssembly.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/CompileNativeAssembly.cs @@ -22,6 +22,7 @@ sealed class Config public string? AssemblerPath; public string? AssemblerOptions; public string? InputSource; + public string? OutputFile; } [Required] @@ -43,6 +44,14 @@ public override System.Threading.Tasks.Task RunTaskAsync () void RunAssembler (Config config) { + if (config.OutputFile is not null && File.Exists (config.OutputFile)) { + string sourceFile = Path.Combine (WorkingDirectory, Path.GetFileName (config.InputSource)); + if (File.Exists (sourceFile) && File.GetLastWriteTimeUtc (config.OutputFile) >= File.GetLastWriteTimeUtc (sourceFile)) { + LogDebugMessage ($"[LLVM llc] Skipping '{Path.GetFileName (config.InputSource)}' because '{Path.GetFileName (config.OutputFile)}' is up to date"); + return; + } + } + var stdout_completed = new ManualResetEvent (false); var stderr_completed = new ManualResetEvent (false); var psi = new ProcessStartInfo () { @@ -118,10 +127,13 @@ IEnumerable GetAssemblerConfigs () string executableDir = Path.GetDirectoryName (llcPath); string executableName = MonoAndroidHelper.GetExecutablePath (executableDir, Path.GetFileName (llcPath)); + string outputFilePath = Path.Combine (WorkingDirectory, sourceFile.Replace (".ll", ".o")); + yield return new Config { InputSource = item.ItemSpec, AssemblerPath = Path.Combine (executableDir, executableName), AssemblerOptions = $"{assemblerOptions} -o={outputFile} {QuoteFileName (sourceFile)}", + OutputFile = outputFilePath, }; } } From 996b2772cc87676cc31ce2861870b9e9e44fecf4 Mon Sep 17 00:00:00 2001 From: David Nguyen <87228593+davidnguyen-tech@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:19:08 +0200 Subject: [PATCH 2/6] [Build] Externalize JniAddNativeMethodRegistrationAttributePresent as MSBuild property Add [Output] JniAddNativeMethodRegistrationAttributePresent to GenerateTypeMappings and a matching input property on GenerateNativeApplicationConfigSources. This decouples the cross-target static state so the value can be persisted to a cache file and read back when _GenerateTypeMappings is skipped on incremental builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/GenerateNativeApplicationConfigSources.cs | 5 +++-- .../Tasks/GenerateTypeMappings.cs | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index a17945d8cee..c0249a1a4a1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -57,6 +57,7 @@ public class GenerateNativeApplicationConfigSources : AndroidTask public bool EnableMarshalMethods { get; set; } public bool EnableManagedMarshalMethodsLookup { get; set; } + public bool JniAddNativeMethodRegistrationAttributePresent { get; set; } public string? RuntimeConfigBinFilePath { get; set; } public string ProjectRuntimeConfigFilePath { get; set; } = String.Empty; public string? BoundExceptionType { get; set; } @@ -251,7 +252,7 @@ public override bool RunTask () UsesAssemblyPreload = envBuilder.Parser.UsesAssemblyPreload, AndroidPackageName = AndroidPackageName, PackageNamingPolicy = pnp, - JniAddNativeMethodRegistrationAttributePresent = NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent, + JniAddNativeMethodRegistrationAttributePresent = JniAddNativeMethodRegistrationAttributePresent, NumberOfAssembliesInApk = assemblyCount, BundledAssemblyNameWidth = assemblyNameWidth, NativeLibraries = uniqueNativeLibraries, @@ -277,7 +278,7 @@ public override bool RunTask () 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/Tasks/GenerateTypeMappings.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs index 70a6a60f5a9..beacfd442a3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs @@ -31,6 +31,9 @@ public class GenerateTypeMappings : AndroidTask [Required] public string IntermediateOutputDirectory { get; set; } = ""; + [Output] + public bool JniAddNativeMethodRegistrationAttributePresent { get; set; } + public bool SkipJniAddNativeMethodRegistrationAttributeScan { get; set; } [Required] @@ -102,6 +105,7 @@ void GenerateTypeMap (AndroidTargetArch arch, List assemblies) // Set for use by task later NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent = state.JniAddNativeMethodRegistrationAttributePresent; + JniAddNativeMethodRegistrationAttributePresent = NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent; AddOutputTypeMaps (tmg, state.TargetArch); } @@ -126,8 +130,10 @@ void GenerateAllTypeMappingsFromNativeState (bool useMarshalMethods) throw new InvalidOperationException ($"Internal error: no native code generator state defined"); // Set for use by task later - if (useMarshalMethods) + if (useMarshalMethods) { NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent = templateCodeGenState.JniAddNativeMethodRegistrationAttributePresent; + JniAddNativeMethodRegistrationAttributePresent = NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent; + } } void GenerateTypeMapFromNativeState (NativeCodeGenState state, bool useMarshalMethods) From 57b23741983d4c5ae56f8455f9bd50b01646eddb Mon Sep 17 00:00:00 2001 From: David Nguyen <87228593+davidnguyen-tech@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:19:20 +0200 Subject: [PATCH 3/6] [Build] Add input gatherer sub-targets for _GenerateJavaStubs split Add four input gatherer targets that compute narrowed input sets for the upcoming sub-targets of _GenerateJavaStubs: - _GetGenerateJavaCallableWrappersInputs: .jlo.xml sidecar files - _GetGenerateJavaStubsCoreInputs: assembly hash, manifest, environment - _GetGenerateTypeMappingsInputs: .typemap.xml sidecar files - _GetGenerateAndroidManifestInputs: manifest template, environment Each sub-target has narrower inputs than the monolithic target, enabling MSBuild to skip unchanged sub-tasks on incremental builds. The core inputs include manifest and environment files to ensure NativeCodeGenState is always registered when downstream targets need it. Also wire the JNI registration attribute cache file to GenerateNativeApplicationConfigSources via ReadLinesFromFile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Common.targets | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index e2cd6eeca42..2c244a226f0 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1493,6 +1493,48 @@ because xbuild doesn't support framework reference assemblies. + + + <_GenerateJavaCallableWrappersInputs Include="@(_AndroidMSBuildAllProjects)" /> + <_GenerateJavaCallableWrappersInputs Include="$(_AndroidBuildPropertiesCache)" /> + <_GenerateJavaCallableWrappersInputs Include="@(_ResolvedAssemblies->'$([System.IO.Path]::ChangeExtension("%(Identity)", ".jlo.xml"))')" /> + + + + + + <_GenerateJavaStubsCoreInputs Include="@(_AndroidMSBuildAllProjects)" /> + <_GenerateJavaStubsCoreInputs Include="$(_ResolvedUserAssembliesHashFile)" /> + <_GenerateJavaStubsCoreInputs Include="@(_ResolvedUserMonoAndroidAssemblies)" /> + <_GenerateJavaStubsCoreInputs Include="$(_AndroidBuildPropertiesCache)" /> + <_GenerateJavaStubsCoreInputs Include="$(_AndroidManifestAbs)" /> + <_GenerateJavaStubsCoreInputs Include="@(AndroidEnvironment);@(LibraryEnvironments)" Condition=" '$(_AndroidFastDeployEnvironmentFiles)' != 'true' " /> + + + + + + <_GenerateTypeMappingsInputs Include="@(_AndroidMSBuildAllProjects)" /> + <_GenerateTypeMappingsInputs Include="$(_AndroidBuildPropertiesCache)" /> + <_GenerateTypeMappingsInputs Include="@(_ResolvedAssemblies->'$([System.IO.Path]::ChangeExtension("%(Identity)", ".typemap.xml"))')" /> + <_GenerateTypeMappingsInputs + Condition=" '$(_AndroidUseMarshalMethods)' == 'true' Or '$(_AndroidJLOCheckedBuild)' == 'true' " + Include="$(_AndroidStampDirectory)_GenerateJavaStubsCore.stamp" /> + + + + + + <_GenerateAndroidManifestInputs Include="@(_AndroidMSBuildAllProjects)" /> + <_GenerateAndroidManifestInputs Include="$(_AndroidManifestAbs)" /> + <_GenerateAndroidManifestInputs Include="$(_AndroidBuildPropertiesCache)" /> + <_GenerateAndroidManifestInputs Include="@(_ResolvedUserMonoAndroidAssemblies)" /> + <_GenerateAndroidManifestInputs Include="@(AndroidEnvironment);@(LibraryEnvironments)" Condition=" '$(_AndroidFastDeployEnvironmentFiles)' != 'true' " /> + + <_GenerateAndroidManifestInputs Include="$(_AndroidStampDirectory)_GenerateJavaStubsCore.stamp" /> + + + + + + + + From 2b94be14e2e17d71430d018af6e9f7426a3cd2a4 Mon Sep 17 00:00:00 2001 From: David Nguyen <87228593+davidnguyen-tech@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:21:00 +0200 Subject: [PATCH 4/6] [Build] Split _GenerateJavaStubs into independent sub-targets Split the monolithic _GenerateJavaStubs target into four independent sub-targets, each with their own Inputs/Outputs for incrementality: - _GenerateJavaCallableWrappers: JCW generation + ACW map - _GenerateJavaStubsCore: code gen state + marshal method rewriting - _GenerateTypeMappings: type map .ll file generation - _GenerateAndroidManifest: manifest + provider source generation The original _GenerateJavaStubs target is preserved as a no-op wrapper with DependsOnTargets for backward compatibility. Sub-targets explicitly depend on _GetGenerateJavaStubsInputs to ensure @(_EnvironmentFiles) is populated. The JNI registration attribute flag is persisted to a cache file for downstream targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 89 ++++++++++++++----- 1 file changed, 69 insertions(+), 20 deletions(-) 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 4be55630c91..2ce0fe4f1db 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 @@ -24,25 +24,21 @@ _PrepareNativeAssemblySources; $(_AfterPrepareAssemblies); _GetGenerateJavaStubsInputs; + _GetGenerateJavaCallableWrappersInputs; + _GetGenerateJavaStubsCoreInputs; + _GetGenerateTypeMappingsInputs; + _GetGenerateAndroidManifestInputs; + _GenerateJavaCallableWrappers; + _GenerateJavaStubsCore; + _GenerateTypeMappings; + _GenerateAndroidManifest; - - - - <_ManifestOutput Condition=" '$(AndroidManifestMerger)' == 'legacy' ">$(IntermediateOutputPath)android\AndroidManifest.xml - <_ManifestOutput Condition=" '$(AndroidManifestMerger)' != 'legacy' ">$(IntermediateOutputPath)AndroidManifest.xml - <_LinkingEnabled Condition=" '$(AndroidLinkMode)' != 'None'">True - <_LinkingEnabled Condition=" '$(AndroidLinkMode)' == 'None'">False - <_HaveMultipleRIDs Condition=" '$(RuntimeIdentifiers)' != '' ">True - <_HaveMultipleRIDs Condition=" '$(RuntimeIdentifiers)' == '' ">False - - - <_MergedManifestDocuments Condition=" '$(AndroidManifestMerger)' == 'legacy' " Include="@(ExtractedManifestDocuments)" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ManifestOutput Condition=" '$(AndroidManifestMerger)' == 'legacy' ">$(IntermediateOutputPath)android\AndroidManifest.xml + <_ManifestOutput Condition=" '$(AndroidManifestMerger)' != 'legacy' ">$(IntermediateOutputPath)AndroidManifest.xml + + + <_MergedManifestDocuments Condition=" '$(AndroidManifestMerger)' == 'legacy' " Include="@(ExtractedManifestDocuments)" /> + + - - - - + + + + + From 3c25cea339b2ae201aef94de6b88de9b7b24b4d3 Mon Sep 17 00:00:00 2001 From: David Nguyen <87228593+davidnguyen-tech@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:21:08 +0200 Subject: [PATCH 5/6] [Build] Add stub sub-targets for Trimmable typemap path Add matching stub sub-targets in the Trimmable typemap targets file to maintain consistency with the LlvmIr path. All sub-targets are stubs that skip immediately, matching the existing behavior where _GenerateJavaStubs was a single stub target. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) 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 51cae6aab85..3b2162ce8d4 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 @@ -17,12 +17,42 @@ tasks needed by all typemap paths, but they currently depend on NativeCodeGenState from the Cecil-based GenerateJavaStubs task. Extracting them into a shared target requires decoupling from NativeCodeGenState first. See #10807. --> + + + + + + + + + + + + + + + + + + + + - - + DependsOnTargets="_SetLatestTargetFrameworkVersion;_PrepareAssemblies;_GetGenerateJavaStubsInputs;_GenerateJavaCallableWrappers;_GenerateJavaStubsCore;_GenerateTypeMappings;_GenerateAndroidManifest"> + From 810d40c12767fe29ac67501996348b6d4e136c22 Mon Sep 17 00:00:00 2001 From: David Nguyen <87228593+davidnguyen-tech@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:21:14 +0200 Subject: [PATCH 6/6] [Build] Add incrementality tests for _GenerateJavaStubs sub-targets Update the existing GenerateJavaStubsAndAssembly test to verify the new sub-targets follow the same incrementality rules. Add a new GenerateJavaStubsSubTargetIncrementality test verifying all sub-targets skip on no-change rebuilds. Update skip assertions to check _GenerateJavaStubsCore instead of the wrapper _GenerateJavaStubs (which has no Inputs/Outputs and never reports as skipped). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AndroidUpdateResourcesTest.cs | 2 +- .../IncrementalBuildTest.cs | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs index f04aabcb3dd..87d656b3a45 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs @@ -543,7 +543,7 @@ protected override void OnClick() Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput, "AndroidResgen: Warning while updating Resource XML"), "Warning while processing resources should not have been raised."); Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true), "Build should have succeeded."); - Assert.IsTrue (b.Output.IsTargetSkipped ("_GenerateJavaStubs"), "Target _GenerateJavaStubs should have been skipped"); + Assert.IsTrue (b.Output.IsTargetSkipped ("_GenerateJavaStubsCore"), "Target _GenerateJavaStubsCore should have been skipped"); lib.Touch ("CustomTextView.cs"); 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 8e3600f20dd..1b4e3314598 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 @@ -746,7 +746,7 @@ public void ProduceReferenceAssembly ([Values] AndroidRuntime runtime) appBuilder.Output.AssertTargetIsSkipped ("CoreCompile"); appBuilder.Output.AssertTargetIsSkipped ("_BuildLibraryImportsCache"); appBuilder.Output.AssertTargetIsSkipped ("_ResolveLibraryProjectImports"); - appBuilder.Output.AssertTargetIsSkipped ("_GenerateJavaStubs"); + appBuilder.Output.AssertTargetIsSkipped ("_GenerateJavaStubsCore"); appBuilder.Output.AssertTargetIsPartiallyBuilt (KnownTargets.LinkAssembliesNoShrink); @@ -1237,7 +1237,10 @@ public void GenerateJavaStubsAndAssembly ([Values] bool isRelease, [Values] Andr } var targets = new [] { - "_GenerateJavaStubs", + "_GenerateJavaCallableWrappers", + "_GenerateJavaStubsCore", + "_GenerateTypeMappings", + "_GenerateAndroidManifest", "_GeneratePackageManagerJava", }; var proj = new XamarinAndroidApplicationProject { @@ -1289,6 +1292,34 @@ public void GenerateJavaStubsAndAssembly ([Values] bool isRelease, [Values] Andr } } + [Test] + public void GenerateJavaStubsSubTargetIncrementality ([Values (false, true)] bool isRelease) + { + // Test that all sub-targets correctly skip on no-change rebuilds. + var targets = new [] { + "_GenerateJavaCallableWrappers", + "_GenerateJavaStubsCore", + "_GenerateTypeMappings", + "_GenerateAndroidManifest", + }; + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + + using (var b = CreateApkBuilder ()) { + Assert.IsTrue (b.Build (proj), "first build should have succeeded."); + foreach (var target in targets) { + Assert.IsFalse (b.Output.IsTargetSkipped (target), $"first build: `{target}` should *not* be skipped!"); + } + + // No-change rebuild: all sub-targets should skip + Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "no-change rebuild should have succeeded."); + foreach (var target in targets) { + Assert.IsTrue (b.Output.IsTargetSkipped (target), $"no-change: `{target}` should be skipped!"); + } + } + } + readonly string [] ExpectedAssemblyFiles = new [] { Path.Combine ("android", "environment.@ABI@.o"), Path.Combine ("android", "environment.@ABI@.ll"), @@ -1683,7 +1714,7 @@ public void AndroidResourceChange ([Values] AndroidRuntime runtime) // TODO: NativeAOT doesn't skip this target if (runtime != AndroidRuntime.NativeAOT) { - builder.Output.AssertTargetIsSkipped ("_GenerateJavaStubs"); + builder.Output.AssertTargetIsSkipped ("_GenerateJavaStubsCore"); } builder.Output.AssertTargetIsSkipped ("_CompileJava"); builder.Output.AssertTargetIsSkipped ("_CompileToDalvik");