Skip to content

[r8] Add configurable private-member obfuscation and optimization - #12668

Merged
jonathanpeppers merged 12 commits into
dotnet:mainfrom
jonathanpeppers:jonathanpeppers-selective-r8-obfuscation
Sep 10, 2026
Merged

jonathanpeppers merged 12 commits into
dotnet:mainfrom
jonathanpeppers:jonathanpeppers-selective-r8-obfuscation

Conversation

@jonathanpeppers

@jonathanpeppers jonathanpeppers commented Sep 3, 2026 •

Copy link
Copy Markdown
Member

Description

Add the public $(AndroidR8ObfuscationMode) build property and enable selective Java obfuscation and R8 optimization by default in .NET 11.

Mode Obfuscation Optimization Default
disabled Disabled; all Java names are preserved Uses the legacy non-optimizing Android R8 configuration .NET 10
private-members Private and package-private members may be renamed Uses proguard-android-optimize.txt .NET 11 and later

Both modes continue to use R8 shrinking. The property was introduced in .NET 10 servicing with disabled as its default so servicing updates preserve existing behavior. .NET 11 changes the default to private-members.

The selective rules preserve JNI-sensitive Java class names and public/protected member names without rewriting managed IL:

-keep,allowshrinking,allowoptimization class **
-keepclassmembers,allowshrinking,allowoptimization class ** {
   public protected *;
}
-keep,allowoptimization interface ** {
   public protected *;
}
-keep,allowshrinking class * implements **

Interfaces and their surviving implementation classes are retained from optimization because managed proxy selection observes Class.getInterfaces(). R8 cannot infer that reflective dependency. Keeping only interface declarations is insufficient: horizontal class merging can still remove a derived-interface relationship from an implementation class, causing managed code to select a base-interface proxy. The implementation rule still permits unused classes to be shrunk.

Runtime rules also preserve net.dot.android.ApplicationRegistration, which managed code loads by name. NativeAOT preserves the package-private java.lang.Class fields on mono.android.Runtime because native hosts resolve them by literal name during startup.

The previous private $(_AndroidR8DontObfuscate) and $(_AndroidR8DontOptimize) switches are removed; $(AndroidR8ObfuscationMode)=disabled is the supported compatibility path.

Tests

  • Added R8ObfuscationTests device coverage for CoreCLR and NativeAOT using an AndroidJavaSource probe. It verifies stable class/public/protected names, renamed private/package-private methods, and successful JNI execution.
  • Added task coverage for the exact disabled and private-members rule sets, including implementation-class preservation.
  • Extended AAB metadata coverage for CoreCLR and NativeAOT to verify both modes' optimization state and incremental behavior.
  • Updated affected R8 app-size baselines.

Results

Currently, Google Play reports a dotnet new maui -sc project template as:

  • Optimization percentage: 62%
  • Obfuscation percentage: 28%
  • Shrinking percentage: 65%

This is an initial implementation that gets us past the 25% requirement for now.

Preserve Java class names and public/protected member names while allowing R8 to obfuscate private and package-private members. Keep _AndroidR8DontObfuscate as a private escape hatch that only restores the previous global -dontobfuscate behavior.

Co-authored-by: Copilot App <[email protected]>

Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8

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.

🟡 Changes recommended

The new device test can leave apps installed on failure paths, which risks flakiness and cross-test interference in device CI runs.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR changes the default R8 behavior to allow obfuscation of private/package-private Java implementation details while keeping Java class names and public/protected member names stable for JNI compatibility, with a private MSBuild escape hatch to restore the previous global -dontobfuscate behavior.

Changes:

  • Add a new selective-obfuscation default by generating base ProGuard/R8 rules at build time (and wiring an internal $(_AndroidR8DontObfuscate) toggle).
  • Update embedded ProGuard config resources to no longer globally disable obfuscation, and add a NativeAOT-specific keep rule for mono.android.Runtime startup fields.
  • Add device integration tests validating mapping + DEX output + runtime execution for CoreCLR and NativeAOT.
File summaries
File Description
tests/MSBuildDeviceIntegration/Tests/R8ObfuscationTests.cs Adds device test coverage verifying selective obfuscation via mapping/DEX inspection and a runtime probe.
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets Plumbs a private MSBuild escape hatch property into the R8 task invocation.
src/Xamarin.Android.Build.Tasks/Tasks/R8.cs Implements default selective-obfuscation ProGuard directives generation and supports DontObfuscate.
src/Xamarin.Android.Build.Tasks/Resources/proguard_xamarin.cfg Removes the global -dontobfuscate default from the common R8 config resource.
src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg Removes -dontobfuscate and adds a keep rule for mono.android.Runtime fields needed at NativeAOT startup.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread tests/MSBuildDeviceIntegration/Tests/R8ObfuscationTests.cs Outdated
Track successful installation and attempt uninstall from a finally block so assertion failures do not leave packages on the test device.

