Why can't you write `libs.junit.jupiter` directly inside a convention plugin's `apply()` method in buildSrc, even though it works fine in a regular module's build.gradle.kts?
answer
- accessor generated only for owning build's scripts
- buildSrc = separate build, no libs symbol
- VersionCatalogsExtension.named("libs")
- findLibrary returns Optional<Provider<...>>
- dots become dashes in findLibrary alias
basics
~10 sThe type-safe libs.* accessors are generated for build scripts that the catalog is in scope for. Inside a buildSrc convention plugin those generated accessors aren't on the classpath, so libs.* doesn't resolve.
solid answer
~40 sGradle generates the type-safe `libs` accessor as synthetic code only for the build scripts (`build.gradle.kts`, `settings.gradle.kts`) of the build that declares the catalog. A convention plugin compiled in buildSrc (or `build-logic`) is plain Kotlin/Java code in a *separate* build — it has no implicit `libs` symbol in scope and the generated accessor type isn't on its compile classpath. So referencing `libs.junit.jupiter` won't compile there. Inside the plugin you must reach the catalog programmatically: get the `VersionCatalogsExtension` via `project.extensions.getByType<VersionCatalogsExtension>()`, then `versionCatalogs.named("libs").findLibrary("junit-jupiter")` which returns an `Optional<Provider<MinimalExternalModuleDependency>>`. There's also a workaround of generating the accessor by adding the catalog as a dependency, but `findLibrary` is the standard route.
code
kotlin · 15 lines// Inside buildSrc/src/main/kotlin/MyConventionPlugin.kt
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
class MyConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
project.dependencies.add(
"implementation",
libs.findLibrary("junit-jupiter").get()
)
}
}go deeper
Recall that libs.* is auto-available in build.gradle.kts but not inside plugin code in buildSrc.
Explain that the accessor is generated only for the owning build's scripts and that buildSrc is a separate build, then name VersionCatalogsExtension/findLibrary as the workaround.
Articulate the build-isolation reason (buildSrc is its own build, its classpath has no generated accessor) and the Optional<Provider> return shape with lazy semantics and alias mangling.
Frame the trade-off for shared build logic: programmatic catalog access vs. generating accessors, and how this constrains convention-plugin ergonomics across a large module graph.
## What the type-safe accessor actually is When you declare a version catalog (default name `libs`, in `gradle/libs.versions.toml`), Gradle generates a synthetic Kotlin/Java class — an *accessor* — that gives you `libs.junit.jupiter`, `libs.versions.kotlin`, `libs.plugins.spring`, etc. with IDE completion and compile-time safety. Crucially, this generated accessor is injected **only into the build scripts of the build that owns the catalog**: the `build.gradle.kts` / `settings.gradle.kts` files. It is not a globally visible symbol. ## Why buildSrc is different `buildSrc` (and an included `build-logic` build) is a **separate Gradle build** whose output — your convention plugins — is put on the classpath of the main build's scripts. A convention plugin is ordinary compiled code: ```kotlin class MyConventionPlugin : Plugin<Project> { override fun apply(project: Project) { /* ... */ } } ``` This class is compiled by the buildSrc build. At that point there is no `libs` symbol in scope and the generated accessor type is not on buildSrc's compile classpath, so `libs.junit.jupiter` simply does not exist as far as the compiler is concerned. ## The programmatic API Inside the plugin you reach the catalog through the `VersionCatalogsExtension`: ```kotlin val catalog = project.extensions.getByType(VersionCatalogsExtension::class.java) val libs = catalog.named("libs") libs.findLibrary("junit-jupiter") // Optional<Provider<MinimalExternalModuleDependency>> libs.findVersion("kotlin") // Optional<VersionConstraint> libs.findBundle("testing") // Optional<Provider<ExternalModuleDependency...>> libs.findPlugin("spring-boot") // Optional<Provider<PluginDependency>> ``` Note the **name mangling**: a TOML alias `junit-jupiter` (dotted/dashed) is looked up with the string `"junit-jupiter"` here, whereas in a build script the same alias is accessed as `libs.junit.jupiter`. The dots in the type-safe accessor become dashes/the original alias string in `findLibrary`. ## Why it returns Optional<Provider<...>> `findLibrary` returns an `Optional` because the alias may not exist (you get a clean empty Optional instead of a hard failure), and the inner `Provider` defers resolution so the dependency coordinates are computed lazily during configuration. You typically do `.get()` and pass it to `dependencies.add("implementation", ...)`.
- What does `findLibrary("junit-jupiter")` return, and why is it wrapped that way?An `Optional<Provider<MinimalExternalModuleDependency>>`. The `Optional` handles a possibly-missing alias gracefully; the `Provider` defers resolving the actual coordinates until configuration/use, keeping it lazy.
- How does the alias spelling differ between a build script and `findLibrary`?In a script you write `libs.junit.jupiter` (dots). In `findLibrary` you pass the original alias string `"junit-jupiter"` (dashes). The accessor turns dashes into navigable dots; the programmatic API uses the raw alias.
The libs.* accessor is like an autogenerated phrasebook handed only to people sitting in the main build's rooms (the build scripts). A buildSrc plugin works in a different building entirely, so it has to phone the catalog desk directly (findLibrary) instead of reading the phrasebook.
saying these in an interview costs you the question
- Claiming `libs.*` works everywhere including plugin code — it's only injected into the owning build's scripts.
- Thinking buildSrc shares the main build's compile classpath for accessors — it's a separate build.
- Forgetting that `findLibrary` returns an Optional and calling members on it directly without `.get()`.