From 06eb51e38109bcaf22557cdb760429ca0cb550fb Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 3 Sep 2026 11:48:42 -0500 Subject: [PATCH] [r8] Include build metadata in app bundles (#12646) - pass `--build-metadata-output` to R8 - include the generated metadata at `BUNDLE-METADATA/com.android.tools/r8.json` in app bundles - verify both R8 metadata files are packaged and incremental builds remain up to date This metadata lets Google Play report R8 optimization, shrinking, and obfuscation coverage for the upcoming app-quality requirements announced in [App quality: Memory optimization and secure onboarding](https://android-developers.googleblog.com/2026/08/app-quality-memory-optimization-secure-onboarding.html). Related to #12639. Related to #12535. Save the following as `Directory.Build.targets` beside the project. It enables R8 for Release builds, downloads R8 9.0.32, generates `r8.json`, and includes it in the app bundle. The response file is required for this workaround because the shipping .NET 10 task passes `$(AndroidR8ExtraArguments)` directly on the Java command line, with platform-dependent handling of multiple arguments. ```xml r8 <_R8BuildMetadataFile>$(IntermediateOutputPath)r8.json <_R8JarFile>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)r8-9.0.32.jar')) <_R8MetadataRsp>$(IntermediateOutputPath)r8-build-metadata.rsp $(_R8JarFile) @$(_R8MetadataRsp) ``` Build the app bundle with: ```console dotnet build -c Release -p:AndroidPackageFormat=aab ``` The resulting signed AAB should contain `BUNDLE-METADATA/com.android.tools/r8.json`. This workaround was verified on macOS by @yuseok-kim-edushare and end-to-end on Windows with the .NET 10 Android workload: the metadata reports R8 9.0.32 and its DEX SHA-256 matches the packaged `classes.dex`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> (cherry picked from commit b4f3cd01fee7a5c410d42229b944132c60655eb4) Copilot-Session: 459f8db0-c8d7-4c77-99ea-3d43dd9f57d8 --- src/Xamarin.Android.Build.Tasks/Tasks/R8.cs | 6 ++++++ .../Xamarin.Android.Build.Tests/PackagingTest.cs | 14 +++++++++++++- .../Xamarin.Android.Common.targets | 5 +++++ .../Xamarin.Android.D8.targets | 2 ++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index fbf66e7bf48..b11d504734f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -34,6 +34,7 @@ public class R8 : D8 public string? ProguardGeneratedApplicationConfiguration { get; set; } public string? ProguardCommonXamarinConfiguration { get; set; } public string? ProguardMappingFileOutput { get; set; } + public string? BuildMetadataFileOutput { get; set; } public string []? ProguardConfigurationFiles { get; set; } protected override string MainClass => "com.android.tools.r8.R8"; @@ -63,6 +64,11 @@ protected override string CreateResponseFile () // Now append R8-specific arguments to the response file using var response = new StreamWriter (responseFile, append: true, encoding: Files.UTF8withoutBOM); + if (!BuildMetadataFileOutput.IsNullOrEmpty ()) { + WriteArg (response, "--build-metadata-output"); + WriteArg (response, Path.GetFullPath (BuildMetadataFileOutput)); + } + if (EnableMultiDex) { if (MinSdkVersion >= 21) { if (CustomMainDexListFiles?.Length > 0) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index d987bad2987..617ef224248 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -17,7 +17,7 @@ namespace Xamarin.Android.Build.Tests public class PackagingTest : BaseTest { [Test] - public void CheckProguardMappingFileExists () + public void CheckR8MetadataFilesExist () { var proj = new XamarinAndroidApplicationProject { IsRelease = true, @@ -25,11 +25,23 @@ public void CheckProguardMappingFileExists () proj.SetProperty (proj.ReleaseProperties, KnownProperties.AndroidLinkTool, "r8"); // Projects must set $(AndroidCreateProguardMappingFile) to true to opt in proj.SetProperty (proj.ReleaseProperties, "AndroidCreateProguardMappingFile", true); + proj.SetProperty ("AndroidPackageFormat", "aab"); using (var b = CreateApkBuilder ()) { string mappingFile = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath, "mapping.txt"); Assert.IsTrue (b.Build (proj), "build should have succeeded."); FileAssert.Exists (mappingFile, $"'{mappingFile}' should have been generated."); + var aab = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath, $"{proj.PackageName}-Signed.aab"); + FileAssert.Exists (aab, $"'{aab}' should have been generated."); + using (var zip = ZipHelper.OpenZip (aab)) { + Assert.IsTrue (zip.Any (e => e.FullName == "BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map"), $"AAB file `{aab}` should contain the ProGuard mapping."); + Assert.IsTrue (zip.Any (e => e.FullName == "BUNDLE-METADATA/com.android.tools/r8.json"), $"AAB file `{aab}` should contain the R8 build metadata."); + } + + Assert.IsTrue (b.Build (proj), "second build should have succeeded."); + foreach (var target in new [] { "_CompileToDalvik", "_BuildApkEmbed" }) { + Assert.IsTrue (b.Output.IsTargetSkipped (target), $"`{target}` should be skipped!"); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index b2956f39da6..eafdb06f3aa 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -230,6 +230,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_AndroidLibraryFlatArchivesDirectory>$(IntermediateOutputPath)flata\ <_AndroidLibraryFlatFilesDirectory>$(IntermediateOutputPath)flat\ <_AndroidStampDirectory>$(IntermediateOutputPath)stamp\ + <_AndroidR8BuildMetadataFile>$(IntermediateOutputPath)r8.json <_AndroidApplicationSharedLibraryPath>$(IntermediateOutputPath)app_shared_libraries\ <_ResolvedUserAssembliesHashFile>$(IntermediateOutputPath)resolvedassemblies.hash <_AndroidResolvedResourcesHashFile>$(IntermediateOutputPath)_AndroidResolvedResources.hash @@ -2270,6 +2271,10 @@ because xbuild doesn't support framework reference assemblies. Condition=" Exists('$(AndroidProguardMappingFile)') " Include="com.android.tools.build.obfuscation/proguard.map:$(AndroidProguardMappingFile)" /> + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index 0796e933155..5f197b76ffa 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -73,6 +73,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. ProguardGeneratedReferenceConfiguration="$(_ProguardProjectConfiguration)" ProguardGeneratedApplicationConfiguration="$(IntermediateOutputPath)proguard\proguard_project_primary.cfg" ProguardMappingFileOutput="$(AndroidProguardMappingFile)" + BuildMetadataFileOutput="$(_AndroidR8BuildMetadataFile)" ProguardConfigurationFiles="@(_ProguardConfiguration)" EnableShrinking="$(_R8EnableShrinking)" EnableMultiDex="$(AndroidEnableMultiDex)" @@ -109,6 +110,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. +