From 763c8941cb69119a1bd84c51694cfbbfdc4f3b99 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 22 Jun 2026 09:31:50 -0500 Subject: [PATCH 1/2] [Xamarin.Android.Build.Tasks] Skip library proguard.txt with disallowed global options Android Gradle Plugin 9.0 disallows `global'' ProGuard options such as -printmapping, -printconfiguration, -printusage, -printseeds, and -dump in consumer keep files shipped inside an .aar. AGP either fails library publishing or silently strips the options when consuming a pre-built dependency: https://developer.android.com/build/releases/agp-9-0-0-release-notes#behavior-changes Bring the same protection to .NET for Android: when an .aar's proguard.txt (extracted by ResolveLibraryProjectImports) contains one of these options, skip the whole file when invoking R8 and emit a new XA4322 warning naming the NuGet package (or .aar path) the configuration came from. Files generated by us or added directly by the user are untouched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/index.md | 1 + Documentation/docs-mobile/messages/xa4322.md | 45 +++++++++++ .../Properties/Resources.Designer.cs | 9 +++ .../Properties/Resources.resx | 7 ++ src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 79 +++++++++++++++++-- .../Tasks/R8Tests.cs | 29 +++++++ 6 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 Documentation/docs-mobile/messages/xa4322.md create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 4dfe2ffdf55..b9e3a345395 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -245,6 +245,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4319](xa4319.md): No NativeAOT DGML files were provided. + [XA4320](xa4320.md): ACW map file '{file}' was not found. + [XA4321](xa4321.md): NativeAOT DGML file '{file}' was not found. ++ [XA4322](xa4322.md): Skipping library ProGuard configuration file '{file}' (from {source}) because it contains the unsupported global option '{option}'. Global ProGuard options are only allowed in application projects. ## XA5xxx: GCC and toolchain diff --git a/Documentation/docs-mobile/messages/xa4322.md b/Documentation/docs-mobile/messages/xa4322.md new file mode 100644 index 00000000000..839e4635cf2 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4322.md @@ -0,0 +1,45 @@ +--- +title: .NET for Android warning XA4322 +description: XA4322 warning code +ms.date: 06/22/2026 +f1_keywords: + - "XA4322" +--- + +# .NET for Android warning XA4322 + +## Example messages + +``` +warning XA4322: Skipping library ProGuard configuration file 'obj/Debug/net10.0-android/lp/0/jl/proguard.txt' (from NuGet package 'Acme.SomeLibrary' 1.2.3) because it contains the unsupported global option '-printmapping'. Global ProGuard options are only allowed in application projects. +``` + +## Issue + +An Android library (`.aar`) shipped a `proguard.txt` file (a "consumer ProGuard +configuration") that contains a global ProGuard option such as `-printmapping`, +`-printconfiguration`, `-printusage`, `-printseeds`, or `-dump`. + +These options control the entire build (for example, where R8 writes the +mapping file) and must only be set by the application project that consumes the +library. They are not allowed inside an `.aar`'s `proguard.txt`. This matches +the behavior introduced in [Android Gradle Plugin 9.0](https://developer.android.com/build/releases/agp-9-0-0-release-notes#behavior-changes), +where R8 either fails library publishing or silently ignores such global +options when they appear in a pre-compiled dependency (JAR or AAR). To keep +your build working, .NET for Android skips the entire offending file before +invoking R8 and emits this warning. + +## Solution + +If you own the library, remove the global options from its `proguard.txt` / +`ProguardConfiguration` items and instead set them in the application project's +own `ProguardConfiguration` (for example, by setting +`$(AndroidProguardMappingFile)` to control where the mapping file is written). +Once the global options are removed, the library's keep rules will once again +be passed to R8. + +If you do not own the library, file an issue with its maintainers and ask them +to remove the disallowed options. Until then, R8 will not see any of the keep +rules from this library's `proguard.txt`, which may cause needed code to be +shrunk. Add equivalent `-keep` rules to your own application's +`ProguardConfiguration` as a workaround. diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 6d9b7e9df46..42e59329019 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1841,6 +1841,15 @@ public static string XA4321 { } } + /// + /// Looks up a localized string similar to Skipping library ProGuard configuration file '{1}' (from {2}) because it contains the unsupported global option '{0}'. Global ProGuard options are only allowed in application projects.. + /// + public static string XA4322 { + get { + return ResourceManager.GetString("XA4322", resourceCulture); + } + } + /// /// Looks up a localized string similar to Missing Android NDK toolchains directory '{0}'. Please install the Android NDK.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index 620b9f7bbaf..f41614e9e2a 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -858,6 +858,13 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins NativeAOT DGML file '{0}' was not found. The following are literal names and should not be translated: NativeAOT, DGML {0} - The path to the NativeAOT DGML file. + + + Skipping library ProGuard configuration file '{1}' (from {2}) because it contains the unsupported global option '{0}'. Global ProGuard options are only allowed in application projects. + The following are literal names and should not be translated: ProGuard +{0} - The unsupported ProGuard option, such as -printmapping. +{1} - The path to the proguard.txt file inside the extracted .aar. +{2} - A description of the source: either a NuGet package id/version or an .aar file path. Missing Android NDK toolchains directory '{0}'. Please install the Android NDK. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index 89370b1d12c..f7d262c0727 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -34,7 +34,7 @@ public class R8 : D8 public string? ProguardGeneratedApplicationConfiguration { get; set; } public string? ProguardCommonXamarinConfiguration { get; set; } public string? ProguardMappingFileOutput { get; set; } - public string []? ProguardConfigurationFiles { get; set; } + public ITaskItem []? ProguardConfigurationFiles { get; set; } public bool UseTrimmableNativeAotProguardConfiguration { get; set; } protected override string MainClass => "com.android.tools.r8.R8"; @@ -155,19 +155,86 @@ protected override string CreateResponseFile () WriteArg (response, temp); } if (ProguardConfigurationFiles != null) { - foreach (var file in ProguardConfigurationFiles) { - if (File.Exists (file)) { - WriteArg (response, "--pg-conf"); - WriteArg (response, file); - } else { + foreach (var item in ProguardConfigurationFiles) { + var file = item.ItemSpec; + if (!File.Exists (file)) { Log.LogCodedWarning ("XA4304", file, 0, Properties.Resources.XA4304, file); + continue; } + if (HasDisallowedLibraryProguardOption (item, out var option)) { + Log.LogCodedWarning ("XA4322", file, 0, Properties.Resources.XA4322, + option, file, DescribeProguardSource (item)); + continue; + } + WriteArg (response, "--pg-conf"); + WriteArg (response, file); } } return responseFile; } + // ProGuard "global" options that affect the whole build and are not allowed inside + // a library's proguard.txt (the file packaged inside an .aar's root). AGP 9.0 + // introduced the same restriction — see "Behavior changes" in the AGP 9.0 release + // notes: + // https://developer.android.com/build/releases/agp-9-0-0-release-notes#behavior-changes + // We skip the whole offending file and emit a warning naming the source library + // so the build can still succeed. + static readonly string [] DisallowedLibraryProguardOptions = { + "-dump", + "-printconfiguration", + "-printmapping", + "-printseeds", + "-printusage", + }; + + bool HasDisallowedLibraryProguardOption (ITaskItem item, out string option) + { + option = ""; + // Only library-provided proguard.txt files (extracted from .aar) carry OriginalFile + // metadata. Skip files we generate ourselves or that the user added directly. + if (item.GetMetadata ("OriginalFile").IsNullOrEmpty ()) { + return false; + } + foreach (var raw in File.ReadLines (item.ItemSpec)) { + if (TryGetDisallowedOption (raw, out var found)) { + option = found; + return true; + } + } + return false; + } + + internal static bool TryGetDisallowedOption (string line, out string option) + { + var trimmed = line.TrimStart (); + foreach (var candidate in DisallowedLibraryProguardOptions) { + if (trimmed.StartsWith (candidate, StringComparison.OrdinalIgnoreCase)) { + option = candidate; + return true; + } + } + option = ""; + return false; + } + + static string DescribeProguardSource (ITaskItem item) + { + var packageId = item.GetMetadata ("NuGetPackageId"); + if (!packageId.IsNullOrEmpty ()) { + var version = item.GetMetadata ("NuGetPackageVersion"); + return version.IsNullOrEmpty () + ? $"NuGet package '{packageId}'" + : $"NuGet package '{packageId}' {version}"; + } + var originalFile = item.GetMetadata ("OriginalFile"); + if (!originalFile.IsNullOrEmpty ()) { + return $"'{originalFile}'"; + } + return $"'{item.ItemSpec}'"; + } + Stream GetEmbeddedResourceStream (string resourceName) { var stream = GetType ().Assembly.GetManifestResourceStream (resourceName); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs new file mode 100644 index 00000000000..eb77a50ab73 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs @@ -0,0 +1,29 @@ +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + public class R8Tests + { + [TestCase ("-keep class com.example.Foo { *; }", false, "")] + [TestCase ("-dontwarn com.example.**", false, "")] + [TestCase ("# -printmapping comment", false, "")] + [TestCase ("", false, "")] + [TestCase ("-printmapping mapping.txt", true, "-printmapping")] + [TestCase (" -printmapping mapping.txt", true, "-printmapping")] + [TestCase ("\t-printseeds seeds.txt", true, "-printseeds")] + [TestCase ("-printusage usage.txt", true, "-printusage")] + [TestCase ("-printconfiguration config.txt", true, "-printconfiguration")] + [TestCase ("-dump dump.txt", true, "-dump")] + [TestCase ("-PrintMapping mapping.txt", true, "-printmapping")] // case-insensitive + [TestCase ("-DUMP dump.txt", true, "-dump")] + public void TryGetDisallowedOption (string line, bool expected, string expectedOption) + { + var actual = R8.TryGetDisallowedOption (line, out var option); + Assert.AreEqual (expected, actual); + Assert.AreEqual (expectedOption, option); + } + } +} + From fbff89d92947e18a2af947e5c6eaf311848b8b28 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 22 Jun 2026 12:23:38 -0500 Subject: [PATCH 2/2] Address review: add -dontoptimize/-dontobfuscate and token-boundary check Per the AGP 9.0 release notes, the primary global options called out are -dontoptimize and -dontobfuscate. Add them to DisallowedLibraryProguardOptions so libraries cannot silently disable optimization/obfuscation for consuming apps. Also tighten TryGetDisallowedOption to require an end-of-token boundary (end-of-line or whitespace) after the matched option, so e.g. '-printmappingFoo' is no longer wrongly treated as '-printmapping'. Tests cover both new options and the boundary case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/xa4322.md | 5 +++-- src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 9 ++++++++- .../Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs | 6 ++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Documentation/docs-mobile/messages/xa4322.md b/Documentation/docs-mobile/messages/xa4322.md index 839e4635cf2..bb6be22a631 100644 --- a/Documentation/docs-mobile/messages/xa4322.md +++ b/Documentation/docs-mobile/messages/xa4322.md @@ -17,8 +17,9 @@ warning XA4322: Skipping library ProGuard configuration file 'obj/Debug/net10.0- ## Issue An Android library (`.aar`) shipped a `proguard.txt` file (a "consumer ProGuard -configuration") that contains a global ProGuard option such as `-printmapping`, -`-printconfiguration`, `-printusage`, `-printseeds`, or `-dump`. +configuration") that contains a global ProGuard option such as `-dontoptimize`, +`-dontobfuscate`, `-printmapping`, `-printconfiguration`, `-printusage`, +`-printseeds`, or `-dump`. These options control the entire build (for example, where R8 writes the mapping file) and must only be set by the application project that consumes the diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index f7d262c0727..afcc44839fb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -182,6 +182,8 @@ protected override string CreateResponseFile () // We skip the whole offending file and emit a warning naming the source library // so the build can still succeed. static readonly string [] DisallowedLibraryProguardOptions = { + "-dontobfuscate", + "-dontoptimize", "-dump", "-printconfiguration", "-printmapping", @@ -210,7 +212,12 @@ internal static bool TryGetDisallowedOption (string line, out string option) { var trimmed = line.TrimStart (); foreach (var candidate in DisallowedLibraryProguardOptions) { - if (trimmed.StartsWith (candidate, StringComparison.OrdinalIgnoreCase)) { + if (trimmed.Length < candidate.Length) + continue; + if (!trimmed.StartsWith (candidate, StringComparison.OrdinalIgnoreCase)) + continue; + // Require an end-of-token boundary so "-printmappingFoo" does not match "-printmapping". + if (trimmed.Length == candidate.Length || char.IsWhiteSpace (trimmed [candidate.Length])) { option = candidate; return true; } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs index eb77a50ab73..8366adfd341 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs @@ -10,14 +10,20 @@ public class R8Tests [TestCase ("-dontwarn com.example.**", false, "")] [TestCase ("# -printmapping comment", false, "")] [TestCase ("", false, "")] + [TestCase ("-printmappingFoo foo.txt", false, "")] // token-boundary: must not match -printmapping + [TestCase ("-dumpsterfire", false, "")] // token-boundary: must not match -dump [TestCase ("-printmapping mapping.txt", true, "-printmapping")] + [TestCase ("-printmapping", true, "-printmapping")] // option with no argument [TestCase (" -printmapping mapping.txt", true, "-printmapping")] [TestCase ("\t-printseeds seeds.txt", true, "-printseeds")] [TestCase ("-printusage usage.txt", true, "-printusage")] [TestCase ("-printconfiguration config.txt", true, "-printconfiguration")] [TestCase ("-dump dump.txt", true, "-dump")] + [TestCase ("-dontoptimize", true, "-dontoptimize")] + [TestCase ("-dontobfuscate", true, "-dontobfuscate")] [TestCase ("-PrintMapping mapping.txt", true, "-printmapping")] // case-insensitive [TestCase ("-DUMP dump.txt", true, "-dump")] + [TestCase ("-DontOptimize", true, "-dontoptimize")] public void TryGetDisallowedOption (string line, bool expected, string expectedOption) { var actual = R8.TryGetDisallowedOption (line, out var option);