Co-authored-by: Copilot App <[email protected]>

Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
Co-authored-by: Copilot App <[email protected]>

Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
@jonathanpeppers

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #12668

@github-actions github-actions Bot 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.

⚠️ Needs Changes — 0 errors, 1 warning, 0 suggestions.

The selective R8 rules preserve class and JNI-visible public/protected member names while allowing private implementation details to be renamed, and the runtime-specific keep configuration is consistent with the native field lookups. The new device coverage exercises both CoreCLR and NativeAOT, but its shared logcat marker is not isolated between parameterized cases and can produce a false pass; please address the inline testing comment. CI is green.

Generated by Android PR Reviewer for #12668 · gpt56 · 125.5 AIC · ⌖ 9.02 AIC · ⊞ 25.7K
Comment /review to run again

Comment thread tests/MSBuildDeviceIntegration/Tests/R8ObfuscationTests.cs
@jonathanpeppers

Copy link
Copy Markdown
Member Author

The changes here, get us this far with a dotnet new maui -sc project template:

image

Previously, obfuscation was at 0%.

I'm investigating what else is missing, if we should add more changes here or a separate PR.

Use Android's optimized default R8 configuration unless the private _AndroidR8DontOptimize escape hatch is enabled. Ship both configurations and preserve ApplicationRegistration, which is loaded by name from managed code.

Co-authored-by: Copilot App <[email protected]>

Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
@jonathanpeppers jonathanpeppers changed the title [r8] Obfuscate private Java members by default [r8] Enable private-member obfuscation and optimization by default Sep 4, 2026
@jonathanpeppers

Copy link
Copy Markdown
Member Author

This is now getting this far:

image

jonathanpeppers and others added 3 commits September 7, 2026 10:31
Co-authored-by: Copilot App <[email protected]>

Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
Keep Java interface hierarchy visible to managed proxy selection while still allowing R8 optimization.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
Replace the private R8 obfuscation and optimization switches with AndroidR8ObfuscationMode. The disabled mode restores legacy behavior while private-members is the .NET 11 default.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
@jonathanpeppers jonathanpeppers changed the title [r8] Enable private-member obfuscation and optimization by default [r8] Add configurable private-member obfuscation and optimization Sep 8, 2026
Co-authored-by: Copilot App <[email protected]>

Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
Co-authored-by: Copilot App <[email protected]>

Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
@simonrozsival

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #12668

@github-actions github-actions Bot 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.

❌ Reject

Findings: 1 error · 0 warnings · 0 suggestions. The new public enum-style property accepts arbitrary values and silently maps them to the obfuscating/optimizing mode; this needs validation before merge.

CI build 1587708 is not green: Package Tests macOS > Tests > APKs 2 is failing, two Windows MSBuild lanes remain in progress, and the aggregate check is queued. The existing unresolved device-test isolation thread also remains open.

The incremental cache input, mode-specific metadata coverage, and CoreCLR/NativeAOT runtime coverage are strong additions.

Generated by Android PR Reviewer for #12668 · gpt56 · 397.2 AIC · ⌖ 8.91 AIC · ⊞ 25.7K
Comment /review to run again

Comment thread src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
Prevent R8 class optimization from changing reflection-visible interface relationships used by managed proxy selection.

Co-authored-by: Copilot App <[email protected]>

Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
Normalize the generated R8 rule expectation to platform line endings.

Co-authored-by: Copilot App <[email protected]>

Copilot-Session: cf67f43d-2eef-46ac-9d52-e9a677671fc8
simonrozsival added a commit that referenced this pull request Sep 10, 2026
Use AndroidR8ObfuscationMode as the sole opt-in, place its behavior-preserving default with the shared R8 defaults, and keep runtime-specific derivation and validation late. This prepares the branch to add runtime-remapping to the mode contract from PR #12668 after it merges.

Co-authored-by: Copilot App <[email protected]>
@jonathanpeppers jonathanpeppers added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Sep 10, 2026
@jonathanpeppers
jonathanpeppers enabled auto-merge (squash) September 10, 2026 13:25
@jonathanpeppers
jonathanpeppers merged commit da55b86 into dotnet:main Sep 10, 2026
44 checks passed
jonathanpeppers added a commit that referenced this pull request Sep 10, 2026
## Summary

Backports the complete relevant selective R8 obfuscation/optimization work from #12668 to .NET 10 servicing and incorporates the follow-up validation changes from #12753.

