Raise Kotlin to 2.4.10 so the bytecode can join the toolchain on Java 25

The previous commit settled for Java 24 everywhere because Kotlin 2.2.10 refuses
jvmTarget 25. That was the wrong constraint to accept, for two reasons.

The first is that 24 turned out to be unbuyable. Adoptium's repository carries
8, 11, 17, 21, 25 and 26 -- no 24, because it is a non-LTS that went end of life
in July 2025. The builds passed only because Gradle quietly auto-provisioned
24.0.2+12 through foojay, and .idea/misc.xml had been pointed at a temurin-24
that cannot be installed. A toolchain nobody can install is not pinned, it is
lucky.

The second is that the cap was never on the toolchain at all. Kotlin's ceiling
applies to jvmTarget -- the bytecode -- and the JDK running the build is a
separate axis. Conflating them is what steered this at 24 in the first place.

So the fix is the one the sibling repo already uses: put KGP on the root
buildscript classpath, where AGP's built-in Kotlin picks it up instead of the
2.2.10 it bundles. Kotlin 2.4.10 supports jvmTarget through 26, which lifts the
ceiling above the toolchain rather than under it. The Compose compiler plugin is
versioned in lockstep and reads the same catalog entry, so the two cannot drift,
and the module now applies both by id() because they come from the classpath
rather than from plugin resolution.

Checked rather than assumed, since a silent downgrade would look identical to
success: compiled classes report major version 69, which is Java 25. D8 dexes
them, R8 minifies them, and ktlint, detekt, lint, the unit tests and the
androidTest compile are all green on top.

25 is the right landing place independent of all this: it is LTS, it is in the
Adoptium repository, and temurin-25-jdk is already installed here -- so the
daemon runs on a real system JDK rather than a provisioned copy of an unpatched
one.

