skip to content

Why are catalog accessors like `libs` not available by default inside precompiled convention plugins or `buildSrc`, and how do you make them work there?

level: seniorimportance: should knowfreq 30%

answer

  1. accessors generated for project scripts only
  2. buildSrc/convention plugin = separate compilation
  3. VersionCatalogsExtension.named("libs")
  4. findLibrary/findBundle/findPlugin/findVersion
  5. build-logic settings must declare catalog

basics

~20 s

The libs accessor is generated for project build scripts, not for buildSrc/convention-plugin code, so it isn't on that classpath by default. You expose it by adding the catalog as a dependency in buildSrc and reading it via extensions.getByType<VersionCatalogsExtension>().named("libs").

solid answer

~40 s

Type-safe accessors (`libs`) are **generated only for the project's own build scripts** in the main build. Code living in `buildSrc` or in **precompiled script plugins** runs in a separate compilation unit whose classpath doesn't include those generated accessors, so `libs.foo` won't compile there. Two common fixes: (1) In `buildSrc/build.gradle.kts`, add the generated catalog as a dependency — `implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))` is hacky; the supported route is the **`VersionCatalogsExtension`**: `val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")` then `libs.findLibrary("guava").get()`. (2) For Kotlin precompiled convention plugins, declare the catalog in `buildSrc`'s own `settings.gradle.kts` and add the API dependency so the generated accessor type is on the convention-plugin classpath. The cleanest modern approach is the `VersionCatalogsExtension` lookup inside the plugin, since it doesn't depend on generated-accessor availability and works uniformly. This matters for any team factoring shared dependency declarations into convention plugins.

code

kotlin · 5 lines
kotlin
// inside a convention plugin
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
    add("implementation", libs.findLibrary("guava").get())
}

go deeper

for a junior

Likely out of scope; at most know libs is for normal build scripts.

for a middle

Recognize that libs may not resolve in buildSrc/convention plugins and that there's a programmatic catalog API.

for a senior

Use VersionCatalogsExtension.named(...).findLibrary(...) in convention plugins and explain why the typed accessor isn't on that classpath.

for a principal

Design the build-logic layer so shared dependency declarations read the catalog via a stable seam, keeping convention plugins upgrade-resilient across Gradle versions.

## The root cause: where accessors are generated Gradle generates the `libs` type-safe accessors and puts them on the classpath of the **root build's project build scripts**. Code in **`buildSrc`** or in a **precompiled script plugin** (a `*.gradle.kts` in `src/main/kotlin` of a convention-plugins project) is compiled as part of a *separate* build — that classpath does **not** automatically include the generated accessors. Hence `libs.guava` simply doesn't resolve there. ## Fix 1 — `VersionCatalogsExtension` (recommended) Gradle exposes catalogs programmatically through the `VersionCatalogsExtension`. This works anywhere you have a `Project`, including inside a convention plugin: ```kotlin import org.gradle.accessors.dm.LibrariesForLibs // sometimes available import org.gradle.api.artifacts.VersionCatalogsExtension val libs = extensions.getByType<VersionCatalogsExtension>().named("libs") dependencies { add("implementation", libs.findLibrary("guava").get()) libs.findBundle("networking").ifPresent { add("implementation", it) } } ``` `named("libs")` returns a `VersionCatalog` with `findLibrary`, `findBundle`, `findPlugin`, and `findVersion` (each returning an `Optional<Provider<...>>`). This is robust because it doesn't rely on generated accessor classes being visible. ## Fix 2 — make the generated accessor type visible (typed `libs`) To keep the nice typed `libs.guava` syntax inside precompiled convention plugins, you wire the catalog into the convention-plugins project so Gradle generates `LibrariesForLibs` for it, then add it as an `implementation`/`api` dependency: ```kotlin // build-logic/build.gradle.kts dependencies { implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } ``` This pattern (and variants pointing at the generated `LibrariesForLibs` jar) lets the typed accessor compile in convention plugins, but it's brittle across Gradle versions — many teams prefer Fix 1. ## buildSrc vs build-logic Whether you use `buildSrc` or an included `build-logic` build (via `includeBuild`), the same limitation applies: convention-plugin code is a separate compilation. With an included build you also declare the catalog in *that* build's `settings.gradle.kts` so its scripts can see `libs` too. ## Why this is an architecture question Factoring dependencies into convention plugins is how large multi-module builds avoid repetition and enforce a curated stack. Knowing the accessor-availability boundary — and choosing the `VersionCatalogsExtension` lookup as the stable seam — is the difference between a convention-plugin layer that compiles cleanly and one that breaks on every Gradle upgrade.

  • What does `VersionCatalogsExtension.named("libs").findLibrary("guava")` return?
    An `Optional<Provider<MinimalExternalModuleDependency>>` — present if the alias exists. You typically call `.get()` (or `.ifPresent { }`) and pass it to `add("implementation", ...)`.
  • Does this limitation differ between `buildSrc` and an included `build-logic` build?
    The accessor-availability limitation is the same (both are separate compilations). With an included build you additionally declare the catalog in that build's `settings.gradle.kts` so its scripts can use `libs`.

saying these in an interview costs you the question

  • Assuming `libs.foo` just works inside `buildSrc`/convention plugins like in a project build script.
  • Hardcoding versions in convention plugins to dodge the problem instead of reading the catalog via `VersionCatalogsExtension`.

context