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); + } + } +} +