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?
answer
- accessor is a generated class artifact
- declare catalog in build-logic settings (from files)
- implementation(files(libs.javaClass...codeSource))
- build-logic has controllable settings, buildSrc doesn't
- ergonomics vs brittleness trade-off
basics
~20 sBy 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 sThe 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// 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
Recognize that getting libs.* inside plugins is possible but needs extra setup; not expected to wire it.
Explain that the catalog must be declared in build-logic settings and the generated accessor added to the compile classpath.
Describe the full build-logic pattern, the reflective classpath line, why buildSrc is harder, and the ergonomics-vs-brittleness trade-off against findLibrary.
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.