skip to content

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?

level: middleimportance: must knowfreq 55%

answer

  1. includeBuild in settings
  2. java-gradle-plugin + gradlePlugin block
  3. plugin marker substitution
  4. no publishToMavenLocal
  5. apply by real id

basics

~10 s

Put 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 s

Composite 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
kotlin
// 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

for a junior

Know that includeBuild in settings.gradle lets a project use a plugin from another local build without publishing it.

for a middle

Explain the marker-artifact substitution, the java-gradle-plugin + gradlePlugin block, and that you apply the plugin by its real id with no version.

for a senior

Contrast with buildSrc and maven-local loops, explain why this exercises the true consumption path, and discuss IDE/composite import and rebuild semantics.

for a principal

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.

context