skip to content

How do you make pluginManagement resolve a plugin from a local included build instead of a repository?

level: seniorimportance: should knowfreq 38%

answer

  1. includeBuild inside pluginManagement
  2. build-logic convention plugins
  3. plugin id substitution, no version
  4. gradlePlugin { register { id, implementationClass } }
  5. vs top-level includeBuild = dependency substitution

basics

~10 s

Put includeBuild("build-logic") inside pluginManagement {}. Gradle then resolves the plugin from that local build by matching its declared plugin id, instead of downloading it from a repository.

solid answer

~40 s

`pluginManagement {}` can host an `includeBuild("...")` call that points at a sibling build (typically a `build-logic` directory) containing your custom plugins. When a `plugins { id("my.convention") }` block requests a plugin id, Gradle first checks the included builds declared in `pluginManagement` and **substitutes** the local build's output for that id — no repository download, no published version needed. This is the standard way to share convention plugins inside a project without publishing them. The included build is a self-contained Gradle build (its own `settings.gradle.kts`) that applies the `java-gradle-plugin` plugin and registers plugin ids via the `gradlePlugin {}` extension. Placing `includeBuild` inside `pluginManagement` (rather than at the top level of settings) is what makes it a *plugin* source rather than a regular composite-build dependency provider.

code

kotlin · 10 lines
kotlin
// root settings.gradle.kts
pluginManagement {
    includeBuild("build-logic")   // makes build-logic's plugins resolvable by id
    repositories { gradlePluginPortal() }
}

// any subproject build.gradle.kts — no version needed
plugins {
    id("my.java-conventions")
}

go deeper

for a junior

Recognize that includeBuild can point at a local build-logic project for custom plugins.

for a middle

Explain that includeBuild inside pluginManagement makes a convention plugin resolvable by id with no version.

for a senior

Contrast pluginManagement.includeBuild (plugin source) with top-level includeBuild (dependency substitution) and describe the build-logic setup.

for a principal

Position included-build convention plugins as the standard for sharing build logic across many modules/repos, with implications for versioning and CI.

## The problem: sharing custom build logic Large builds accumulate repeated configuration — the same Kotlin/Java setup, the same test conventions — across many subprojects. The clean solution is a **convention plugin**: a custom plugin applied as `plugins { id("my.java-conventions") }`. But how does Gradle find a plugin you wrote locally and never published? ## includeBuild inside pluginManagement You create a separate Gradle build, conventionally a directory called `build-logic`, and wire it as a **plugin source**: ```kotlin // settings.gradle.kts (root) pluginManagement { includeBuild("build-logic") repositories { gradlePluginPortal() } } ``` The `includeBuild("build-logic")` inside `pluginManagement` tells Gradle: *when a plugin id is requested, also look in this included build*. Gradle scans the included build for a project that declares that plugin id and **substitutes** its output for the plugin request. No `version` is needed in the `plugins {}` block, and nothing is fetched from a repository. ## What the included build looks like ```kotlin // build-logic/settings.gradle.kts rootProject.name = "build-logic" // build-logic/build.gradle.kts plugins { `kotlin-dsl` // or java-gradle-plugin } gradlePlugin { plugins { register("javaConventions") { id = "my.java-conventions" implementationClass = "com.example.JavaConventionsPlugin" } } } ``` Now any subproject can do `plugins { id("my.java-conventions") }` and Gradle resolves it from `build-logic`. ## pluginManagement.includeBuild vs top-level includeBuild This distinction is the crux: - **`includeBuild` at the top level of settings** wires a composite build for **dependency substitution** — replacing a normal `implementation("group:artifact")` dependency with a local build's output. - **`includeBuild` inside `pluginManagement {}`** wires the included build as a **plugin** source — used to resolve `plugins { id(...) }` requests. A build can use both. Putting it in the wrong place means your convention plugin id won't be found, producing a *"Plugin with id 'my.java-conventions' not found"* error. ## Why this is preferred Using an included build for plugins gives you: type-safe, debuggable plugin code; no publish/version churn during development; and incremental compilation of the build logic. It is the idiomatic modern alternative to `buildSrc` for shareable conventions, and it keeps the configuration in one auditable place — the `pluginManagement {}` block.

  • What's the difference between includeBuild inside pluginManagement and at the top level of settings?
    Inside pluginManagement it registers the included build as a plugin source (resolves plugins { id(...) }). At the top level it wires a composite build for dependency substitution of library coordinates. Same keyword, different roles.
  • Why don't you specify a version when applying a plugin from an included build?
    Because Gradle substitutes the local build's output directly for the plugin id; there is no published artifact to version-match, so a version would be meaningless (and an error if you supply one).
  • How does the included build declare which plugin ids it provides?
    Via the gradlePlugin {} extension (from java-gradle-plugin / kotlin-dsl), registering each plugin with its id and implementationClass.

Top-level includeBuild is a stand-in actor for a library role; pluginManagement.includeBuild is a stand-in for the director's toolkit — same mechanism, different job.

saying these in an interview costs you the question

  • Putting includeBuild at the top level and expecting plugin ids to resolve.
  • Adding a version to a plugins {} id that comes from an included build.
  • Confusing this with buildSrc behavior or claiming you must publish the plugin first.

context