[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
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 withBind != Trueto theR8MSBuild task via a newJavaSourceFilesinput. - Adds
R8.GetUserJavaTypes()+R8.ReadJavaPackage()to derive the fully-qualified class names from.javasources and append-keeprules. - Adds unit tests for
ReadJavaPackageparsing 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
* 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>
Member
Author
|
/review |
jonathanpeppers
approved these changes
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' " /> |
Member
There was a problem hiding this comment.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
-keeprules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authoredAndroidJavaSource.javafiles markedBind!=truehave 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 aNoClassDefFoundErrorthat only reproduces in Release/shrunk builds).Fix
Pass user
AndroidJavaSource(Bind!=True) to theR8task and emit-keep class <package>.<Type> { *; }for each, so user Java survives shrinking:D8.targets— collect@(AndroidJavaSource)items where%(Bind) != 'True'into_R8KeepJavaSourceand pass them toR8via the newJavaSourceFilesproperty.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 thepackagedeclaration, skipping comments and stopping at the firstimport/type declaration. Names are de-duplicated.R8Tests.cs— unit tests coveringReadJavaPackage: package present, trailing space before;, comment headers, no package, andpackageafterimport/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
AndroidJavaSourcewas at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.Context / related PRs
main: [TrimmableTypeMap] Honor AndroidApplicationJavaClass for the JCW base and manifest #11794 (AndroidApplicationJavaClass/multidex), [TrimmableTypeMap] Complete trimmable manifest generation parity #11796 (manifest generation parity). Other merged groundwork: [TrimmableTypeMap] Fix inherited generic base typemap refs #11749, [TrimmableTypeMap] Skip unresolvable trimmable typemap peers #11751, [TrimmableTypeMap] Fix trimmable manifest generation parity #11752, [TrimmableTypeMap] Add trimmable array proxy mapping #11753, [TrimmableTypeMap] Improve trimmable typemap build incrementality #11764, [TrimmableTypeMap] Use non-generic JavaPeerProxy base for interface proxies (NativeAOT) #11769.Verification
24/24 R8 unit tests pass (18 existing + 6 new).
BuildAfterMultiDexIsNotRequiredverified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.