diff --git a/CLAUDE.md b/CLAUDE.md index 1975223..0c61649 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,12 +9,28 @@ this file covers only what is not obvious from the code. ## Build, test, lint -**Do not pick a JDK for the daemon — the repo does.** `gradle/gradle-daemon-jvm.properties` pins -`toolchainVersion=25` and carries foojay download URLs per platform, so Gradle provisions and runs -the daemon on **Java 25** regardless of what `JAVA_HOME` points at (that only sets the launcher). -`./gradlew --version` prints both if you need to confirm which is which. This is why CI's -`java-version: '17'` is not the version that compiles anything, and why the daemon JVM is identical -on a laptop and on a runner. Change it with `./gradlew updateDaemonJvm`, not by hand. +**Everything is on Java 24** — 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 | +| 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 +`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. + +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 +--gradle-distribution-sha256-sum ` so the two stay consistent. ```bash ./gradlew :app:assembleDebug # build debug APK @@ -74,6 +90,29 @@ install for code that can never run — and on API 37 the full APK does not fit settled first. - `kotlin.code.style=official`. Gradle stays Kotlin DSL. +## Dependency versions + +Libraries **float on minor + patch** (`coreKtx = "1.+"`). Three groups deliberately do not: + +- **`agp`, `kotlin`, `ksp` are version-locked to each other.** AGP 9.3.1's POM declares + `kotlin-gradle-plugin` 2.2.10, and that is what AGP's built-in Kotlin compiles with. Android lint + will suggest Kotlin 2.4.10; taking it breaks the Compose compiler unless KGP is *also* forced onto + the root buildscript classpath. Move all three together, by hand, or none. +- **ktlint, detekt and JaCoCo are pinned.** A new rule in a linter makes files nobody touched stop + passing, so CI goes red on a PR whose diff cannot explain it. Upgrading them is its own commit: + run the tool, read the new findings, fix or relax them. +- **The FFmpeg AAR** is a committed file, not a coordinate. + +**`+` does not mean "newest stable" on its own** — Gradle will happily resolve it to an alpha, and +androidx routinely publishes alphas numbered above the current stable (at last check: lifecycle, +navigation, work, datastore and annotation all did). The `componentSelection` block in +`app/build.gradle.kts` rejects prereleases, which is the only reason `2.+` means 2.11.0 rather than +2.12.0-alpha01. **Do not remove it.** To try a prerelease, name the exact version in the catalog — +that pins it, which is the right way round. + +Because versions float, a build can change without a commit. `./gradlew :app:dependencies +--configuration debugRuntimeClasspath` shows what actually resolved. + ## Traps - **Do not apply `org.jetbrains.kotlin.android`.** AGP 9 has built-in Kotlin; applying the legacy diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 325396f..bd91b93 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -114,6 +114,31 @@ kotlin { compilerOptions {} } +// --- Prerelease guard for the floating dependency versions ------------------------------ +// The library versions in libs.versions.toml float on minor + patch ("1.+"). Gradle resolves +// `+` to the highest version it finds, and it does NOT skip prereleases -- so without this, +// androidx would quietly hand the app an alpha. That is not hypothetical here: at the time +// of writing lifecycle, navigation, work, datastore and annotation ALL publish an alpha or +// rc numbered above their newest stable, so five of the floats would have moved onto +// unreleased code on the next build, with nothing in the diff to say so. +// +// Rejecting them here means "+" reads as "the newest RELEASED version", which is what +// floating was meant to buy. Trying an alpha stays possible -- name the exact version in +// the catalog, and it is pinned rather than floating, which is the right way round. +val prereleaseMarker = Regex("""[-.](alpha|beta|rc|eap|dev|snapshot|pre|m)\d*$""", RegexOption.IGNORE_CASE) + +configurations.configureEach { + resolutionStrategy { + componentSelection { + all { + if (prereleaseMarker.containsMatchIn(candidate.version)) { + reject("prerelease; floating versions take released builds only") + } + } + } + } +} + detekt { // Merge the project overrides in config/detekt onto detekt's bundled defaults, so this // repo's file only has to carry the rules it actually changes. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05c730d..c3ab12a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,27 +11,38 @@ agp = "9.3.1" kotlin = "2.2.10" ksp = "2.3.11" -# AndroidX / Compose -composeBom = "2026.08.00" -coreKtx = "1.19.0" -activityCompose = "1.13.0" -lifecycle = "2.11.0" -navigation = "2.9.8" -work = "2.11.2" -datastore = "1.2.1" -media3 = "1.11.0" -room = "2.8.4" -documentfile = "1.1.0" -annotation = "1.10.0" +# AndroidX / Compose -- floating on minor + patch. The prerelease guard in +# app/build.gradle.kts is what keeps `+` from selecting an alpha: several of these +# (lifecycle, navigation, work, datastore, annotation) publish alphas and RCs with +# version numbers ABOVE their newest stable, and Gradle's `+` would take them. +composeBom = "2026.+" +coreKtx = "1.+" +activityCompose = "1.+" +lifecycle = "2.+" +navigation = "2.+" +work = "2.+" +datastore = "1.+" +media3 = "1.+" +room = "2.+" +documentfile = "1.+" +annotation = "1.+" -# Test -junit = "4.13.2" -androidxJunit = "1.3.0" -espressoCore = "3.7.0" -smartException = "0.2.1" +# Test -- floating, same rules as above. +junit = "4.+" +androidxJunit = "1.+" +espressoCore = "3.+" +smartException = "0.+" -# Lint/format. ktlint owns formatting; detekt owns static analysis (its formatting -# ruleset stays off, so the two can never disagree about the same line). +# Lint/format. PINNED, deliberately, while the libraries above float. +# +# A library bump that misbehaves usually still compiles. An analysis-tool bump does +# something worse: a new rule in ktlint or detekt makes files nobody touched stop +# passing, so CI goes red on a PR whose diff cannot explain it. Upgrading these is +# therefore an act with its own commit -- run the tool, read the new findings, fix or +# relax them -- which is exactly the reviewable step floating is meant to skip. +# +# ktlint owns formatting; detekt owns static analysis (its formatting ruleset stays +# off, so the two can never disagree about the same line). # detekt 2.0 is the only line with Gradle 9 support -- stable 1.23.x tops out at # Gradle 8.12, and this project is on 9.5. ktlint = "14.2.0"