Skip to content

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

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java
Jun 29, 2026
Merged

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating -keep rules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authored AndroidJavaSource .java files marked Bind != true have no managed peer, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a NoClassDefFoundError that only reproduces in Release/shrunk builds).

Fix

Pass user AndroidJavaSource (Bind != True) to the R8 task and emit -keep class <package>.<Type> { *; } for each, so user Java survives shrinking:

  • D8.targets — collect @(AndroidJavaSource) items where %(Bind) != 'True' into _R8KeepJavaSource and pass them to R8 via the new JavaSourceFiles property.
  • R8.cs — append keep rules in the same block that emits acw-map keeps. GetUserJavaTypes() derives the fully-qualified name as <package>.<FileNameWithoutExtension> (Java requires the public top-level type name to match the file name); ReadJavaPackage() parses the package declaration, skipping comments and stopping at the first import/type declaration. Names are de-duplicated.
  • R8Tests.cs — unit tests covering ReadJavaPackage: package present, trailing space before ;, comment headers, no package, and package after import/type (ignored).

Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, it is not trimmable-specific — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user AndroidJavaSource was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

Context / related PRs

Verification

24/24 R8 unit tests pass (18 existing + 6 new). BuildAfterMultiDexIsNotRequired verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

simonrozsival and others added 2 commits June 29, 2026 09:54
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>
Cover package detection: present, trailing space, comment headers, no
package, and package-after-import/type (ignored). Exposes ReadJavaPackage
as internal for direct testing, matching the TryGetDisallowedOption pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 29, 2026 07:59
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

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

Fixes R8 shrinking removing user-authored AndroidJavaSource .java files (those without a managed peer / absent from acw-map) by deriving their Java FQNs and emitting -keep class … { *; } rules when shrinking is enabled.

Changes:

  • Passes @(AndroidJavaSource) items with Bind != True to the R8 MSBuild task via a new JavaSourceFiles input.
  • Adds R8.GetUserJavaTypes() + R8.ReadJavaPackage() to derive the fully-qualified class names from .java sources and append -keep rules.
  • Adds unit tests for ReadJavaPackage parsing behavior across several common source layouts.
Show a summary per file
File Description
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets Collects non-binding AndroidJavaSource items and forwards them to R8 as JavaSourceFiles.
src/Xamarin.Android.Build.Tasks/Tasks/R8.cs Adds Java package parsing + FQN derivation and appends -keep rules to the generated R8 app config.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs Adds test coverage for ReadJavaPackage() parsing rules.

Copilot's findings

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

Comment thread src/Xamarin.Android.Build.Tasks/Tasks/R8.cs
* R8Tests: expected parameter is string? (TestCase passes null).
* R8: comment that only the public top-level type is kept, and that
  ReadJavaPackage is a lightweight scan (package precedes types in practice).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
Comment on lines +54 to +56
<!-- User-authored AndroidJavaSource (Bind != True) is not in the acw-map; pass it to R8 so it
is kept when shrinking is enabled rather than silently removed. -->
<_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />

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.

There is not an easy way to turn this off if you don't want it, but I can't think of a case you would need to do that.

@jonathanpeppers jonathanpeppers merged commit e86f075 into main Jun 29, 2026
40 checks passed
@jonathanpeppers jonathanpeppers deleted the dev/simonrozsival/r8-keep-user-java branch June 29, 2026 13:27
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 ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants