Skip to content

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11783

Open
simonrozsival wants to merge 1 commit into
dotnet:mainfrom
simonrozsival:dev/simonrozsival/r8-keep-user-java-nativeaot
Open

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11783
simonrozsival wants to merge 1 commit into
dotnet:mainfrom
simonrozsival:dev/simonrozsival/r8-keep-user-java-nativeaot

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Summary

User-authored AndroidJavaSource (.java files with Bind != True) are plain Java the developer adds to their project. They have no managed (.NET) peer, so they are not present in the acw-map (the map of Java types that have a generated Android Callable Wrapper).

When R8 generates the application ProGuard configuration from the acw-map (the else if (!AcwMapFile.IsNullOrEmpty ()) branch in R8.cs — the default for R8 release builds), it only emits -keep rules for the acw-map's managed-mapped Java types. Because user AndroidJavaSource isn't in that map, no keep rule is emitted for it and R8 shrinks it away.

This is most visible on the trimmable NativeAOT path, which enables R8 with shrinking by default (AndroidLinkTool=r8_R8EnableShrinking=True). It made BuildAfterMultiDexIsNotRequired(NativeAOT) fail: the test's huge ManyMethods.java user-Java classes were removed, so the app no longer exceeded the per-dex method limit, multidex was no longer required, and the expected classes2.dex was never produced.

Fixes #11774.

Fix

Pass the user AndroidJavaSource (.java with Bind != True) to the R8 task and emit an explicit keep rule for each so it survives shrinking:

-keep class <package>.<TypeName> { *; }

The type name is <package>.<FileNameWithoutExtension> — Java requires the public top-level type name to match the file name. The package is read from the file's package …; declaration (skipping comments, and bailing once a type/import is reached, since the package declaration must precede them).

  • R8.cs: new JavaSourceFiles input + GetUserJavaTypes()/ReadJavaPackage() helpers; emits the keep rules alongside the existing acw-map keep rules.
  • Xamarin.Android.D8.targets: collect @(AndroidJavaSource) with Bind != True into @(_R8KeepJavaSource) and pass it to the task.

Scope / risk

The keep rules are emitted in the acw-map-generated config branch, which is the default for R8 release builds across runtimes — not NativeAOT-only. The effect is purely additive (more -keep rules), so it can only prevent removal of user-authored Java; it never removes anything that was previously kept. User-authored Java is generally intended to be present (e.g. referenced via JNI/reflection from native), so keeping it is the conservative, correct behavior.

Testing

Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) both pass. The change is also exercised by the full test matrix in #11617, from which it is sliced for focused review.

The trimmable NativeAOT path enables R8 with shrinking (AndroidLinkTool=r8 ->
_R8EnableShrinking=True). When the application ProGuard config is generated from
the acw-map (the default, UseTrimmableNativeAotProguardConfiguration=false), the
R8 task only emits -keep rules for managed-mapped Java types. User-authored
AndroidJavaSource (Bind != true) has no managed peer and is therefore absent from
the acw-map, so R8 shrank it away. This made BuildAfterMultiDexIsNotRequired fail
on NativeAOT: the huge ManyMethods.java classes were removed, so multidex was no
longer required and classes2.dex was never produced.

Pass the user AndroidJavaSource (.java with Bind != true) to the R8 task and emit
'-keep class <package>.<Type> { *; }' for each, so user Java survives shrinking.
The type name is '<package>.<FileNameWithoutExtension>' (Java requires the public
top-level type name to match the file name).

Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 28, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Ensures user-authored AndroidJavaSource (Bind != True) Java classes aren’t removed by R8 shrinking by explicitly passing these sources to the R8 MSBuild task and emitting -keep rules for them during ProGuard config generation. This fits into the build pipeline in Xamarin.Android.Build.Tasks by making R8’s generated application configuration account for Java types that have no managed peer (and thus aren’t present in the ACW map).

Changes:

  • Collect @(AndroidJavaSource) items with Bind != True into @(_R8KeepJavaSource) and pass them into the R8 task invocation.
  • Add JavaSourceFiles input to R8 task and emit -keep class … { *; } rules for derived user Java types alongside existing ACW-map keep rules.
  • Add a lightweight package …; parser to derive fully-qualified type names from .java sources.
Show a summary per file
File Description
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets Passes user AndroidJavaSource items (Bind != True) into the R8 task so it can generate explicit keep rules.
src/Xamarin.Android.Build.Tasks/Tasks/R8.cs Adds JavaSourceFiles input and emits R8 -keep rules for user Java sources by deriving Java type names (package + filename).

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Comment on lines +84 to +103
static string? ReadJavaPackage (string path)
{
foreach (var raw in File.ReadLines (path)) {
var line = raw.Trim ();
if (line.Length == 0 || line.StartsWith ("//", StringComparison.Ordinal) || line.StartsWith ("*", StringComparison.Ordinal) || line.StartsWith ("/*", StringComparison.Ordinal)) {
continue;
}
if (line.StartsWith ("package ", StringComparison.Ordinal)) {
var end = line.IndexOf (';');
if (end > "package ".Length) {
return line.Substring ("package ".Length, end - "package ".Length).Trim ();
}
}
// The package declaration, if present, must precede any type declaration.
if (line.StartsWith ("import ", StringComparison.Ordinal) || line.Contains ("class ") || line.Contains ("interface ") || line.Contains ("enum ")) {
break;
}
}
return null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TrimmableTypeMap] R8 shrinking removes unreferenced AndroidJavaSource (user Java) classes under NativeAOT

2 participants