From cdac2ef88d740e8ce8df9863885805fa0b892566 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 12 Oct 2026 10:11:49 +0200 Subject: [PATCH] [Xamarin.Android.Build.Tasks] Preserve AAR consumer rules on cache hits Keep extracted AAR proguard.txt entries in the imports cache when the resolver takes the unchanged-AAR path, and cover the incremental R8 flow. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/ResolveLibraryProjectImports.cs | 7 ++ .../IncrementalBuildTest.cs | 73 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs index 790d853c55b..a8757d5dbea 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs @@ -414,6 +414,13 @@ void Extract ( [NuGetPackageId] = nuGetPackageId, [NuGetPackageVersion] = nuGetPackageVersion, })); + if (AndroidApplication && File.Exists (proguardFile)) { + proguardConfigFiles.Add (new TaskItem (Path.GetFullPath (proguardFile), new Dictionary { + [OriginalFile] = aarFullPath, + [NuGetPackageId] = nuGetPackageId, + [NuGetPackageVersion] = nuGetPackageVersion, + })); + } continue; } 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 015fadc342b..2bcb25c5fe0 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 @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Text; using Xamarin.Android.Tasks; @@ -73,6 +74,78 @@ public void AaptRulesRemainInR8ConfigurationAfterIncrementalBuild () "Subsequent R8 builds should still receive the merged AAPT2 rules."); } + [Test] + public void AarConsumerRulesRemainInR8ConfigurationAfterAppProjectTimestampChange () + { + const AndroidRuntime runtime = AndroidRuntime.CoreCLR; + if (IgnoreUnsupportedConfiguration (runtime, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + OtherBuildItems = { + new AndroidItem.AndroidAarLibrary ("consumer-rules.aar") { + BinaryContent = CreateAarWithProguardRules, + }, + new AndroidItem.AndroidJavaSource ("Extra.java") { + TextContent = () => "public class Extra { }", + Encoding = Encoding.ASCII, + MetadataValues = "Bind=False", + }, + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty (proj.ReleaseProperties, KnownProperties.AndroidLinkTool, "r8"); + proj.SetProperty (proj.ReleaseProperties, "TrimMode", "full"); + + using var builder = CreateApkBuilder (); + builder.Verbosity = LoggerVerbosity.Detailed; + Assert.IsTrue (builder.Build (proj), "Initial build should succeed."); + + var cacheFile = builder.Output.GetIntermediaryPath ("libraryprojectimports.cache"); + var projectFile = Path.Combine (Root, builder.ProjectDirectory, proj.ProjectFilePath); + File.SetLastWriteTimeUtc (projectFile, DateTime.UtcNow.AddMinutes (2)); + + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), + "Build after touching the project file should succeed."); + builder.Output.AssertTargetIsNotSkipped ("_ResolveLibraryProjectImports"); + var rules = ReadCache (cacheFile).ProguardConfigFiles ?? []; + Assert.AreEqual (1, rules.Length, "The AAR consumer rule should remain in the imports cache."); + var proguardFile = Path.GetFullPath (rules [0].ItemSpec); + FileAssert.Exists (proguardFile); + + // Force a subsequent R8 invocation to verify the rule recovered from the cache is passed through. + proj.Touch ("Extra.java"); + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), + "Build after the timestamp-only project-file change should succeed."); + Assert.IsTrue (HasR8ProguardConfigInput (builder.LastBuildOutput, proguardFile), + $"R8 should receive '{proguardFile}' through --pg-conf after touching the project file."); + } + + static byte [] CreateAarWithProguardRules () + { + using var stream = new MemoryStream (); + using (var aar = new ZipArchive (stream, ZipArchiveMode.Create, leaveOpen: true)) { + using var entry = aar.CreateEntry ("proguard.txt").Open (); + using var writer = new StreamWriter (entry); + writer.WriteLine ("-keep class java.lang.String { *; }"); + } + return stream.ToArray (); + } + + static bool HasR8ProguardConfigInput (IEnumerable buildOutput, string proguardFile) + { + var lines = buildOutput.Select (line => line.Trim ()).ToArray (); + for (int i = 0; i < lines.Length - 1; i++) { + if (lines [i].EndsWith ("--pg-conf", StringComparison.Ordinal) && + lines [i + 1].EndsWith (proguardFile, StringComparison.OrdinalIgnoreCase)) { + return true; + } + } + return false; + } + [Test] [Ignore ("Flaky timing-based test. Disabled while investigating incremental build regressions. See: https://github.com/dotnet/android/issues/11792")] public void BasicApplicationRepetitiveBuild ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime)