dotnet / dotnet/android

R8: unconditional -dontobfuscate makes Google Play's Feb 2027 25% obfuscation requirement unachievable

Open
#12,535 19 comments 8 reactions 1 assignee Claimed by @simonrozsival View on GitHub
Area: App+Library Build
Dominant language
C#
Stars
2.1k
Forks
579
Avg merge
1d 19h
Merged PRs (30d)
252

Description

### Summary

`proguard_xamarin.cfg` — the config the SDK generates and passes to R8 on every `$(AndroidLinkTool)=r8` build — contains an unconditional `-dontobfuscate`. There is no supported property to remove or override it, and no R8 CLI flag that can undo it. The result is that **a .NET for Android app cannot produce an obfuscated `classes*.dex` at all.**

That was a cosmetic difference until this month. Google Play has now announced a code-optimization requirement that makes it a compliance problem:

> From February 2027, apps and games on Google Play will need to meet minimum optimization requirements. You will need to achieve a minimum of **25% optimization, obfuscation and shrinking** for any app uploads to Play Console.

The requirement is scoped to non-negligible DEX: **>10 MB for apps**, >50 MB for games. Announcement: . Apps that miss the thresholds "may see store visibility impact" and may get a warning shown on their store listing.

Because obfuscation is one of the three metrics and .NET for Android pins it at zero, **every .NET MAUI / .NET for Android app above 10 MB of DEX fails that metric by construction**, regardless of how the developer configures their project.

### Measured on a shipping app

.NET 10, `Microsoft.Android.Sdk` **36.1.69**, `$(AndroidLinkTool)=r8`, Release, built on `ubuntu-latest`. Measured on the signed APK actually shipped to users:

```
classes.dex 8,570,180
classes2.dex 8,354,084
classes3.dex 7,644,760
classes4.dex 1,745,504
----------
26,314,528 bytes = 25.1 MB DEX
```

R8 markers are present in all four, and full mode is in use:

```
~~R8{"backend":"dex","compilation-mode":"release","has-checksums":false,
"min-api":26,"pg-map-id":"3bb8354","r8-mode":"full","version":"8.11.18"}
```

So shrinking and optimization do run. Obfuscation does not — **zero** renamed class descriptors in any of the four files:

```
Landroidx/activity/R$id;
Landroidx/activity/result/contract/ActivityResultContracts$PickVisualMedia$MediaCapabilities$$ExternalSyntheticApiModelOutline0;
...
```

Count of descriptors matching a minified shape (`L(a-z0-9{1,2}/)+a-z0-9{1,2};`): **0**.

### Root cause

`proguard_xamarin.cfg` is an embedded resource in `Xamarin.Android.Build.Tasks.dll` and begins:

```
# This is Xamarin-specific (and enhanced) configuration.

-dontobfuscate

-keep class android.support.multidex.MultiDexApplication { (); }
-keep class net.dot.jni.** { *; (); }
-keep class mono.MonoRuntimeProvider* { *; (...); }
...
```

`R8.cs` writes it out inside `CreateResponseFile()` immediately before invoking `r8`, so no MSBuild target can get between generation and use. `_CalculateProguardConfigurationFiles` (`Xamarin.Android.Common.targets`) then includes it in `@(_ProguardConfiguration)`, which `Xamarin.Android.D8.targets` passes as `ProguardConfigurationFiles`, and `R8.cs` emits one `--pg-conf` per entry.

The resource carries no comment explaining the flag, and I could not find an issue or PR discussing the decision.

### Why the obvious workarounds do not work

**1. `$(AndroidR8ExtraArguments)` cannot force minification back on.** R8 8.11.18 (the version shipped in `tools/r8.jar`) has `--no-tree-shaking`, `--no-minification`, `--no-data-resources` and `--no-desugaring`, and **no positive counterpart to any of them** — there is no `--minification` / `--obfuscate`.

**2. Appending another `--pg-conf` cannot cancel it.** `-dontobfuscate` is order-independent and sticky, which I verified directly against the shipped `r8.jar` on a two-class fixture:

| configs passed | result |
| --- | --- |
| `b.cfg` (no `-dontobfuscate`) | `Ldemo/Helper;` and `secretMethodName` both renamed |
| `a.cfg` (`-dontobfuscate`) then `b.cfg` | both names survive |
| `b.cfg` then `a.cfg` (`-dontobfuscate`) **last** | both names survive |

**3. `$(ProguardConfigFiles)` "works" but is not a safe answer.** It replaces the entire default list, so `proguard_xamarin.cfg` is never passed and `-dontobfuscate` never reaches R8. But the same list also carries `proguard_project_primary.cfg` (the generated `-keep` for every Android Callable Wrapper) and `proguard_project_references.cfg`. Re-listing those by hand is required for the app to launch at all, means maintaining a copy of an SDK-internal keep file that drifts silently against the SDK, and fails as a **launch crash rather than a build error**. The property is also undocumented — it appears on no page under (which has zero hits for "obfuscat" or "minif" anywhere), and its only description in the tree is the `Common.targets` comment "to keep backwards compatibility".

### Note on #11709

(merged 2026-06-23) already deals with disallowed global options in *library* `proguard.txt` files, and states that "the SDK-generated configs (`proguard_xamarin.cfg`, `proguard_project_primary.cfg`, etc.) are left untouched". So the neighbouring code was revisited recently and the global `-dontobfuscate` was deliberately retained — which is why I am raising this as its own issue rather than assuming it is an oversight.

### Ask

Some supported way to opt in to R8 minification, with the SDK continuing to own the keeps that make it safe (ACWs, `mono.*`, `net.dot.jni.*`, bound types via `GenerateProguardConfiguration`). For example an `$(AndroidEnableObfuscation)` / `$(AndroidR8Minification)` property that omits `-dontobfuscate` from the generated config while leaving every generated `-keep` in place.

Two things that would help even without a full opt-in:

1. **Guidance.** If obfuscation is unsafe for .NET for Android for a structural reason, documenting that reason — and Microsoft's position on the February 2027 requirement — would let teams plan. Right now the build properties page does not mention obfuscation at all.
2. **A supported seam**, if the answer is "bring your own config": documenting `$(ProguardConfigFiles)` and giving a way to append to `@(_ProguardConfiguration)` minus the one file, so nobody has to hand-copy generated keeps.

Happy to test a fix against a real 25 MB-DEX app and report the resulting Play Console numbers.

### Environment

- .NET 10, `Microsoft.Android.Sdk.Windows` **36.1.69** (`tools/Version.commit`: `d549e1d`, `release/10.0.1xx`)
- R8 **8.11.18**, `r8-mode: full`, `min-api: 26`
- Release build on `ubuntu-latest`; `$(AndroidLinkTool)=r8`, `$(JavaMaximumHeapSize)=6G`

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.