- keeps `$(AndroidR8ObfuscationMode)` defaulted to `disabled` on .NET 10, preserving existing behavior
- makes `private-members` available as an explicit opt-in, enabling R8 optimization while preserving Java class/interface and public/protected member names
- adds `XA1050` validation, English resources, documentation, and message documentation for unsupported values
- gates mode validation on `$(AndroidLinkTool) == r8`, so unrelated builds are unaffected
- adds task, packaging, incremental-build, invalid-mode, R8-only validation, and device coverage
- installs both Android default R8 configuration variants

## Release-branch adaptations

The release branch predates main's NativeAOT `R8Mapping` and user-Java-source rooting infrastructure. Device coverage is therefore adapted to MonoVM and CoreCLR, reads `mapping.txt` directly, and supplies the explicit keep rule needed for the Java probe. The release task's existing `string[]` configuration-file shape is retained.

No `.apkdesc` baselines are changed: unlike main, the .NET 10 servicing default remains `disabled`, and measured targeted packaging/device coverage did not require release baseline updates. No unrelated Java.Interop version changes are included.

## Validation

- built `Xamarin.Android.sln` and configured the repository local Android workload
- built `Xamarin.Android.Build.Tests` and `MSBuildDeviceIntegration`
- R8 task and packaging matrix: 9/9 passed, covering default, explicit `disabled`, explicit `private-members`, incremental builds, metadata/optimization state, invalid-mode `XA1050`, and validation with R8 enabled/disabled
- physical-device R8 coverage: 2/2 passed on MonoVM and CoreCLR
- changed XML files parse successfully
- `git diff --check` passes
- final review reported no findings

Backport of #12668; includes #12753.

Co-authored-by: Copilot App <[email protected]>
simonrozsival pushed a commit that referenced this pull request Sep 11, 2026
## Description

Follow-up to #12668 and review feedback on #12752.

Ports `_ValidateAndroidR8ObfuscationMode` to `main` and gates it on `$(AndroidLinkTool) == 'r8'`. Invalid `AndroidR8ObfuscationMode` values report XA1050 only when R8 is selected, while non-R8 builds remain unaffected. This preserves the behavior and `private-members` default introduced by #12668.

## Tests

- `dotnet-local.cmd build src\Xamarin.Android.Build.Tasks\Tests\Xamarin.Android.Build.Tests\Xamarin.Android.Build.Tests.csproj -v:minimal --no-restore`
- `dotnet-local.cmd test bin\TestDebug\net10.0\Xamarin.Android.Build.Tests.dll --filter "Name~InvalidR8ObfuscationModeIsValidatedOnlyForR8" -v:minimal`
  - invalid mode without R8 succeeds
  - invalid mode with R8 fails with XA1050
- XML parsing for `Xamarin.Android.Common.targets` and `Resources.resx`
- `git diff --check`
simonrozsival added a commit that referenced this pull request Sep 21, 2026
## Summary

Remove the experimental build-time managed-assembly rewriting approach for R8 so any future assembly-rewriting design can start from a clean foundation.

This cleanup is related to #12535 and the original prototype in #12575.

## What this PR undoes

This removes the managed rewriting implementation developed across the R8 obfuscation stack:

- #12629 added the PE metadata rebuild substrate.
- #12630 added managed JNI metadata rewriting from R8 mappings.
- #12631 added rewriting for generated trimmable type-map assemblies.
- #12632 and #12634 integrated and tested that rewriting approach for CoreCLR and NativeAOT.

Concretely, this PR removes the rewrite task, rewrite-only metadata/IL utilities, dedicated tests and fixtures, and the XA4325/XA4326 resources and documentation. The intent is to abandon this implementation rather than preserve an unused rewriting stack that a future design would need to work around.

## What remains in place

- The generic `R8Mapping` parser introduced in #12628 remains, along with its tests and the `MSBuildDeviceIntegration` consumer. It is independently useful for reading R8 mapping files.
- The ordinary R8 configuration and `private-members` obfuscation/optimization policy from #12668 remain unchanged. This PR does **not** disable R8 or remove private-member obfuscation.
- Existing D8/R8 packaging, ProGuard rule handling, and non-rewriting build behavior remain unchanged.
- Runtime remapping remains active as the stacked follow-up work in #12847, #12848, #12692, and #12844. Those PRs implement the alternative opt-in strategy without managed assembly rewriting and are not part of this cleanup diff.

We may revisit managed assembly rewriting in .NET 12 based on customer feedback and performance data, but with a fresh design rather than this implementation.

## Validation

- Built `Xamarin.Android.Build.Tasks`
- Ran `Microsoft.Android.Build.Tasks.Tests`
- Ran `Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests`
- Built `MSBuildDeviceIntegration`
- Ran focused `R8MappingTests`
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants