How can you develop a Gradle plugin and a project that consumes it in the same workspace, applying the plugin by its id without publishing first?
answer
- includeBuild in settings
- java-gradle-plugin + gradlePlugin block
- plugin marker substitution
- no publishToMavenLocal
- apply by real id
basics
~10 sPut the plugin in its own build, then add includeBuild("build-logic") to the consumer's settings.gradle. The consumer can then apply the plugin by its id with no publishing.
solid answer
~40 sComposite builds let you wire one Gradle build into another. Put the plugin in a standalone build (e.g. `build-logic/`) that applies the `java-gradle-plugin` plugin and declares the plugin id via the `gradlePlugin { plugins { ... } }` block. In the consumer's `settings.gradle(.kts)` add `includeBuild("build-logic")`. Gradle then maps the plugin id (and its marker artifact) to the included build, so the consumer's `plugins { id("com.example.myplugin") }` resolves to your in-development source. No `publishToMavenLocal`, no version bumps — edit the plugin, re-run the consumer, and Gradle rebuilds the plugin first. This gives a single edit/run loop across both, with full IDE navigation and up-to-date checking on the plugin's compile tasks.
code
kotlin · 18 lines// consumer settings.gradle.kts
includeBuild("build-logic")
// consumer build.gradle.kts
plugins {
id("com.example.myplugin") // resolved from the included build, no publishing
}
// build-logic/build.gradle.kts
plugins { `java-gradle-plugin` }
gradlePlugin {
plugins {
register("myplugin") {
id = "com.example.myplugin"
implementationClass = "com.example.MyPlugin"
}
}
}go deeper
Know that includeBuild in settings.gradle lets a project use a plugin from another local build without publishing it.
Explain the marker-artifact substitution, the java-gradle-plugin + gradlePlugin block, and that you apply the plugin by its real id with no version.
Contrast with buildSrc and maven-local loops, explain why this exercises the true consumption path, and discuss IDE/composite import and rebuild semantics.
Frame it as a workflow choice: shareable build-logic builds vs buildSrc, monorepo plugin development, and how the substitution model keeps the developer loop fast without polluting repositories.
## The problem Normally a binary Gradle plugin lives in its own build and is consumed via the plugin id in a `plugins {}` block. To resolve that id, Gradle looks up a **plugin marker artifact** (`com.example.myplugin:com.example.myplugin.gradle.plugin`) in the configured plugin repositories. That means the usual loop is: change plugin → `publishToMavenLocal` → bump or refresh version → re-run consumer. Slow and error-prone. ## Composite builds to the rescue A **composite (included) build** wires a complete, otherwise-standalone Gradle build into another build using `includeBuild("path")` in `settings.gradle(.kts)`. Gradle treats the included build's outputs as **substitutes** for external dependencies that match its coordinates — including **plugin marker artifacts**. So if the consumer requests plugin id `com.example.myplugin`, and an included build publishes that plugin id, Gradle substitutes the in-development plugin instead of hitting a repository. ## Setting it up 1. The plugin build applies `java-gradle-plugin` and declares its id: ```kotlin // build-logic/build.gradle.kts plugins { `java-gradle-plugin` } gradlePlugin { plugins { register("myplugin") { id = "com.example.myplugin" implementationClass = "com.example.MyPlugin" } } } ``` 2. The consumer includes it: ```kotlin // settings.gradle.kts includeBuild("build-logic") ``` 3. The consumer applies it by id as if it were published: ```kotlin // build.gradle.kts plugins { id("com.example.myplugin") } ``` ## What you get - **No publishing**: Gradle compiles the plugin on demand and substitutes the marker. The `java-gradle-plugin` plugin auto-creates the marker publication metadata that the substitution matches against. - **One edit/run loop**: change the plugin source, re-run any consumer task, and Gradle rebuilds the plugin first (with normal up-to-date checks / incremental compilation). - **IDE integration**: imported as one composite, so you get cross-build navigation, refactoring, and breakpoints into plugin code. ## Key distinction vs buildSrc / convention plugins in the same build - `buildSrc` is *implicitly* an included build for the current build only — handy but it forces a full reconfiguration on any change and can't be shared. An explicit `includeBuild("build-logic")` is shareable and only rebuilds when used. - A plugin defined in the *same* multi-project build (a subproject) can be applied via project path, but you can't apply it by its public `plugins { id(...) }` id the way a real consumer would. Composite builds let you exercise the **real consumption path** (the plugin id) while still editing locally. ## Gotchas - The plugin build must actually expose the id via `gradlePlugin { plugins {} }` (or `java-gradle-plugin` auto-detection) for the marker substitution to fire. - An included build's own `settings` plugins and version catalogs are independent; only dependency/plugin substitutions cross the boundary. - `includeBuild` is declared in **settings**, never in a project build file.
- Why is the `java-gradle-plugin` plugin required in the plugin build for this to work?It generates the plugin marker publication metadata (`<id>:<id>.gradle.plugin`) and the descriptor that lets Gradle resolve the plugin id. The composite-build substitution matches the consumer's requested marker against that, so without it Gradle has nothing to substitute and the id won't resolve.
- Does the consumer need to declare a version when applying the included plugin?No. For an included build the version is irrelevant to resolution — Gradle substitutes by coordinates regardless of version, so you typically apply `id("com.example.myplugin")` with no version. A declared version is simply ignored for the substitution.
saying these in an interview costs you the question
- Saying you must run publishToMavenLocal first — that defeats the whole point of including the build.
- Putting includeBuild in a build.gradle file instead of settings.gradle.
- Claiming you must apply the plugin by project path; the point is to use the real plugin id.