From ee5f7e328b8da56839a5c1ad29030dcfba5c6202 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 26 May 2026 13:54:48 -0700 Subject: [PATCH 1/5] Fix _AfterILLinkAdditionalSteps breaking IlcCompile incrementalism Split _AfterILLinkAdditionalSteps into _RunAfterILLinkAdditionalSteps (incremental, writes to afterlink/) and _AfterILLinkAdditionalSteps (always runs, updates itemgroups to afterlink/ paths). Previously, AssemblyModifierPipeline modified assemblies in-place in linked/, which updated file timestamps even for unchanged files (Cecil opens with ReadWrite, and FileStream.Dispose flushes the mtime). Since this target runs in the outer project after IlcCompile completes in the inner project, the linked/ timestamps ended up newer than native/*.o, causing IlcCompile to re-run on every subsequent build. Writing to a separate afterlink/ directory avoids touching linked/ and preserves IlcCompile's incremental state. Incremental no-op publish drops from ~17s to ~2.5s. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Assisted-by: Claude:claude-opus-4.6-1m --- .../Xamarin.Android.Common.targets | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 42b4601125c..fb9752b824b 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -934,6 +934,7 @@ because xbuild doesn't support framework reference assemblies. <_AndroidApkPerAbiFlagFile>$(IntermediateOutputPath)android\bin\apk_per_abi.flag <_AndroidDebugKeyStoreFlag>$(IntermediateOutputPath)android_debug_keystore.flag <_AdditionalPostLinkerStepsFlag>$(_AndroidStampDirectory)_AdditionalPostLinkerSteps.stamp + <_AfterILLinkOutputDir>$(IntermediateOutputPath)afterlink\ <_AcwMapFile>$(IntermediateOutputPath)acw-map.txt <_CustomViewMapFile>$(IntermediateOutputPath)customview-map.txt $(RootNamespace) @@ -1465,16 +1466,17 @@ because xbuild doesn't support framework reference assemblies. - + - + + + + + + + + + <_OrigResolvedAssemblies Include="@(ResolvedAssemblies)" /> + + + + <_OrigResolvedUserAssemblies Include="@(ResolvedUserAssemblies)" /> + + + + <_OrigResolvedFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies)" /> + + + From 143652f8b09abc4d6933588e7a28b8b938a9e1d9 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Wed, 27 May 2026 13:14:35 -0700 Subject: [PATCH 2/5] Add test for _AfterILLinkAdditionalSteps incrementalism Verify that _RunAfterILLinkAdditionalSteps is skipped on second build, the afterlink/ output directory contains assemblies, and the outer _AfterILLinkAdditionalSteps target always runs to update itemgroups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Assisted-by: Claude:claude-opus-4.6-1m --- .../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 c13c7e400ca..2fefe3a72b4 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 @@ -1944,5 +1944,37 @@ public void BuildPropsBreaksConvertResourcesCasesOnSecondBuild ([Values] Android } } + [Test] + public void AfterILLinkAdditionalStepsIsSkippedOnSecondBuild ([Values] AndroidRuntime runtime) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("PublishTrimmed", "true"); + + using (var b = CreateApkBuilder ()) { + Assert.IsTrue (b.Build (proj), "first build should succeed"); + b.Output.AssertTargetIsNotSkipped ("_RunAfterILLinkAdditionalSteps"); + b.Output.AssertTargetIsNotSkipped ("_AfterILLinkAdditionalSteps"); + + // Verify afterlink/ output directory was created with assemblies + var afterlinkDir = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "afterlink"); + Assert.IsTrue (Directory.Exists (afterlinkDir), "afterlink/ directory should exist after first build"); + var afterlinkFiles = Directory.GetFiles (afterlinkDir, "*.dll"); + Assert.IsTrue (afterlinkFiles.Length > 0, "afterlink/ directory should contain assemblies"); + + Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "second build should succeed"); + b.Output.AssertTargetIsSkipped ("_RunAfterILLinkAdditionalSteps"); + // The outer target must always run to update assembly itemgroups for downstream targets + b.Output.AssertTargetIsNotSkipped ("_AfterILLinkAdditionalSteps"); + } + } + } } From 1a3831e3c8048836234fc953739ad3f1c7119c47 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Wed, 27 May 2026 13:29:06 -0700 Subject: [PATCH 3/5] Use %(DestinationSubPath) in afterlink/ to avoid multi-RID collisions The original PR flattened all ResolvedAssemblies into afterlink/ using %(Filename)%(Extension), which would cause assemblies from different ABIs to overwrite each other. Use %(DestinationSubPath) instead, matching the established pattern in _LinkAssembliesNoShrink. This produces per-ABI subdirectories (e.g. afterlink/arm64-v8a/Foo.dll). Update test to verify per-ABI subdirectories under afterlink/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Assisted-by: Claude:claude-opus-4.6-1m --- .../IncrementalBuildTest.cs | 10 +++++++--- .../Xamarin.Android.Common.targets | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) 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 2fefe3a72b4..de5cf20d655 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 @@ -1963,11 +1963,15 @@ public void AfterILLinkAdditionalStepsIsSkippedOnSecondBuild ([Values] AndroidRu b.Output.AssertTargetIsNotSkipped ("_RunAfterILLinkAdditionalSteps"); b.Output.AssertTargetIsNotSkipped ("_AfterILLinkAdditionalSteps"); - // Verify afterlink/ output directory was created with assemblies + // Verify afterlink/ output directory was created with per-ABI subdirectories containing assemblies var afterlinkDir = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "afterlink"); Assert.IsTrue (Directory.Exists (afterlinkDir), "afterlink/ directory should exist after first build"); - var afterlinkFiles = Directory.GetFiles (afterlinkDir, "*.dll"); - Assert.IsTrue (afterlinkFiles.Length > 0, "afterlink/ directory should contain assemblies"); + var abiDirs = Directory.GetDirectories (afterlinkDir); + Assert.IsTrue (abiDirs.Length > 0, "afterlink/ should contain ABI subdirectories"); + foreach (var abiDir in abiDirs) { + var afterlinkFiles = Directory.GetFiles (abiDir, "*.dll"); + Assert.IsTrue (afterlinkFiles.Length > 0, $"afterlink/{Path.GetFileName (abiDir)}/ should contain assemblies"); + } Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "second build should succeed"); b.Output.AssertTargetIsSkipped ("_RunAfterILLinkAdditionalSteps"); diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index fb9752b824b..0bd020d899e 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1476,7 +1476,7 @@ because xbuild doesn't support framework reference assemblies. ApplicationJavaClass="$(AndroidApplicationJavaClass)" CodeGenerationTarget="$(_AndroidJcwCodegenTarget)" Debug="$(AndroidIncludeDebugSymbols)" - DestinationFiles="@(ResolvedAssemblies->'$(_AfterILLinkOutputDir)%(Filename)%(Extension)')" + DestinationFiles="@(ResolvedAssemblies->'$(_AfterILLinkOutputDir)%(DestinationSubPath)')" Deterministic="$(Deterministic)" EnableMarshalMethods="$(_AndroidUseMarshalMethods)" ErrorOnCustomJavaObject="$(AndroidErrorOnCustomJavaObject)" @@ -1502,15 +1502,15 @@ because xbuild doesn't support framework reference assemblies. <_OrigResolvedAssemblies Include="@(ResolvedAssemblies)" /> - + <_OrigResolvedUserAssemblies Include="@(ResolvedUserAssemblies)" /> - + <_OrigResolvedFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies)" /> - + From 097325130ef90043b44d5d61f7af73e316d99a78 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 28 May 2026 13:38:42 -0700 Subject: [PATCH 4/5] Fix CheckSignApk test: resource-only incremental build no longer triggers IlcCompile With the _AfterILLinkAdditionalSteps fix, assembly inputs to IlcCompile are unchanged when only Android resources (Strings.xml) are modified. IlcCompile is correctly skipped, so no IL3053 warnings are emitted on the second build. The test now expects zero warnings on both runtimes for the incremental build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Assisted-by: Claude:claude-opus-4.6 --- .../Tests/Xamarin.Android.Build.Tests/PackagingTest.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index dd599912590..96f9a9d52c7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -601,11 +601,8 @@ public void CheckSignApk ([Values] bool useApkSigner, [Values] bool perAbiApk, [ item.TextContent = () => proj.StringsXml.Replace ("${PROJECT_NAME}", "Foo"); item.Timestamp = null; Assert.IsTrue (b.Build (proj), "Second build failed"); - if (runtime != AndroidRuntime.NativeAOT) { - b.AssertHasNoWarnings (); - } else { - StringAssertEx.Contains ("2 Warning(s)", b.LastBuildOutput, "NativeAOT should produce two IL3053 warnings"); - } + // Resource-only change does not trigger IlcCompile, so no IL3053 warnings on second build + b.AssertHasNoWarnings (); //Make sure the APKs are signed foreach (var apk in Directory.GetFiles (bin, "*-Signed.apk")) { From a3e6beb876d9d18318acf519893c87f82156ef9b Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 28 May 2026 13:41:39 -0700 Subject: [PATCH 5/5] Clean up temporary _OrigResolved* items after use Remove the intermediate itemgroups to avoid polluting the item namespace for downstream targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Assisted-by: Claude:claude-opus-4.6 --- .../Xamarin.Android.Common.targets | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 0bd020d899e..f3dab9d5eb8 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1511,6 +1511,10 @@ because xbuild doesn't support framework reference assemblies. <_OrigResolvedFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies)" /> + + <_OrigResolvedAssemblies Remove="@(_OrigResolvedAssemblies)" /> + <_OrigResolvedUserAssemblies Remove="@(_OrigResolvedUserAssemblies)" /> + <_OrigResolvedFrameworkAssemblies Remove="@(_OrigResolvedFrameworkAssemblies)" />