Step 3 of openspec/changes/upgrade-agp-9-1-1 is the major-version
hop: 8.13.2 → 9.0.x (followed by 9.0.x → 9.1.1 at Step 4). The
realised sequence after the Step 2 collapse is 8.12.3 → 8.13.2 → 9.0.x
→ 9.1.1. On opening the step, :androidApp:assembleDebug and
:composeApp:tasks :composeApp:desktopJar succeed cleanly under AGP
9.0.0, 9.0.1, and 9.1.1 (with the two source edits the AGP
9 release notes call out — see Fix below). The ProGuard/R8 step
(:androidApp:assembleRelease → :androidApp:minifyReleaseWithR8)
fails identically on all three AGP versions:
1
2
3
4
5
6
7
8
9
ERROR: …/transforms/…/transformed/core-1.18.0-runtime.jar:
R8: java.lang.NullPointerException:
Cannot invoke "com.android.tools.r8.androidapi.f$a.a()"
because the return value of "com.android.tools.r8.androidapi.f.H()" is null
> Task :androidApp:minifyReleaseWithR8 FAILED
> Compilation failed to complete,
position: Landroidx/core/app/ActivityCompat;->$r8$lambda$PEvuXAyy2Ld2s8FnpyspwjlByy4(Landroid/app/Activity;)V,
origin: …/core-1.18.0-runtime.jar:androidx/core/app/ActivityCompat.class
The exact failing lambda rotates between ActivityCompat,
LocationManagerCompat, and ViewGroupCompat from invocation to
invocation (it’s whichever class R8 reaches first), but the crash
site is always the same: an R8 internal Android-API DB lookup
(com.android.tools.r8.androidapi.f.H()) returning null for one
of the prebuilt $r8$lambda$* synthetic dispatch methods that ship
inside androidx.core:core:1.18.0. Stack trace identifies the R8
build as R8_9.1.31_… when overridden via the buildscript classpath,
or the AGP-bundled R8 (9.0.32 for AGP 9.0.x, 9.1.31 for AGP 9.1.1)
otherwise — the bug is the same in both.
:androidApp:assembleDebug is unaffected because debug builds skip
R8 (isMinifyEnabled = false). The release build is mandatory: the
Verify Agent Ghost workflow installs and smoke-tests the release
.deb outputs.
The bundled R8 in AGP 9.0.x and 9.1.1 has a regression in its
Android-API database lookup for synthetic lambda dispatch methods
that androidx.core:core 1.18.0 generates as part of its own R8
processing. The R8 method com.android.tools.r8.androidapi.f.H()
returns null for those $r8$lambda$* symbols, and the immediate
caller dereferences the return value without a null check, throwing
the NPE quoted above. This is in R8’s internal code path, not in
anything our androidApp/proguard-rules.pro controls.
Empirical investigation (run against this repo’s actual dependency
surface — Compose Multiplatform 1.10.3, androidx-core-ktx = 1.18.0,
compileSdk = 36, minSdk = 30, JDK 21):
ActivityCompat
$r8$lambda$PEvuXAyy2Ld2s8FnpyspwjlByy4.LocationManagerCompat
$r8$lambda$z8e3G9khg88yB8-hC19d9V6P1Gc.Workarounds tested (each tried independently — none change the failure):
build.gradle.kts buildscript {
classpath("com.android.tools:r8:9.1.31") } — bundled R8 in AGP 9.x
is not overridable at the consumer level (the classpath entry
loads but AGP keeps using its bundled copy). Tested with R8 9.1.31
to be sure; same NPE.android.r8.fullMode=false in gradle.properties — no change.-keep class androidx.core.** { *; } in proguard-rules.pro — does
not bypass R8’s lambda-inspection pass; the NPE fires before the
keep rules are consulted.androidx-core-ktx to 1.16.0 in the catalog — the
resolved androidx.core:core is still 1.18.0 because Compose MP
1.10.3 and androidx-lifecycle 2.10.0 both transitively require
≥ 1.18.0. Downgrading the transitive dep would require coordinating
bumps to the entire Compose MP / AndroidX surface, which is out of
scope per Decision 5 of design.md (“defer Kotlin / Compose plugin
/ KSP bumps unless forced”).isMinifyEnabled = true would unblock the build but
loses the obfuscation that proguard-rules.pro exists to apply
(the file’s stated goal is “Protect IP in ONLY these packages:
krill.zone.shared.krillapp.**”). Per workflow.md “do not use
destructive actions as a shortcut”, that’s not a fix — it’s
silently weakening the release artefact.The rational landing point given those constraints is AGP 8.13.2
— the final 8.x release per Section 3’s lessons entry — until
upstream ships an R8 patch that handles androidx.core 1.18.0’s
prebuilt lambdas. This matches the resolution path Open Question Q1
in design.md already authorised: “ceiling-out at 8.13.2 — the
final 8.x release per Section 3’s empirical finding — is an
acceptable landing point”.
Documentation-only PR. No gradle/libs.versions.toml edit, no source
or build-script change.
openspec/changes/upgrade-agp-9-1-1/tasks.md — Section 4 (“Step 3 —
Cross the major: bump to AGP 9.0.x”) is rewritten with an
empirical-finding banner; sub-tasks 4.1–4.15 are checked off with
the actual disposition (mostly “N/A — AGP not bumped” or “done as
part of the empirical investigation”). Section 5 (Step 4) is marked
“N/A while R8 9.x bug stands” with each sub-task deferred until
upstream ships a fix. Section 6 (Step 5) is closed out — the
landed ceiling of 8.13.2 is recorded; the spec text is data-driven
on the catalog so no scenario edit is required.openspec/changes/upgrade-agp-9-1-1/design.md — Decision 1 gets a
“Second empirical correction (2026-05-05, recorded during Step 3)”
block recording the R8 9.x NPE finding and the further-collapsed
one-hop sequence; Q1 is marked Resolved with the empirical
outcome (“plugin compat is fine; bundled R8 is the ceiling”); the
Migration Plan Step 3/4/5 bullets are updated to match.openspec/changes/upgrade-agp-9-1-1/proposal.md — the “What
Changes” bullet is annotated with the second correction note so
the proposal reads consistently with the design.docs/lessons/2026-05-05-agp-9-r8-blocking-bug.md (this file).For the avoidance of doubt, the source edits that the AGP 9 release notes do call for — and that would be in scope if the R8 bug weren’t blocking the bump — were verified to apply cleanly during the investigation:
androidApp/build.gradle.kts — drop alias(libs.plugins.kotlin.android)
from the plugins { … } block; AGP 9 auto-applies built-in Kotlin
compiler support for com.android.application, and keeping the
standalone plugin produces “the ‘org.jetbrains.kotlin.android’
plugin is no longer required for Kotlin support since AGP 9.0”.composeApp/build.gradle.kts — switch alias(libs.plugins.androidLibrary)
to alias(libs.plugins.androidKmpLibrary) and migrate the standalone
android { … } block + androidTarget { compilerOptions { … } }
into a kotlin { androidLibrary { namespace = …; compileSdk = …;
minSdk = …; compilations.configureEach { compileTaskProvider.configure
{ compilerOptions { jvmTarget = JVM_21; freeCompilerArgs +=
"-Xexpect-actual-classes" } } } } } block. AGP 9’s com.android.library
kotlinMultiplatform collide on the kotlin extension; only the
com.android.kotlin.multiplatform.library plugin is supported for
KMP-Android modules. Documented at
https://developer.android.com/kotlin/multiplatform/plugin (note:
the androidLibrary { } DSL is itself deprecated since AGP
9.1.0-alpha09 and replaced by an android { } block — for the
current 8.13.2 ceiling neither edit lands, but both will be
reproduced when this PR’s deferred work resumes).These edits stay in this lessons entry rather than the build files, so the next dev-agent run that reaches Step 3 (after R8 is fixed) can grab them as a starting point.
Sanity check on the spec-correction branch:
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew help --warning-mode=all
is BUILD SUCCESSFUL against the unchanged AGP 8.13.2. The full
test gate, :androidApp:assembleDebug, :composeApp:desktopJar, and
:androidApp:assembleRelease are unchanged from main (PR #226’s
verification) and not re-run for this docs-only PR.
:androidApp:assembleRelease before assuming an AGP
bump is green. :androidApp:assembleDebug skips R8 entirely
(isMinifyEnabled = false), so it cannot detect bundled-R8
regressions. This is exactly why tasks.md task 4.9 calls out
the release build as its own checkpoint — without it, the
R8 NPE would have shipped through to QA verification on Ghost’s
runner, which would have failed the same way one stage later
with much harder context to recover. The pattern from the staged
plan is correct; this finding validates it.buildscript { classpath("com.android.tools:r8:X.Y.Z") } recipe
that worked through AGP 7/8 no longer overrides AGP’s bundled
copy. If a future major-version bump is blocked on an R8 bug, the
options are (a) wait for an AGP patch, (b) downgrade the offending
transitive library if the surface allows, or (c) disable
isMinifyEnabled and accept the loss of obfuscation. There is no
fourth path involving an out-of-band R8 swap. Don’t waste an
investigation cycle re-trying the buildscript-classpath recipe at
the next AGP cliff.[friction] issue against krill-agents (this PR
does not — Section 7 of tasks.md already plans the
krill-agents follow-up “after series merges”, and an
agp-r8-watch line item belongs there once a future series
re-opens) and re-check dl.google.com/android/maven2/com/android/tools/r8/maven-metadata.xml
plus the Google issue tracker every few weeks. When R8 ships a
fix, the deferred Section 5 sub-tasks become executable.:androidApp:assembleRelease against the current dependency
surface actually completes, before writing any code change. The
exploratory bumps in this PR are what surfaced the blocker; a
bump-first-test-later approach would have wasted churn on plugin
edits that ultimately rolled back.