From 3bfeac6c7b8e52aa5a7079668c0494285b02bba8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 24 Jul 2026 19:11:46 +0200 Subject: [PATCH 1/2] [Build] Make universal APK generation incremental Preserve a current filesystem timestamp on the extracted universal APK and track all relevant bundletool, AAPT2, signing, and project inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a301fa68-0e6c-4280-b2f2-bc3d4c9ae3de --- .../IncrementalBuildTest.cs | 49 +++++++++++++++++-- .../Xamarin.Android.Common.targets | 25 +++++++++- 2 files changed, 68 insertions(+), 6 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 0f24f1ac54a..e30998962d0 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 @@ -1832,14 +1832,53 @@ public void BuildPropsBreaksConvertResourcesCasesOnSecondBuild ([Values (Android Assert.IsTrue (secondAssemblyWrite > firstAssemblyWrite, $"Assembly write time was not updated on partially incremental build. Before: {firstAssemblyWrite}. After: {secondAssemblyWrite}."); - // TODO: NativeAOT fails this with "Apk write time was not updated on partially incremental build. Before: 1/1/1981 1:01:02 AM. After: 1/1/1981 1:01:02 AM." - if (runtime != AndroidRuntime.NativeAOT) { - Assert.IsTrue (secondApkWrite > firstApkWrite, - $"Apk write time was not updated on partially incremental build. Before: {firstApkWrite}. After: {secondApkWrite}."); - } + Assert.IsTrue (secondApkWrite > firstApkWrite, + $"Apk write time was not updated on partially incremental build. Before: {firstApkWrite}. After: {secondApkWrite}."); } } + [Test] + public void UniversalApkFromBundleIsIncremental () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty (KnownProperties.RuntimeIdentifier, "android-arm64"); + proj.SetProperty ("AndroidPackageFormats", "aab;apk"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_CreateUniversalApkFromBundle"); + + var outputDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath); + var signedApk = Directory.GetFiles (outputDirectory, "*-Signed.apk", SearchOption.AllDirectories).Single (); + var firstWriteTime = File.GetLastWriteTimeUtc (signedApk); + + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "No-op build should have succeeded."); + builder.Output.AssertTargetIsSkipped ("_CreateUniversalApkFromBundle"); + Assert.AreEqual (firstWriteTime, File.GetLastWriteTimeUtc (signedApk), "No-op builds should preserve the signed APK timestamp."); + + Assert.IsTrue ( + builder.Build (proj, parameters: new [] { "AndroidBundleToolExtraArgs=--overwrite" }, doNotCleanupOnUpdate: true, saveProject: false), + "Changing bundletool arguments should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_CreateUniversalApkFromBundle"); + + Assert.IsTrue ( + builder.Build (proj, parameters: new [] { "AndroidBundleToolExtraArgs=" }, doNotCleanupOnUpdate: true, saveProject: false), + "Clearing bundletool arguments should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_CreateUniversalApkFromBundle"); + + Assert.IsTrue ( + builder.Build (proj, parameters: new [] { "AndroidBundleToolExtraArgs=" }, doNotCleanupOnUpdate: true, saveProject: false), + "No-op build after clearing bundletool arguments should have succeeded."); + builder.Output.AssertTargetIsSkipped ("_CreateUniversalApkFromBundle"); + } + [Test] public void AfterILLinkAdditionalStepsIsSkippedOnSecondBuild ([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 1e57801422e..c952a2db4d5 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -976,11 +976,15 @@ because xbuild doesn't support framework reference assemblies. <_PropertyCacheItems Include="AndroidDexTool=$(AndroidDexTool)" /> <_PropertyCacheItems Include="AndroidLinkTool=$(AndroidLinkTool)" /> <_PropertyCacheItems Include="AndroidLinkResources=$(AndroidLinkResources)" /> + <_PropertyCacheItems Include="AndroidBundleToolExtraArgs=$(AndroidBundleToolExtraArgs)" /> + <_PropertyCacheItems Include="AndroidKeyStore=$(AndroidKeyStore)" /> <_PropertyCacheItems Include="AndroidPackageFormat=$(AndroidPackageFormat)" /> <_PropertyCacheItems Include="EmbedAssembliesIntoApk=$(EmbedAssembliesIntoApk)" /> <_PropertyCacheItems Include="AndroidLinkMode=$(AndroidLinkMode)" /> <_PropertyCacheItems Include="AndroidLinkSkip=$(AndroidLinkSkip)" /> <_PropertyCacheItems Include="AndroidSdkBuildToolsVersion=$(AndroidSdkBuildToolsVersion)" /> + <_PropertyCacheItems Include="Aapt2ToolPath=$(Aapt2ToolPath)" /> + <_PropertyCacheItems Include="Aapt2ToolExe=$(Aapt2ToolExe)" /> <_PropertyCacheItems Include="AndroidSdkPath=$(_AndroidSdkDirectory)" /> <_PropertyCacheItems Include="AndroidNdkPath=$(_AndroidNdkDirectory)" /> <_PropertyCacheItems Include="JavaSdkPath=$(_JavaSdkDirectory)" /> @@ -2672,9 +2676,23 @@ because xbuild doesn't support framework reference assemblies. + + + <_UniversalApkAapt2ToolExe Condition=" '$(Aapt2ToolExe)' != '' ">$(Aapt2ToolExe) + <_UniversalApkAapt2ToolExe Condition=" '$(_UniversalApkAapt2ToolExe)' == '' and '$(OS)' == 'Windows_NT' ">aapt2.exe + <_UniversalApkAapt2ToolExe Condition=" '$(_UniversalApkAapt2ToolExe)' == '' ">aapt2 + <_UniversalApkAapt2ToolPath>$([System.IO.Path]::Combine('$(Aapt2ToolPath)', '$(_UniversalApkAapt2ToolExe)')) + + <_UniversalApkAlwaysRunInput Condition=" '$(AndroidKeyStore)' == 'True' or '$(AndroidBundleToolExtraArgs)' != '' ">$(IntermediateOutputPath)__always_rebuild_universal_apk__ + + + @@ -2687,6 +2705,7 @@ because xbuild doesn't support framework reference assemblies. AdbToolPath="$(AdbToolPath)" AdbTarget="$(AdbTarget)" Aapt2ToolPath="$(Aapt2ToolPath)" + Aapt2ToolExe="$(Aapt2ToolExe)" AppBundle="$(_AppBundleIntermediate)" Output="$(_UniversalApkSetIntermediate)" KeyStore="$(_ApkKeyStore)" @@ -2705,6 +2724,9 @@ because xbuild doesn't support framework reference assemblies. + + Date: Fri, 24 Jul 2026 22:38:50 +0200 Subject: [PATCH 2/2] [Build] Fix bundletool comment typo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a301fa68-0e6c-4280-b2f2-bc3d4c9ae3de --- src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets | 2 +- 1 file changed, 1 insertion(+), 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 c952a2db4d5..f050205a471 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2695,7 +2695,7 @@ because xbuild doesn't support framework reference assemblies. Inputs="$(_AppBundleIntermediate);$(_AndroidBuildPropertiesCache);@(_AndroidMSBuildAllProjects);$(AndroidBundleToolJarPath);$(_UniversalApkAapt2ToolPath);$(_ApkKeyStore);$(_UniversalApkAlwaysRunInput)" Outputs="$(OutDir)$(_AndroidPackage)-Signed.apk" > - +