Two catalog plugin aliases went with it. android-application and kotlin-compose
now resolve from the buildscript classpath, so leaving aliases behind would have
left two entries that read like the source of truth and control nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 15:05:54 -05:00
co-authored by Claude Opus 5
parent a7aa003020
commit fd6e5325cf
8 changed files with 86 additions and 42 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: temurin distribution: temurin
java-version: '24' # Matches the daemon JVM pinned in gradle/gradle-daemon-jvm.properties java-version: '25' # Matches the daemon JVM pinned in gradle/gradle-daemon-jvm.properties
# Gradle runs through the committed wrapper rather than a setup action. The # Gradle runs through the committed wrapper rather than a setup action. The
# wrapper verifies its own distribution against distributionSha256Sum, and # wrapper verifies its own distribution against distributionSha256Sum, and
@@ -66,7 +66,7 @@ jobs:
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: temurin distribution: temurin
java-version: '24' java-version: '25'
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
+3 -3
View File
@@ -80,7 +80,7 @@ jobs:
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: temurin distribution: temurin
java-version: '24' # Matches the daemon JVM pinned in gradle/gradle-daemon-jvm.properties java-version: '25' # Matches the daemon JVM pinned in gradle/gradle-daemon-jvm.properties
# Gradle is invoked through the committed wrapper rather than a setup action. # Gradle is invoked through the committed wrapper rather than a setup action.
# The wrapper verifies its own distribution against distributionSha256Sum, and # The wrapper verifies its own distribution against distributionSha256Sum, and
@@ -140,7 +140,7 @@ jobs:
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: temurin distribution: temurin
java-version: '24' java-version: '25'
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
@@ -209,7 +209,7 @@ jobs:
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: temurin distribution: temurin
java-version: '24' java-version: '25'
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with: with:
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_24" default="true" project-jdk-name="temurin-24" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="temurin-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">
+17 -7
View File
@@ -9,24 +9,34 @@ this file covers only what is not obvious from the code.
## Build, test, lint ## Build, test, lint
**Everything is on Java 24** — daemon, CI, IDE, and the app's own bytecode. Four places say so and **Everything is on Java 25** — daemon, CI, IDE, and the app's own bytecode. Four places say so and
they must not drift apart: they must not drift apart:
| Where | What sets it | | Where | What sets it |
|---|---| |---|---|
| Gradle daemon | `gradle/gradle-daemon-jvm.properties` → `toolchainVersion=24` | | Gradle daemon | `gradle/gradle-daemon-jvm.properties` → `toolchainVersion=25` |
| CI | `java-version: '24'` in both workflows | | CI | `java-version: '25'` in both workflows |
| IDE | `.idea/misc.xml` | | IDE | `.idea/misc.xml` |
| App bytecode | `compileOptions` in `app/build.gradle.kts` | | App bytecode | `compileOptions` in `app/build.gradle.kts` |
**Do not pick a JDK for the daemon — the repo does.** `gradle-daemon-jvm.properties` carries foojay **Do not pick a JDK for the daemon — the repo does.** `gradle-daemon-jvm.properties` carries foojay
download URLs per platform, so Gradle provisions and runs the daemon on Java 24 regardless of what download URLs per platform, so Gradle provisions and runs the daemon on Java 25 regardless of what
`JAVA_HOME` says (that only sets the *launcher* — `./gradlew --version` prints both). Change it with `JAVA_HOME` says (that only sets the *launcher* — `./gradlew --version` prints both). Change it with
`./gradlew updateDaemonJvm --jvm-version=NN`, never by hand. `./gradlew updateDaemonJvm --jvm-version=NN`, never by hand.
**24, not 25, is deliberate.** Kotlin 2.2.10 refuses `jvmTarget` 25 outright — its available targets **Reaching 25 in the bytecode row took a deliberate build change.** AGP 9's built-in Kotlin compiles
stop at 24 — so the app's bytecode cannot join a 25 toolchain, and 24 is the highest number all four with the KGP it bundles — 2.2.10 for AGP 9.3.1 — and that caps `jvmTarget` at 24. The root
rows can actually hold. D8 dexes Java 24 class files and R8 minifies them, both verified. `build.gradle.kts` puts KGP (and the lockstep Compose compiler plugin) on the buildscript classpath
so AGP picks up 2.4.10 instead, which supports up to 26. That is why the module applies
`com.android.application` and the Compose plugin by `id()` rather than from the catalog. Verified end
to end, not assumed: compiled classes report major version 69, D8 dexes them, and R8 minifies them.
Consequences worth knowing before touching any of it:
- Raising `kotlin` requires a matching `compose-compiler-gradle-plugin`; they are one version.
- Still **do not** apply `org.jetbrains.kotlin.android` — incompatible with AGP 9's DSL.
- Java 24 is *not* an option even though Kotlin allows it: Adoptium dropped the EOL non-LTS, so
there is no installable temurin-24. 25 is LTS and in the repo.
The Gradle wrapper does **not** float and cannot: `distributionUrl` names one archive and The Gradle wrapper does **not** float and cannot: `distributionUrl` names one archive and
`distributionSha256Sum` is that file's checksum. Bump it with `./gradlew wrapper --gradle-version X `distributionSha256Sum` is that file's checksum. Bump it with `./gradlew wrapper --gradle-version X
+6 -4
View File
@@ -1,9 +1,11 @@
import org.gradle.testing.jacoco.tasks.JacocoReport import org.gradle.testing.jacoco.tasks.JacocoReport
plugins { plugins {
alias(libs.plugins.android.application) // Applied by id: these two come from the root buildscript classpath, which is what
// overrides AGP's bundled Kotlin. See the comment in the root build file.
id("com.android.application")
// Required even under AGP 9: the Compose compiler plugin is NOT built in. // Required even under AGP 9: the Compose compiler plugin is NOT built in.
alias(libs.plugins.kotlin.compose) id("org.jetbrains.kotlin.plugin.compose")
// Lint/format. Resolved from the Gradle Plugin Portal, not AGP's buildscript // Lint/format. Resolved from the Gradle Plugin Portal, not AGP's buildscript
// classpath -- neither is an Android plugin. // classpath -- neither is an Android plugin.
@@ -69,8 +71,8 @@ android {
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_24 sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_24 targetCompatibility = JavaVersion.VERSION_25
} }
lint { lint {
+23 -3
View File
@@ -1,4 +1,24 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { //
alias(libs.plugins.android.application) apply false // AGP 9 uses "built-in Kotlin": it compiles Kotlin itself, with the KGP it bundles (2.2.10 for
} // AGP 9.3.1, per its POM). That version caps jvmTarget at 24. Putting a newer KGP on this single
// buildscript classpath is what raises the ceiling -- AGP's built-in Kotlin then compiles with
// 2.4.10 instead. The Compose compiler plugin must match KGP exactly, so it moves in lockstep.
//
// This is why the module below applies these two by id() rather than alias(): the plugins come
// from here, not from the version catalog's plugin resolution. The catalog still carries the
// version numbers, so there is exactly one place to edit.
//
// We still do NOT apply org.jetbrains.kotlin.android -- that plugin is incompatible with AGP 9's
// built-in-Kotlin DSL and fails the build.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:${libs.versions.agp.get()}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}")
classpath("org.jetbrains.kotlin:compose-compiler-gradle-plugin:${libs.versions.kotlin.get()}")
}
}
+11 -11
View File
@@ -1,12 +1,12 @@
#This file is generated by updateDaemonJvm #This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/539ea72837c6244075f8ee037babc29e/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/876dbbf00bf4ef6190f7da074cee903a/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/539ea72837c6244075f8ee037babc29e/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/876dbbf00bf4ef6190f7da074cee903a/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/4f4ebe4f162f6deb29540c4ebe629d79/redirect toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/1df82b76031285ef2e6a4f59ffe9299d/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/79d5995ef1c3e4df39a3b2f545cada5e/redirect toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/4d0277c49991306ac78048580fe34bcf/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/539ea72837c6244075f8ee037babc29e/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/876dbbf00bf4ef6190f7da074cee903a/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/826893cbcf0f86d8eb0975e2fb0788f7/redirect toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/a9fa3b809e484a1e1ac4308a5dda661a/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/61fdd87038cf8b094dadad8566609a29/redirect toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/aa99913fa6d767d899ffc96082da01ca/redirect
toolchainVersion=24 toolchainVersion=25
+23 -11
View File
@@ -1,14 +1,22 @@
[versions] [versions]
# Build tooling. # Build tooling. Pinned, never floating. agp and kotlin are coupled (below); ksp is
# NOTE: AGP 9 has BUILT-IN Kotlin support. Applying org.jetbrains.kotlin.android # staged for a later phase and unapplied today -- KSP2 versions independently of Kotlin.
# FAILS the build. AGP 9.3.1 brings kotlin-gradle-plugin 2.2.10 transitively, so the #
# Compose compiler plugin below must match that version, not the newest Kotlin release. # AGP 9 has BUILT-IN Kotlin: it compiles Kotlin itself rather than delegating to the
# Verified against the AGP 9.3.1 POM, which declares kotlin-gradle-plugin 2.2.10 at # org.jetbrains.kotlin.android plugin, which is incompatible with its DSL and fails the
# runtime scope. Kotlin 2.4.10 exists and Android lint will suggest it -- taking that # build if applied. DO NOT add it.
# suggestion breaks the build unless KGP is ALSO forced onto the root buildscript #
# classpath. These three move together or not at all; none of them may float. # By default AGP compiles with the KGP it bundles -- 2.2.10 for AGP 9.3.1, per its POM --
# and that version caps jvmTarget at 24, which would cap the app's bytecode below the JDK
# everything else runs on. The root build.gradle.kts therefore puts KGP on the buildscript
# classpath explicitly, which AGP's built-in Kotlin picks up instead. That is the only
# reason `kotlin` here can be ahead of what AGP ships.
#
# The Compose compiler plugin is versioned in lockstep with Kotlin and is read from this
# same `kotlin` entry, so the two cannot drift. Raising `kotlin` means checking that a
# matching compose-compiler-gradle-plugin exists.
agp = "9.3.1" agp = "9.3.1"
kotlin = "2.2.10" kotlin = "2.4.10"
ksp = "2.3.11" ksp = "2.3.11"
# AndroidX / Compose -- floating on minor + patch. The prerelease guard in # AndroidX / Compose -- floating on minor + patch. The prerelease guard in
@@ -108,8 +116,12 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "a
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } # com.android.application and org.jetbrains.kotlin.plugin.compose are deliberately absent.
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } # They come from the root buildscript classpath (see build.gradle.kts) so that a newer KGP
# can override AGP's bundled one, and the module applies them by id(). An alias here would
# be dead weight that reads like the source of truth.
#
# ktlint and detekt are ordinary plugin-portal resolutions and stay aliases.
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
detekt = { id = "dev.detekt", version.ref = "detekt" } detekt = { id = "dev.detekt", version.ref = "detekt" }