Skip to content

Commit b4f3cd0

Browse files
[r8] Include build metadata in app bundles (#12646)
## Summary - 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. ## What .NET 10 customers can try now 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 <Project> <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <AndroidLinkTool>r8</AndroidLinkTool> </PropertyGroup> <Target Name="IncludeR8BuildMetadata" BeforeTargets="_CompileToDalvik" Condition=" '$(AndroidLinkTool)' == 'r8' "> <PropertyGroup> <_R8BuildMetadataFile>$(IntermediateOutputPath)r8.json</_R8BuildMetadataFile> <_R8JarFile>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)r8-9.0.32.jar'))</_R8JarFile> <_R8MetadataRsp>$(IntermediateOutputPath)r8-build-metadata.rsp</_R8MetadataRsp> <AndroidR8JarPath>$(_R8JarFile)</AndroidR8JarPath> <AndroidR8ExtraArguments>@$(_R8MetadataRsp)</AndroidR8ExtraArguments> </PropertyGroup> <DownloadFile Condition=" !Exists('$(_R8JarFile)') " SourceUrl="https://storage.googleapis.com/r8-releases/raw/9.0.32/r8.jar" DestinationFolder="$(IntermediateOutputPath)" DestinationFileName="r8-9.0.32.jar" /> <WriteLinesToFile File="$(_R8MetadataRsp)" Overwrite="true" Lines="--build-metadata-output;$(_R8BuildMetadataFile)" /> <ItemGroup> <AndroidAppBundleMetaDataFile Include="com.android.tools/r8.json:$(_R8BuildMetadataFile)" /> </ItemGroup> </Target> </Project> ``` 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>
1 parent 876c57e commit b4f3cd0

4 files changed

Lines changed: 26 additions & 1 deletion

File tree

src/Xamarin.Android.Build.Tasks/Tasks/R8.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class R8 : D8
3434
public string? ProguardGeneratedApplicationConfiguration { get; set; }
3535
public string? ProguardCommonXamarinConfiguration { get; set; }
3636
public string? ProguardMappingFileOutput { get; set; }
37+
public string? BuildMetadataFileOutput { get; set; }
3738
public ITaskItem []? ProguardConfigurationFiles { get; set; }
3839
public bool UseTrimmableNativeAotProguardConfiguration { get; set; }
3940

@@ -122,6 +123,11 @@ protected override string CreateResponseFile ()
122123
// Now append R8-specific arguments to the response file
123124
using var response = new StreamWriter (responseFile, append: true, encoding: Files.UTF8withoutBOM);
124125

126+
if (!BuildMetadataFileOutput.IsNullOrEmpty ()) {
127+
WriteArg (response, "--build-metadata-output");
128+
WriteArg (response, Path.GetFullPath (BuildMetadataFileOutput));
129+
}
130+
125131
if (EnableMultiDex) {
126132
if (MinSdkVersion >= 21) {
127133
if (CustomMainDexListFiles?.Length > 0) {

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Xamarin.Android.Build.Tests
1717
public class PackagingTest : BaseTest
1818
{
1919
[Test]
20-
public void CheckProguardMappingFileExists ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime)
20+
public void CheckR8MetadataFilesExist ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime)
2121
{
2222
const bool isRelease = true;
2323
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
@@ -31,11 +31,23 @@ public void CheckProguardMappingFileExists ([Values (AndroidRuntime.CoreCLR, And
3131
proj.SetProperty (proj.ReleaseProperties, KnownProperties.AndroidLinkTool, "r8");
3232
// Projects must set $(AndroidCreateProguardMappingFile) to true to opt in
3333
proj.SetProperty (proj.ReleaseProperties, "AndroidCreateProguardMappingFile", true);
34+
proj.SetProperty ("AndroidPackageFormat", "aab");
3435

3536
using (var b = CreateApkBuilder ()) {
3637
string mappingFile = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath, "mapping.txt");
3738
Assert.IsTrue (b.Build (proj), "build should have succeeded.");
3839
FileAssert.Exists (mappingFile, $"'{mappingFile}' should have been generated.");
40+
var aab = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath, $"{proj.PackageName}-Signed.aab");
41+
FileAssert.Exists (aab, $"'{aab}' should have been generated.");
42+
using (var zip = ZipHelper.OpenZip (aab)) {
43+
Assert.IsTrue (zip.Any (e => e.FullName == "BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map"), $"AAB file `{aab}` should contain the ProGuard mapping.");
44+
Assert.IsTrue (zip.Any (e => e.FullName == "BUNDLE-METADATA/com.android.tools/r8.json"), $"AAB file `{aab}` should contain the R8 build metadata.");
45+
}
46+
47+
Assert.IsTrue (b.Build (proj), "second build should have succeeded.");
48+
foreach (var target in new [] { "_CompileToDalvik", "_BuildApkEmbed" }) {
49+
Assert.IsTrue (b.Output.IsTargetSkipped (target), $"`{target}` should be skipped!");
50+
}
3951
}
4052
}
4153

src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
230230
<_AndroidLibraryFlatArchivesDirectory>$(IntermediateOutputPath)flata\</_AndroidLibraryFlatArchivesDirectory>
231231
<_AndroidLibraryFlatFilesDirectory>$(IntermediateOutputPath)flat\</_AndroidLibraryFlatFilesDirectory>
232232
<_AndroidStampDirectory>$(IntermediateOutputPath)stamp\</_AndroidStampDirectory>
233+
<_AndroidR8BuildMetadataFile>$(IntermediateOutputPath)r8.json</_AndroidR8BuildMetadataFile>
233234
<_AndroidApplicationSharedLibraryPath>$(IntermediateOutputPath)app_shared_libraries\</_AndroidApplicationSharedLibraryPath>
234235
<_ResolvedUserAssembliesHashFile>$(IntermediateOutputPath)resolvedassemblies.hash</_ResolvedUserAssembliesHashFile>
235236
<_AndroidResolvedResourcesHashFile>$(IntermediateOutputPath)_AndroidResolvedResources.hash</_AndroidResolvedResourcesHashFile>
@@ -2167,6 +2168,10 @@ because xbuild doesn't support framework reference assemblies.
21672168
Condition=" Exists('$(AndroidProguardMappingFile)') "
21682169
Include="com.android.tools.build.obfuscation/proguard.map:$(AndroidProguardMappingFile)"
21692170
/>
2171+
<AndroidAppBundleMetaDataFile
2172+
Condition=" (('$(AndroidLinkTool)' == 'r8' And '$(_ProguardProjectConfiguration)' != '') Or '$(AndroidEnableMultiDex)' == 'True') And Exists('$(_AndroidR8BuildMetadataFile)') "
2173+
Include="com.android.tools/r8.json:$(_AndroidR8BuildMetadataFile)"
2174+
/>
21702175
</ItemGroup>
21712176
</Target>
21722177

src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Copyright (C) 2018 Xamarin. All rights reserved.
7878
ProguardGeneratedReferenceConfiguration="$(_ProguardProjectConfiguration)"
7979
ProguardGeneratedApplicationConfiguration="$(IntermediateOutputPath)proguard\proguard_project_primary.cfg"
8080
ProguardMappingFileOutput="$(AndroidProguardMappingFile)"
81+
BuildMetadataFileOutput="$(_AndroidR8BuildMetadataFile)"
8182
ProguardConfigurationFiles="@(_ProguardConfiguration)"
8283
UseTrimmableNativeAotProguardConfiguration="$(_UseTrimmableNativeAotProguardConfiguration)"
8384
EnableShrinking="$(_R8EnableShrinking)"
@@ -115,6 +116,7 @@ Copyright (C) 2018 Xamarin. All rights reserved.
115116
<ItemGroup>
116117
<FileWrites Include="$(_AndroidIntermediateDexOutputDirectory)*.dex" />
117118
<FileWrites Include="$(AndroidProguardMappingFile)" />
119+
<FileWrites Include="$(_AndroidR8BuildMetadataFile)" Condition=" Exists('$(_AndroidR8BuildMetadataFile)') " />
118120
</ItemGroup>
119121

120122
</Target>

0 commit comments

Comments
 (0)