Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Documentation/docs-mobile/messages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 46 additions & 0 deletions Documentation/docs-mobile/messages/xa4322.md
Original file line number Diff line number Diff line change
@@ -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.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,13 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins
<value>NativeAOT DGML file '{0}' was not found.</value>
<comment>The following are literal names and should not be translated: NativeAOT, DGML
{0} - The path to the NativeAOT DGML file.</comment>
</data>
<data name="XA4322" xml:space="preserve">
<value>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.</value>
<comment>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.</comment>
</data>
<data name="XA5101" xml:space="preserve">
<value>Missing Android NDK toolchains directory '{0}'. Please install the Android NDK.</value>
Expand Down
86 changes: 80 additions & 6 deletions src/Xamarin.Android.Build.Tasks/Tasks/R8.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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",
};
Comment thread
Copilot marked this conversation as resolved.

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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}