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..bb6be22a631
--- /dev/null
+++ b/Documentation/docs-mobile/messages/xa4322.md
@@ -0,0 +1,46 @@
+---
+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 `-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
+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..afcc44839fb 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,93 @@ 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 = {
+ "-dontobfuscate",
+ "-dontoptimize",
+ "-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.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;
+ }
+ }
+ 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..8366adfd341
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs
@@ -0,0 +1,35 @@
+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 ("-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);
+ Assert.AreEqual (expected, actual);
+ Assert.AreEqual (expectedOption, option);
+ }
+ }
+}
+