skip to content

In a multi-module Kotlin build using Room and Hilt via KSP, how do you pass processor options (e.g., Room's schema export directory) and keep the KSP/processor versions consistent across modules?

level: principalimportance: nice to knowfreq 25%

answer

  1. Options via ksp { arg("room.schemaLocation", ...) }
  2. kapt used kapt { arguments { arg(...) } } instead
  3. Options apply per module that runs the processor
  4. Convention plugin to share the wiring
  5. Version catalog to pin KSP+processor versions everywhere

basics

~20 s

You pass processor options through the ksp { arg(...) } block in the module (for example Room's schema directory), and you keep versions aligned by centralizing them in a version catalog or a shared convention plugin so every module uses the same KSP and processor versions.

solid answer

~40 s

KSP exposes a ksp { } extension where you set processor arguments with arg("key", "value"); Room reads room.schemaLocation that way. Because options must be set in the module that runs the processor, factor the wiring into a Gradle convention (precompiled script) plugin so each data-layer module applies the same KSP plugin, the same ksp(...) dependencies, and the same arg(...) options. Pin the KSP plugin version to your Kotlin version, and declare all processor coordinates (room-compiler, hilt-android-compiler) in a single libs.versions.toml version catalog so a Kotlin bump updates KSP everywhere at once. This avoids version drift (mismatched KSP vs Kotlin breaks the build) and duplicated, divergent schema/arg config across modules.

code

kotlin · 12 lines
kotlin
// in a data module (or convention plugin)
plugins { alias(libs.plugins.ksp) }

dependencies {
    implementation(libs.room.runtime)
    ksp(libs.room.compiler)
}

ksp {
    arg("room.schemaLocation", "$projectDir/schemas")
    arg("room.incremental", "true")
}

go deeper

for a junior

Knows options go in a ksp { } block but not the multi-module concerns.

for a middle

Can set room.schemaLocation via ksp { arg(...) } in a single module.

for a senior

Understands per-module scoping and uses a version catalog to align processor versions.

for a principal

Designs convention plugins + catalog so all data modules share identical, version-aligned Room/Hilt KSP wiring with one source of truth.

## Passing options to a KSP processor KSP processors read **arguments** you set in the `ksp { }` extension: ```kotlin ksp { arg("room.schemaLocation", "$projectDir/schemas") arg("room.incremental", "true") } ``` `arg(key, value)` puts a string option into KSP's `SymbolProcessorEnvironment.options`, which Room (and other processors) read. The kapt equivalent was `kapt { arguments { arg(...) } }` — different block, same idea. **Scope:** options apply to the module where the processor actually runs, so they must be declared in each module that uses the processor (not just the root). ## The multi-module problem If you have many `data`/`feature` modules each using Room + Hilt, copy-pasting the plugin application, `ksp(...)` lines, and `arg(...)` options into every `build.gradle.kts` leads to **drift** — different schema paths, different versions, subtle breakage. ### Solution 1 — convention plugins Factor the shared wiring into a **precompiled script plugin** (e.g., `buildSrc` or an included build) such as `myapp.room-module.gradle.kts`: ```kotlin plugins { id("com.google.devtools.ksp") } dependencies { "ksp"("androidx.room:room-compiler") } extensions.configure<com.google.devtools.ksp.gradle.KspExtension> { arg("room.schemaLocation", "$projectDir/schemas") } ``` Each data module just does `plugins { id("myapp.room-module") }`. ### Solution 2 — version catalog Declare every coordinate and the KSP plugin version once in `gradle/libs.versions.toml`: ```toml [versions] kotlin = "2.x" ksp = "2.x-<kspbuild>" room = "..." [plugins] ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } [libraries] room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } ``` Modules reference `libs.room.compiler` and `alias(libs.plugins.ksp)` — one edit updates everyone. ## Why version alignment is critical **KSP plugin versions are tied to a specific Kotlin version.** A module on a stale KSP version against a newer Kotlin compiler fails to load the processor. Centralizing the version (catalog + convention plugin) makes a Kotlin upgrade a one-line change and prevents per-module drift. ## Putting it together - Options → `ksp { arg(...) }`, in each module that runs the processor. - Consistency → convention plugin for the wiring + version catalog for the numbers. - Result: every data module gets identical, correct Room/Hilt KSP setup with a single source of truth.

  • How did passing the same Room option differ under kapt?
    With kapt you used kapt { arguments { arg("room.schemaLocation", ...) } } instead of the ksp { arg(...) } block — a different extension, but the option key Room reads is the same.
  • Why not just set the schema location once in the root build.gradle?
    KSP processor options are scoped to the module where the processor runs, so a root-only setting won't reach sub-modules; you centralize via a convention plugin that each module applies instead.

saying these in an interview costs you the question

  • Setting ksp arg(...) only in the root project and expecting submodules to inherit it
  • Hardcoding processor versions per module instead of a catalog
  • Confusing the kapt { arguments } block with the ksp { } block
  • Ignoring KSP-to-Kotlin version coupling across modules

context