skip to content

Your in-development plugin is a Settings plugin (applied in settings.gradle), not a project plugin. Can you co-develop it with a consumer via includeBuild, and what changes?

level: seniorimportance: nice to knowfreq 15%

answer

  1. Plugin<Settings> applied early
  2. pluginManagement { includeBuild }
  3. top-level includeBuild = project plugins
  4. resolution-phase ordering
  5. same java-gradle-plugin setup

basics

~20 s

A Settings plugin applies in the consumer's settings.gradle, but includeBuild's plugin substitution is processed during settings evaluation. To apply an included Settings plugin by id you use pluginManagement { includeBuild(...) } so the plugin build is available early enough.

solid answer

~40 s

Project plugins are applied in `build.gradle`, after settings is evaluated, so a normal top-level `includeBuild` suffices to substitute their markers. A **Settings plugin** is applied inside `settings.gradle` itself — earlier than the point where a top-level `includeBuild` would make the included build's plugins resolvable. To apply an in-development Settings plugin by id, you include the plugin build inside the **`pluginManagement {}`** block: `pluginManagement { includeBuild("build-logic") }`. That makes the included build contribute plugin markers to settings-plugin resolution, so `plugins { id("com.example.settingsplugin") }` in the consumer's settings file resolves to your local source. The plugin build still applies `java-gradle-plugin` and declares the id; only *where* you put `includeBuild` changes. This is the classic gotcha when a convention/settings plugin won't resolve from an included build.

code

kotlin · 18 lines
kotlin
// consumer settings.gradle.kts
pluginManagement {
    includeBuild("build-logic")   // makes settings-plugin markers resolvable early
}
plugins {
    id("com.example.settingsplugin")
}

// build-logic/build.gradle.kts
plugins { `java-gradle-plugin` }
gradlePlugin {
    plugins {
        register("settingsplugin") {
            id = "com.example.settingsplugin"
            implementationClass = "com.example.MySettingsPlugin" // Plugin<Settings>
        }
    }
}

go deeper

for a junior

Likely out of scope; at most know there's a distinction between settings plugins and project plugins.

for a middle

Recognize that settings plugins apply earlier and may need a different include placement.

for a senior

Explain pluginManagement { includeBuild } vs top-level includeBuild and the resolution-phase reason.

for a principal

Discuss settings-plugin distribution strategy and how composite-build resolution phases affect shared convention infrastructure.

## Project plugin vs Settings plugin - A **project plugin** implements `Plugin<Project>` and is applied in a project's `build.gradle(.kts)` via `plugins { id(...) }`. - A **Settings plugin** implements `Plugin<Settings>` and is applied in `settings.gradle(.kts)` to customize the build before projects exist (e.g. configuring `dependencyResolutionManagement`, declaring subprojects, etc.). ## Why placement of includeBuild matters Gradle resolves things in phases. A **top-level** `includeBuild("build-logic")` (a direct call in `settings.gradle`) wires the build for **project** plugin and dependency substitution — which happens after the settings file is evaluated. But a Settings plugin must be resolved **while the settings file itself is being evaluated**, which is earlier. At that moment the top-level includeBuild hasn't yet been registered for plugin resolution. The fix is to include the build inside **`pluginManagement {}`**, which is the very first thing Gradle processes in a settings file: ```kotlin // consumer settings.gradle.kts pluginManagement { includeBuild("build-logic") } plugins { id("com.example.settingsplugin") // now resolvable from the included build } ``` `pluginManagement { includeBuild(...) }` tells Gradle: when resolving plugins (including settings plugins), also consider this included build's plugin markers. With that, your in-development Settings plugin is substituted just like a project plugin would be — no publishing. ## The plugin build is unchanged The `build-logic` build still: - applies `java-gradle-plugin`, - declares the id and `implementationClass` in `gradlePlugin { plugins { register(...) } }`, - where `implementationClass` is a `Plugin<Settings>`. ## Summary rule - Co-developing a **project** plugin → top-level `includeBuild("build-logic")` in settings. - Co-developing a **settings** plugin → `pluginManagement { includeBuild("build-logic") }`. ## Why this is a common stumbling block Developers add a top-level `includeBuild`, see project plugins resolve fine, then are baffled when a settings/convention plugin reports an unresolved id. The cause is purely the resolution phase, fixed by moving the include into `pluginManagement`.

  • Why does a top-level includeBuild work for a project plugin but not a settings plugin?
    Project plugins are applied after the settings file is evaluated, so a top-level includeBuild has already registered its substitutions by then. A settings plugin must resolve while settings.gradle is still being evaluated, which is before top-level includeBuild substitutions are active — hence you move the include into pluginManagement, processed first.
  • Does the plugin build itself need to differ for a settings plugin vs a project plugin?
    No structural difference in wiring: it still applies java-gradle-plugin and declares the id and implementationClass. The only difference is that implementationClass is a Plugin<Settings> rather than Plugin<Project>; the build-logic declaration and includeBuild substitution mechanics are otherwise the same.

saying these in an interview costs you the question

  • Saying a settings plugin can't be co-developed via includeBuild at all.
  • Putting includeBuild for a settings plugin at the top level and expecting it to resolve.
  • Confusing Plugin<Settings> with Plugin<Project> application points.

context