gradle-multimodule
Rules for editing the SKaiNET multi-module Gradle build. The project is a Gradle composite build: build-logic/ is included via includeBuild and contributes convention plugins; every module under skainet-*/ is included from settings.gradle.kts; all dependency coordinates resolve through gradle/libs.versions.toml.
When to use
- Adding, renaming, or removing a Gradle module.
- Editing any
build.gradle.kts, settings.gradle.kts, or *.gradle.kts under build-logic/.
- Editing
gradle/libs.versions.toml (versions, libraries, plugins, bundles).
- Wiring a new dependency into a module.
- Adjusting publication, BOM membership, or coverage configuration.
When NOT to use
- Selecting which Kotlin source-set a file belongs in or which KMP target a module supports — that's
kmp.
- Writing the actual production Kotlin code — that's
kotlin.
- Test wiring beyond adding the dependency line —
skainet-testing owns assertion APIs and source-set placement for tests.
Hard rules
- Version-catalog-only. Every dependency, plugin, and version reference in any
build.gradle.kts MUST go through libs.versions.<x>, libs.<library>, or libs.plugins.<plugin>. Hard-coded version strings ("1.10.2", version = "2.3.21") MUST NOT appear in module build scripts.
- Plugins via
alias(libs.plugins.<x>). Direct id("...") version "..." is reserved for two cases only: (a) the sk.ainet.documentation convention plugin (no version because it's local), (b) plugins in build-logic/build.gradle.kts itself.
- New module = three edits in one change. When adding a
skainet-foo module: (a) create skainet-foo/build.gradle.kts, (b) add include("skainet-foo") (or the nested form) to settings.gradle.kts, (c) register it in skainet-bom/build.gradle.kts if it ships as a published artifact.
- Dokka comes from the
sk.ainet.dokka convention plugin. Modules apply it via id("sk.ainet.dokka"), not by configuring Dokka directly. Convention plugins live in build-logic/convention/src/main/kotlin/.
binary-compatibility-validator is applied on every published module. Don't suppress it locally; if the API dump changes, regenerate via ./gradlew :module:apiDump and commit the diff (see kotlin skill, api-stability reference).
- JVM target = 11 across the project. Set via
compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } inside the android { } block and inherited elsewhere through KMP defaults. Do not set higher.
- No transitive accidents. Use
api(...) only when the dependency's types appear in a module's public Kotlin signatures. Default to implementation(...). KSP-generated source goes through add("kspCommonMainMetadata", project(":skainet-...:...-ksp-processor")).
Workflow — adding a new module
- Decide the area: pick the right top-level group (
skainet-lang, skainet-data, skainet-io, skainet-backends, skainet-compile, skainet-models, skainet-pipeline, skainet-apps, skainet-test).
- Create the directory and a minimal
build.gradle.kts that mirrors the closest sibling. Apply the same plugin set (kotlinMultiplatform, vanniktech.mavenPublish, binary.compatibility.validator, sk.ainet.dokka, optionally ksp, kotlinx-benchmark).
- Add
include("skainet-<group>:<module>") to settings.gradle.kts in the matching // ====== <GROUP> section.
- Add the catalog entry if the module exposes a new external dependency (rare).
- If the module is published, register it in
skainet-bom/build.gradle.kts.
- Run
./gradlew :skainet-<group>:<module>:assemble to validate the wiring.
- If a public API was introduced, run
./gradlew :skainet-<group>:<module>:apiDump and commit the dump.
Canonical examples
Module build.gradle.kts — KMP library with KSP and Dokka:
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidMultiplatformLibrary)
alias(libs.plugins.vanniktech.mavenPublish)
alias(libs.plugins.binary.compatibility.validator)
alias(libs.plugins.ksp)
id("sk.ainet.dokka")
id("org.jetbrains.kotlinx.benchmark")
}
kotlin {
explicitApi()
// ... target list goes here — see kmp skill ...
sourceSets {
commonMain {
kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
dependencies {
api(project(":skainet-lang:skainet-lang-ksp-annotations"))
}
}
jvmMain.dependencies {
implementation(libs.kotlinx.benchmark.runtime)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
}
// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:4-69
Catalog entries — every coordinate routed through libs.versions.toml:
[versions]
kotlin = "2.3.21"
ksp = "2.3.6"
dokka = "2.1.0"
kotest = "6.1.11"
kover = "0.9.8"
binaryCompatibilityValidator = "0.18.1"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
skainet-docs = { id = "sk.ainet.documentation" }
// from: SKaiNET/gradle/libs.versions.toml:1-95
settings.gradle.kts — module registration, grouped by area:
includeBuild("build-logic")
// ====== LANG
include("skainet-lang:skainet-lang-core")
include("skainet-lang:skainet-lang-models")
include("skainet-lang:skainet-lang-ksp-annotations")
include("skainet-lang:skainet-lang-ksp-processor")
include("skainet-lang:skainet-lang-dag")
// ====== DATA
include("skainet-data:skainet-data-api")
include("skainet-data:skainet-data-transform")
include("skainet-data:skainet-data-simple")
include("skainet-data:skainet-data-media")
// ====== TEST
include("skainet-test:skainet-test-groundtruth")
include("skainet-test:skainet-test-java")
// from: SKaiNET/settings.gradle.kts:21-73
Related skills
Anti-patterns
// WRONG — hard-coded coordinate / version
implementation("io.kotest:kotest-runner-junit5:6.1.11")
// RIGHT — catalog reference
implementation(libs.kotest.runner.junit5)
// WRONG — applying Dokka by id
plugins { id("org.jetbrains.dokka") version "2.1.0" }
// RIGHT — apply the convention plugin (it configures Dokka)
plugins { id("sk.ainet.dokka") }
// WRONG — adding a new module without registering it
// (skainet-io/skainet-io-newformat/build.gradle.kts created, but settings.gradle.kts unchanged)
// RIGHT — also edit settings.gradle.kts AND skainet-bom in the same change
include("skainet-io:skainet-io-newformat") // in settings.gradle.kts
References
references/catalog-aliases.md — version, library, and plugin aliases pulled from libs.versions.toml.
references/convention-plugins.md — what sk.ainet.documentation / sk.ainet.dokka does and how to add a new convention plugin.
1---2name: gradle-multimodule3description: Use ONLY when editing build scripts INSIDE the SKaiNET repository — `SKaiNET/build.gradle.kts`, `SKaiNET/settings.gradle.kts`, `SKaiNET/gradle/libs.versions.toml`, anything under `SKaiNET/build-logic/`, or adding/renaming/removing a `skainet-*` module within SKaiNET. Enforces version-catalog-only references, convention-plugin reuse, BOM registration, binary-compatibility-validator, vanniktech maven-publish, kover. Do NOT fire on a CONSUMER project's build script that just depends on `sk.ainet:skainet-bom` — that's the `skainet-consumer-setup` skill.4---56# gradle-multimodule78Rules for editing the SKaiNET multi-module Gradle build. The project is a Gradle composite build: `build-logic/` is included via `includeBuild` and contributes convention plugins; every module under `skainet-*/` is included from `settings.gradle.kts`; all dependency coordinates resolve through `gradle/libs.versions.toml`.910## When to use1112- Adding, renaming, or removing a Gradle module.13- Editing any `build.gradle.kts`, `settings.gradle.kts`, or `*.gradle.kts` under `build-logic/`.14- Editing `gradle/libs.versions.toml` (versions, libraries, plugins, bundles).15- Wiring a new dependency into a module.16- Adjusting publication, BOM membership, or coverage configuration.1718## When NOT to use1920- Selecting which Kotlin source-set a file belongs in or which KMP target a module supports — that's `kmp`.21- Writing the actual production Kotlin code — that's `kotlin`.22- Test wiring beyond adding the dependency line — `skainet-testing` owns assertion APIs and source-set placement for tests.2324## Hard rules25261. **Version-catalog-only.** Every dependency, plugin, and version reference in any `build.gradle.kts` MUST go through `libs.versions.<x>`, `libs.<library>`, or `libs.plugins.<plugin>`. Hard-coded version strings (`"1.10.2"`, `version = "2.3.21"`) MUST NOT appear in module build scripts.272. **Plugins via `alias(libs.plugins.<x>)`.** Direct `id("...") version "..."` is reserved for two cases only: (a) the `sk.ainet.documentation` convention plugin (no version because it's local), (b) plugins in `build-logic/build.gradle.kts` itself.283. **New module = three edits in one change.** When adding a `skainet-foo` module: (a) create `skainet-foo/build.gradle.kts`, (b) add `include("skainet-foo")` (or the nested form) to `settings.gradle.kts`, (c) register it in `skainet-bom/build.gradle.kts` if it ships as a published artifact.294. **Dokka comes from the `sk.ainet.dokka` convention plugin.** Modules apply it via `id("sk.ainet.dokka")`, not by configuring Dokka directly. Convention plugins live in `build-logic/convention/src/main/kotlin/`.305. **`binary-compatibility-validator` is applied on every published module.** Don't suppress it locally; if the API dump changes, regenerate via `./gradlew :module:apiDump` and commit the diff (see `kotlin` skill, api-stability reference).316. **JVM target = 11 across the project.** Set via `compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }` inside the `android { }` block and inherited elsewhere through KMP defaults. Do not set higher.327. **No transitive accidents.** Use `api(...)` only when the dependency's types appear in a module's public Kotlin signatures. Default to `implementation(...)`. KSP-generated source goes through `add("kspCommonMainMetadata", project(":skainet-...:...-ksp-processor"))`.3334## Workflow — adding a new module35361. Decide the area: pick the right top-level group (`skainet-lang`, `skainet-data`, `skainet-io`, `skainet-backends`, `skainet-compile`, `skainet-models`, `skainet-pipeline`, `skainet-apps`, `skainet-test`).372. Create the directory and a minimal `build.gradle.kts` that mirrors the closest sibling. Apply the same plugin set (`kotlinMultiplatform`, `vanniktech.mavenPublish`, `binary.compatibility.validator`, `sk.ainet.dokka`, optionally `ksp`, `kotlinx-benchmark`).383. Add `include("skainet-<group>:<module>")` to `settings.gradle.kts` in the matching `// ====== <GROUP>` section.394. Add the catalog entry if the module exposes a new external dependency (rare).405. If the module is published, register it in `skainet-bom/build.gradle.kts`.416. Run `./gradlew :skainet-<group>:<module>:assemble` to validate the wiring.427. If a public API was introduced, run `./gradlew :skainet-<group>:<module>:apiDump` and commit the dump.4344## Canonical examples4546**Module `build.gradle.kts` — KMP library with KSP and Dokka:**4748```kotlin49plugins {50 alias(libs.plugins.kotlinMultiplatform)51 alias(libs.plugins.androidMultiplatformLibrary)52 alias(libs.plugins.vanniktech.mavenPublish)53 alias(libs.plugins.binary.compatibility.validator)54 alias(libs.plugins.ksp)55 id("sk.ainet.dokka")56 id("org.jetbrains.kotlinx.benchmark")57}5859kotlin {60 explicitApi()61 // ... target list goes here — see kmp skill ...6263 sourceSets {64 commonMain {65 kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")66 dependencies {67 api(project(":skainet-lang:skainet-lang-ksp-annotations"))68 }69 }70 jvmMain.dependencies {71 implementation(libs.kotlinx.benchmark.runtime)72 }73 commonTest.dependencies {74 implementation(libs.kotlin.test)75 }76 }77}78// from: SKaiNET/skainet-lang/skainet-lang-core/build.gradle.kts:4-6979```8081**Catalog entries — every coordinate routed through `libs.versions.toml`:**8283```toml84[versions]85kotlin = "2.3.21"86ksp = "2.3.6"87dokka = "2.1.0"88kotest = "6.1.11"89kover = "0.9.8"90binaryCompatibilityValidator = "0.18.1"9192[libraries]93kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }94kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }95kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }9697[plugins]98kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }99ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }100skainet-docs = { id = "sk.ainet.documentation" }101// from: SKaiNET/gradle/libs.versions.toml:1-95102```103104**`settings.gradle.kts` — module registration, grouped by area:**105106```kotlin107includeBuild("build-logic")108109// ====== LANG110include("skainet-lang:skainet-lang-core")111include("skainet-lang:skainet-lang-models")112include("skainet-lang:skainet-lang-ksp-annotations")113include("skainet-lang:skainet-lang-ksp-processor")114include("skainet-lang:skainet-lang-dag")115116// ====== DATA117include("skainet-data:skainet-data-api")118include("skainet-data:skainet-data-transform")119include("skainet-data:skainet-data-simple")120include("skainet-data:skainet-data-media")121122// ====== TEST123include("skainet-test:skainet-test-groundtruth")124include("skainet-test:skainet-test-java")125// from: SKaiNET/settings.gradle.kts:21-73126```127128## Related skills129130- KMP target list, source-set hierarchy, and `expect`/`actual` placement — see [`../kmp/SKILL.md`](../kmp/SKILL.md).131- Source-code style rules and explicit-API mode — see [`../kotlin/SKILL.md`](../kotlin/SKILL.md).132- Adding the Kotest dependency for a test source set — see [`../skainet-testing/SKILL.md`](../skainet-testing/SKILL.md).133134## Anti-patterns135136```kotlin137// WRONG — hard-coded coordinate / version138implementation("io.kotest:kotest-runner-junit5:6.1.11")139```140```kotlin141// RIGHT — catalog reference142implementation(libs.kotest.runner.junit5)143```144145```kotlin146// WRONG — applying Dokka by id147plugins { id("org.jetbrains.dokka") version "2.1.0" }148```149```kotlin150// RIGHT — apply the convention plugin (it configures Dokka)151plugins { id("sk.ainet.dokka") }152```153154```kotlin155// WRONG — adding a new module without registering it156// (skainet-io/skainet-io-newformat/build.gradle.kts created, but settings.gradle.kts unchanged)157```158```kotlin159// RIGHT — also edit settings.gradle.kts AND skainet-bom in the same change160include("skainet-io:skainet-io-newformat") // in settings.gradle.kts161```162163## References164165- [`references/catalog-aliases.md`](references/catalog-aliases.md) — version, library, and plugin aliases pulled from `libs.versions.toml`.166- [`references/convention-plugins.md`](references/convention-plugins.md) — what `sk.ainet.documentation` / `sk.ainet.dokka` does and how to add a new convention plugin.