skip to content

Some teams manage to use type-safe `libs.*` accessors inside their `build-logic` convention plugins. How is that possible, and what makes it work where plain buildSrc fails?

level: seniorimportance: should knowfreq 35%

answer

  1. accessor is a generated class artifact
  2. declare catalog in build-logic settings (from files)
  3. implementation(files(libs.javaClass...codeSource))
  4. build-logic has controllable settings, buildSrc doesn't
  5. ergonomics vs brittleness trade-off

basics

~20 s

By adding the catalog's generated accessor as a dependency of the build-logic build — declaring the catalog in dependencyResolutionManagement of build-logic's own settings and adding implementation(files(libs.javaClass...))/the generated dependency — so the accessor class lands on the plugin's compile classpath.

solid answer

~40 s

The type-safe accessor is an actual generated artifact. To use `libs.*` inside a convention plugin you must put that generated accessor on the plugin project's compile classpath. The common pattern with an included `build-logic` build is: declare the same `libs` catalog in `build-logic/settings.gradle.kts` via `dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } }`, then add a dependency on the generated accessor in `build-logic/build.gradle.kts`: `dependencies { implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) }`. That hack-y line puts the generated catalog class on the classpath so the convention plugin source can compile against `libs.*`. With plain `buildSrc` this is awkward because buildSrc has no separate settings file you control the same way. Many teams prefer `findLibrary` to avoid the fragility, but the accessor route gives full IDE completion and compile-time safety inside plugins.

code

kotlin · 12 lines
kotlin
// build-logic/settings.gradle.kts
dependencyResolutionManagement {
    versionCatalogs {
        create("libs") { from(files("../gradle/libs.versions.toml")) }
    }
}

// build-logic/build.gradle.kts
dependencies {
    // Generated accessor onto the plugin compile classpath -> libs.* usable in plugin source
    implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
}

go deeper

for a junior

Recognize that getting libs.* inside plugins is possible but needs extra setup; not expected to wire it.

for a middle

Explain that the catalog must be declared in build-logic settings and the generated accessor added to the compile classpath.

for a senior

Describe the full build-logic pattern, the reflective classpath line, why buildSrc is harder, and the ergonomics-vs-brittleness trade-off against findLibrary.

for a principal

Decide org-wide whether to standardize on build-logic + accessors or findLibrary, weighing IDE ergonomics, fragility across Gradle upgrades, and onboarding cost for many teams.

## The core insight The type-safe accessor (`LibrariesForLibs` and friends) is **generated code**. `libs.*` works in a build script because Gradle injects that generated class as an implicit receiver/extension. To get the same inside a convention plugin you have to do two things: (1) make the catalog *known* to the build that compiles the plugin, and (2) put the *generated accessor class* on that build's compile classpath. ## Pattern with an included `build-logic` build Most teams use an included `build-logic` build (via `includeBuild("build-logic")` in root settings, or `pluginManagement { includeBuild(...) }`) rather than `buildSrc`, precisely because `build-logic` has its own `settings.gradle.kts` you control. ```kotlin // build-logic/settings.gradle.kts dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } } ``` This makes the catalog and its generated accessor available to `build-logic`'s own build scripts. Then, to make `libs.*` available **inside the Kotlin source** of the convention plugins: ```kotlin // build-logic/build.gradle.kts dependencies { // Put the generated type-safe catalog accessor on the compile classpath implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } ``` That reflective `files(...)` expression resolves to the jar/dir containing the generated `LibrariesForLibs` class, so the plugin source compiles against `libs`. ## Why plain buildSrc is harder `buildSrc` is auto-configured and you don't get an ergonomic, controllable `settings.gradle.kts`/dependency wiring for the catalog accessor in the same way. You *can* add `buildSrc/settings.gradle.kts` and similar tricks, but the included-build approach is cleaner and is why the Android/Now-in-Android community standardized on `build-logic`. ## Trade-offs vs. findLibrary - **Accessor route:** full IDE completion, refactor-safe `libs.kotlin.gradle.plugin`, but relies on a brittle reflective classpath line and tighter coupling between the build-logic build and the catalog file path. - **findLibrary route:** no magic, works in plain buildSrc, but stringly-typed (`findLibrary("kotlin-gradle-plugin")`) and you must `.get()`/`ifPresent`. Large orgs often choose the accessor route in `build-logic` for ergonomics and accept the one-line hack; smaller setups stay with `findLibrary`. ```toml # gradle/libs.versions.toml (shared by app + build-logic) [versions] kotlin = "2.0.0" [libraries] kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } ```

  • Why is `build-logic` (an included build) usually preferred over `buildSrc` for this?
    `build-logic` has its own `settings.gradle.kts` you fully control, making it straightforward to declare the catalog and wire the generated accessor onto the plugin compile classpath. `buildSrc` is more auto-configured and the same wiring is clumsier.
  • What's the downside of the `implementation(files(libs.javaClass...))` line?
    It's reflective and brittle — it depends on internal generated-class layout and a hardcoded path to the TOML, so it can break across Gradle versions or restructuring, and it's harder to read than `findLibrary`.
  • If you don't want the hack, what's the robust alternative?
    Use `versionCatalogs.named("libs").findLibrary(...)` programmatically — no generated accessor needed, works in plain buildSrc, at the cost of string aliases and Optional handling.

saying these in an interview costs you the question

  • Claiming `libs.*` 'just works' in build-logic with no extra wiring — it requires declaring the catalog and putting the accessor on the classpath.
  • Confusing buildSrc and build-logic as identical — the controllable settings file is the key difference here.
  • Presenting the reflective `files(...)` line as a stable, documented API rather than a community workaround.

context