Opinionated Gradle convention plugins for the Framefork family of JVM libraries. A single settings plugin that a consumer applies with a version, after which every module gets uniform build, static-analysis, test and publishing conventions — with no per-module tool versions or copy-pasted buildSrc.
Apply one plugin and every library module gets, consistently:
- Java toolchain model — compile on a modern JDK, emit older-
--releasebytecode, run tests on a configurable JDK (compile-modern / target-old / test-anywhere). - Static-analysis strictness — Error Prone + NullAway in JSpecify mode (
onlyNullMarked+jspecifyMode), with@NullMarkedpackage-infogeneration, over-Werror. - Tests — JUnit 5 on the JUnit Platform + readable test-logger output, with stray JUnit 4 substituted away.
- Publishing —
maven-publishstaging with a full POM (for published library modules). - Kotlin — applied automatically only to modules that actually contain Kotlin sources.
In the consumer's settings.gradle.kts:
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("org.framefork.build") version "0.1.0"
}
framefork {
minJavaVersion = 17 // --release bytecode target (the minimum Java a consumer needs)
jdkVersion = 21 // compile/build toolchain (>= 21); override per-invocation with -Pjdk.version
// testsJdkVersion = 21 // test-runtime JDK; defaults to the resolved jdkVersion; override with -Ptests.jdk.version
// jspecifyMode = true // NullAway JSpecify mode (default on)
}The settings plugin discovers subprojects under modules/ (published libraries) and testing/ (internal test modules), so each module only declares which convention it wants — version-less, because the settings plugin puts them on the classpath:
// modules/foo/build.gradle.kts — a published library module
plugins {
id("org.framefork.build.library-published")
}// testing/bar/build.gradle.kts — an internal/test-only module (everything above, minus publishing)
plugins {
id("org.framefork.build.library-internal")
}A module whose classes carry Google @AutoService adds the version-less org.framefork.build.auto-service feature plugin after its library-* plugin; it wires the annotation (compileOnly) and its processor (annotationProcessor) with no versions:
// modules/foo/build.gradle.kts — a module that registers services via @AutoService
plugins {
id("org.framefork.build.library-published")
id("org.framefork.build.auto-service")
}Kotlin modules need one thing more. The base wiring is javac's annotationProcessor, which only processes Java sources — @AutoService on a Kotlin class generates nothing through it (it fails open, silently). So a Kotlin module also applies a Kotlin annotation-processing backend, and auto-service wires the auto-service processor onto whichever one it finds (order-independent). Prefer KSP — it needs a version matching your Kotlin (KSP is not on the suite's classpath, unlike the kotlin("…") companions):
// KSP (preferred) — pick the KSP release matching the suite's bundled Kotlin
plugins {
id("org.framefork.build.library-published")
id("com.google.devtools.ksp") version "2.2.21-2.0.5"
id("org.framefork.build.auto-service")
}// kapt (fallback) — resolves version-less off the suite classpath
plugins {
id("org.framefork.build.library-published")
kotlin("kapt")
id("org.framefork.build.auto-service")
}If a Kotlin module applies auto-service but neither backend, the build still succeeds but warns that @AutoService on its Kotlin classes registers nothing — see Troubleshooting.
That's the whole consumer surface. No error-prone/nullaway/jspecify/auto-service versions, no buildSrc, no repeated toolchain wiring.
| Knob | Default | Meaning |
|---|---|---|
minJavaVersion |
17 |
--release bytecode target — the minimum Java a consumer of the library needs. |
jdkVersion |
21 |
The JDK the compiler runs on (must be ≥ 21 for Error Prone). Overridable with -Pjdk.version=NN. |
testsJdkVersion |
= resolved jdkVersion |
The JDK the tests execute on. Overridable with -Ptests.jdk.version=NN. |
jspecifyMode |
true |
Whether NullAway runs in JSpecify generics mode. |
sequentialTests |
true |
Run at most one test JVM at a time across all modules — mutual exclusion, not ordering. Set to false to let test JVMs run in parallel across modules. |
dependencyLocking |
false |
Lock all configurations (LockMode DEFAULT) and provide the resolveAndLockAll maintenance task — see Troubleshooting. |
Compilation always runs on a modern jdkVersion (so current Error Prone works) and emits --release minJavaVersion bytecode, so the same artifact is portable across JDKs. Testing across multiple JDKs is a CI-matrix concern — set -Ptests.jdk.version per matrix cell; the plugin models a single scalar, not a list.
- Gradle 9.0+ on the consumer side, with the Gradle daemon running on JDK 21+ — the plugin's module metadata requires a 21+ JVM to resolve at all (on CI, keep a 21+ JDK as the last
setup-javaentry so it becomes the defaultJAVA_HOME). - A JDK ≥ 21 available as a toolchain (plus whatever
testsJdkVersionyou target).
./gradlew build # compile + unit + functional tests
./gradlew functionalTest -PtestedGradleVersion=9.0 # replay the functional suite against the consumer baseline
./gradlew publishToMavenLocal # install locally to try against a consumerTo try an unreleased version against a real consumer without publishing, publishToMavenLocal here and add mavenLocal() to the consumer's pluginManagement.repositories.
- Troubleshooting — common adoption failures (annotation-library
-Werrorbreaks, JDK-knob errors, doclint, Kotlin, dependency locking) and their fixes. - Design decisions — why the suite is built the way it is (distribution, structure, the JDK model, the strictness stack, config-cache safety).
- Contributing — building, testing, the codebase map, and the invariants a change must preserve.
- Releasing — the Maven Central release procedure: pre-flight checks, triggering the workflow, and writing the changelog.
Released under the Apache License 2.0.