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
with:
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
# wrapper verifies its own distribution against distributionSha256Sum, and
@@ -66,7 +66,7 @@ jobs:
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: '24'
java-version: '25'
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
+3 -3
View File
@@ -80,7 +80,7 @@ jobs:
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
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.
# The wrapper verifies its own distribution against distributionSha256Sum, and
@@ -140,7 +140,7 @@ jobs:
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: '24'
java-version: '25'
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
@@ -209,7 +209,7 @@ jobs:
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: temurin
java-version: '24'
java-version: '25'
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<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" />
</component>
<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
**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:
| Where | What sets it |
|---|---|
| Gradle daemon | `gradle/gradle-daemon-jvm.properties` → `toolchainVersion=24` |
| CI | `java-version: '24'` in both workflows |
| Gradle daemon | `gradle/gradle-daemon-jvm.properties` → `toolchainVersion=25` |
| CI | `java-version: '25'` in both workflows |
| IDE | `.idea/misc.xml` |
| 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
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
`./gradlew updateDaemonJvm --jvm-version=NN`, never by hand.
**24, not 25, is deliberate.** Kotlin 2.2.10 refuses `jvmTarget` 25 outright — its available targets
stop at 24 — so the app's bytecode cannot join a 25 toolchain, and 24 is the highest number all four
rows can actually hold. D8 dexes Java 24 class files and R8 minifies them, both verified.
**Reaching 25 in the bytecode row took a deliberate build change.** AGP 9's built-in Kotlin compiles
with the KGP it bundles — 2.2.10 for AGP 9.3.1 — and that caps `jvmTarget` at 24. The root
`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
`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
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.
alias(libs.plugins.kotlin.compose)
id("org.jetbrains.kotlin.plugin.compose")
// Lint/format. Resolved from the Gradle Plugin Portal, not AGP's buildscript
// classpath -- neither is an Android plugin.
@@ -69,8 +71,8 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_24
targetCompatibility = JavaVersion.VERSION_24
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}
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.
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
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/4f4ebe4f162f6deb29540c4ebe629d79/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/79d5995ef1c3e4df39a3b2f545cada5e/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/6d0adbce30460017fe61d2993dfa663e/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/50f16d2dc2bb80a421afc1af38fc92e3/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/826893cbcf0f86d8eb0975e2fb0788f7/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/61fdd87038cf8b094dadad8566609a29/redirect
toolchainVersion=24
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/876dbbf00bf4ef6190f7da074cee903a/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/876dbbf00bf4ef6190f7da074cee903a/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/4d0277c49991306ac78048580fe34bcf/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/876dbbf00bf4ef6190f7da074cee903a/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/aa99913fa6d767d899ffc96082da01ca/redirect
toolchainVersion=25
+23 -11
View File
@@ -1,14 +1,22 @@
[versions]
# Build tooling.
# NOTE: AGP 9 has BUILT-IN Kotlin support. Applying org.jetbrains.kotlin.android
# 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.
# Verified against the AGP 9.3.1 POM, which declares kotlin-gradle-plugin 2.2.10 at
# runtime scope. Kotlin 2.4.10 exists and Android lint will suggest it -- taking that
# 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.
# Build tooling. Pinned, never floating. agp and kotlin are coupled (below); ksp is
# staged for a later phase and unapplied today -- KSP2 versions independently of Kotlin.
#
# AGP 9 has BUILT-IN Kotlin: it compiles Kotlin itself rather than delegating to the
# org.jetbrains.kotlin.android plugin, which is incompatible with its DSL and fails the
# build if applied. DO NOT add it.
#
# 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"
kotlin = "2.2.10"
kotlin = "2.4.10"
ksp = "2.3.11"
# 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" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
# com.android.application and org.jetbrains.kotlin.plugin.compose are deliberately absent.
# 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" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
detekt = { id = "dev.detekt", version.ref = "detekt" }