Two things the first pass got wrong. CLAUDE.md said "use a JDK 17-21, AGP 9 does not support 25+". That was carried over from the sibling repo and is not true here: gradle-daemon-jvm.properties pins toolchainVersion=25, so Gradle provisions and runs the daemon on Java 25 whatever JAVA_HOME says -- JAVA_HOME only picks the launcher. `gradlew --version` prints both, and shows them differing on this machine right now. It also means CI's java-version: '17' is not the JDK that compiles anything, and that the daemon JVM is the same on a laptop as on a runner, which is a better guarantee than the one the file claimed. Marking ConversionDependencies @UnstableApi propagates to its callers, and FakeFailures in androidTest calls it. That is a warning rather than an error in Kotlin, and lint does not read the androidTest source set, so the previous commit compiled clean while leaving one file inconsistent with the very pattern it described. Marked now. Left alone deliberately: gradlew.bat. The new `*.bat text eol=crlf` attribute governs how it is checked out from here on, which is the point of adding it, and the file already has CRLF in both the tree and the index. Rewriting the stored bytes of the wrapper script to prove the attribute works is not this branch's business. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.1 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
LibreMediaConverter is an Android media converter (Kotlin, Jetpack Compose, Material 3) with two
conversion engines: Media3 Transformer for the hardware path and FFmpeg for everything the platform
cannot do. See @README.md for the architecture and @LICENSES/README.md for the split license —
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.
./gradlew :app:assembleDebug # build debug APK
./gradlew :app:testDebugUnitTest # JVM unit tests
./gradlew :app:ktlintCheck # formatting
./gradlew :app:ktlintFormat # fix formatting in place
./gradlew :app:detekt # static analysis
./gradlew :app:lintDebug # Android lint
./gradlew :app:jacocoTestReport # coverage (XML+HTML under app/build/reports/jacoco/)
# single unit test:
./gradlew :app:testDebugUnitTest --tests "org.libremediaconverter.model.ConversionRouterTest"
CI's "Static analysis" gate is exactly ./gradlew :app:ktlintCheck :app:detekt :app:lintDebug --continue. Run it with --continue locally too: one round trip gives you all three lists instead
of the first one that fails.
Before treating a change as done, run: assembleDebug + testDebugUnitTest +
compileDebugAndroidTestKotlin + ktlintCheck + detekt + lintDebug.
compileDebugAndroidTestKotlin matters more here than it looks — the instrumented suite cannot run
on this machine (below), so without it an androidTest compile error is not discovered until CI.
ktlint and detekt also cover the test/androidTest source sets that lintDebug skips.
Instrumented tests do not run locally
Two independent reasons, so do not spend time on either:
- Emulators segfault on this host. qemu dies on every AVD. Instrumented tests run on CI or on the physical Pixel, never in a local emulator.
- The API 37 image is broken.
android-37.0crash-loops surfaceflinger inside its own gralloc mapper, so every test fails there regardless of this app.docs/api-37-emulator-crash.mdrecords the evidence and the ruled-out fixes; CI's matrix therefore stops at API 36 even though targetSdk is 37. API 37 needs a manual check on the Pixel 10 Pro XL before each release.
On a device or emulator, build only the ABI it can execute:
./gradlew :app:connectedDebugAndroidTest -PabiFilters=x86_64
FFmpeg's native libraries dominate the APK, so shipping arm64 to an x86_64 emulator doubles the install for code that can never run — and on API 37 the full APK does not fit at all.
Conventions
- ktlint owns formatting, detekt owns static analysis. detekt's formatting ruleset is off, so
the two can never disagree about the same line. Never hand-fix a formatting complaint — run
ktlintFormat. Style isintellij_ideaat 120 columns, set in.editorconfig. - detekt config is
config/detekt/detekt.yml, merged onto detekt's defaults (buildUponDefaultConfig = true), so it carries only the rules this codebase legitimately breaks — each with the reason written next to it. Relax a rule that way or fix the code; never a bare@Suppress. Do not invent config keys: unknown ones are rejected. - The
modelpackage is excluded fromReturnCountandCyclomaticComplexMethodonly. It is the decision layer, where one branch is one documented user-visible outcome and the metric counts answers rather than complexity. Every other rule still applies there. - Coverage is reported, not gated — currently ~31% of lines. A floor needs a baseline that has settled first.
kotlin.code.style=official. Gradle stays Kotlin DSL.
Traps
- Do not apply
org.jetbrains.kotlin.android. AGP 9 has built-in Kotlin; applying the legacy plugin fails the build. This is whylibs.versions.tomlpinskotlinto AGP's bundled KGP version rather than the newest Kotlin release — the Compose compiler plugin must match it. - The FFmpeg AAR is committed under
bin/, deliberately. It is not on any Maven repo (ffmpeg-kit was archived and delisted). Rebuilding per CI run made red builds ambiguous: broken code, or a cross-compile that hiccuped?bin/README.mdhas provenance and how to regenerate it. - Anything touching Media3 carries
@UnstableApirather than swallowing the marker with@OptIn. Android lint'sUnsafeOptInUsageErrorcatches a missed one. - Release builds ship both ABIs.
-PabiFiltersis a test-run override only;build.ymlverifies the released APK carries every ABI and that all native libraries are 16 KB aligned.