Skip to content

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

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

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11790
simonrozsival wants to merge 1 commit into
mainfrom
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 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 Java files (with Bind != True) are not removed by R8 shrinking by explicitly generating -keep rules for them during the R8 configuration generation step.

Changes:

  • Collect @(AndroidJavaSource) items with Bind != True and pass them into the R8 MSBuild task.
  • Add JavaSourceFiles input to R8 and generate -keep class <package>.<TypeName> { *; } rules by deriving type names from .java source.
  • Implement package parsing from Java source to construct fully-qualified class names for keep rules.
Show a summary per file
File Description
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets Collects user AndroidJavaSource items and passes them to the R8 task.
src/Xamarin.Android.Build.Tasks/Tasks/R8.cs Adds Java-source-driven keep rule generation to prevent R8 shrinking from removing user Java classes.

Copilot's findings

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

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;
}
Comment on lines +162 to +166
// User-authored AndroidJavaSource (Bind != true) has no managed peer and is absent
// from the acw-map, so keep it explicitly; otherwise shrinking removes it.
foreach (var java in GetUserJavaTypes ()) {
appcfg.WriteLine ($"-keep class {java} {{ *; }}");
}
Comment on lines +68 to +72
foreach (var item in JavaSourceFiles) {
var path = item.ItemSpec;
if (path.IsNullOrEmpty () || !File.Exists (path)) {
continue;
}
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

@jonathanpeppers jonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Were there two of the same PR?

@simonrozsival

Copy link
Copy Markdown
Member Author

Yes, this is a duplicate of #11798, which has already merged. #11798 carries the identical R8.cs / Xamarin.Android.D8.targets changes plus unit tests for ReadJavaPackage, so there's nothing unique here to preserve. Closing in favor of #11798.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants