From d2f07a11647a4d2dd63bfb08ed1585609bfd745f Mon Sep 17 00:00:00 2001 From: David Nguyen <87228593+davidnguyen-tech@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:34:45 +0200 Subject: [PATCH 1/2] [Build] Add per-file up-to-date check in CompileNativeAssembly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When _CompileNativeAssemblySources runs, it recompiles ALL .ll files even if only some have changed. This is because MSBuild's target-level Inputs/Outputs is all-or-nothing — if any .ll file is newer than any .o file, every file gets recompiled. Add a per-file timestamp check in RunAssembler() — if the output .o is newer than the input .ll, that file is skipped. This saves time on incremental CoreCLR builds where upstream generators use CopyIfStreamChanged to preserve .ll timestamps for unchanged files. 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..4b3952d8141 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 && config.InputSource 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 f644fcadbeb043c27d48ce49b5235cf312cf0643 Mon Sep 17 00:00:00 2001 From: David Nguyen <87228593+davidnguyen-tech@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:52:12 +0200 Subject: [PATCH 2/2] [Tests] Add test for CompileNativeAssembly per-file skip Add CompileNativeAssemblySourcesSkipsUnchangedFiles integration test that verifies on incremental CoreCLR builds, unchanged .ll files are not recompiled by the CompileNativeAssembly task while changed .ll files are still correctly compiled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../IncrementalBuildTest.cs | 32 +++++++++++++++++++ 1 file changed, 32 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 8e3600f20dd..607a832afb9 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 @@ -1289,6 +1289,38 @@ public void GenerateJavaStubsAndAssembly ([Values] bool isRelease, [Values] Andr } } + [Test] + public void CompileNativeAssemblySourcesSkipsUnchangedFiles ([Values (AndroidRuntime.CoreCLR)] AndroidRuntime runtime) + { + if (IgnoreUnsupportedConfiguration (runtime, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (runtime); + + string abi = "arm64-v8a"; + proj.SetRuntimeIdentifier (abi); + + using (var b = CreateApkBuilder ()) { + b.Verbosity = LoggerVerbosity.Detailed; + Assert.IsTrue (b.Build (proj), "first build should have succeeded."); + + // Modify MainActivity to trigger recompilation of typemap sources + proj.MainActivity = proj.DefaultMainActivity + Environment.NewLine + "// test comment"; + proj.Touch ("MainActivity.cs"); + Assert.IsTrue (b.Build (proj), "second build should have succeeded."); + + Assert.IsFalse (b.Output.IsTargetSkipped ("_CompileNativeAssemblySources"), "`_CompileNativeAssemblySources` should *not* be skipped!"); + + // At least one .ll file should have been skipped as up to date (e.g., environment.arm64-v8a.ll) + Assert.IsTrue ( + StringAssertEx.ContainsRegex (@"\[LLVM llc\] Skipping.*up to date", b.LastBuildOutput), + "Expected at least one .ll file to be skipped as up to date" + ); + } + } + readonly string [] ExpectedAssemblyFiles = new [] { Path.Combine ("android", "environment.@ABI@.o"), Path.Combine ("android", "environment.@ABI@.ll"),