skip to content

In a composite build where the consumer applies an in-development plugin, what happens when you change the plugin source and re-run a consumer task?

level: middleimportance: should knowfreq 30%

answer

  1. cross-build task dependency
  2. plugin builds first
  3. incremental + UP-TO-DATE
  4. no publish, no version bump
  5. one gradle invocation

basics

~10 s

Gradle rebuilds the included plugin build first (with normal up-to-date checks), then runs the consumer task against the freshly compiled plugin. No publishing or version bump is needed between edits.

solid answer

~40 s

Because the plugin's marker is substituted to the included build, the consumer build has an implicit task dependency on the plugin's compile/jar tasks. When you edit the plugin and re-run any consumer task, Gradle first executes the included build's tasks to produce the up-to-date plugin artifact, applying normal incremental-compilation and up-to-date checking — only what changed recompiles. Then it applies the freshly built plugin to the consumer and runs the requested task. The whole thing is one invocation: no `publishToMavenLocal`, no version change, no cache eviction dance. If nothing in the plugin changed, the plugin tasks are UP-TO-DATE and Gradle skips straight to the consumer work. This tight loop — edit plugin, run consumer, repeat — is the core benefit of co-developing them in a composite build.

code

bash · 4 lines
bash
# Edit build-logic/src/main/kotlin/com/example/MyPlugin.kt, then:
./gradlew :app:check
# Gradle output shows the plugin build's compile/jar run first
# (or UP-TO-DATE if unchanged), then :app tasks run with the new plugin

go deeper

for a junior

Know that one Gradle command rebuilds the plugin and then runs the consumer, with no manual publishing.

for a middle

Explain the cross-build task dependency, incremental/up-to-date behavior, and why no version bump is needed.

for a senior

Discuss configuration-time vs execution-time effects, cache/incremental correctness, and diagnosing stale outputs.

for a principal

Reason about keeping the loop fast and correct at scale — task input/output hygiene and build-cache participation across included builds.

## The dependency edge across builds When the consumer applies `id("com.example.myplugin")` and that id is substituted to an included build, Gradle records a **cross-build dependency**: the consumer's plugin classpath depends on the included build's plugin JAR. Resolving that classpath triggers the included build's tasks (compileKotlin/compileJava → jar) just like resolving any project dependency triggers its producing tasks. ## What a single run does 1. You invoke a consumer task (say `./gradlew run` or `./gradlew build`). 2. To configure/apply the plugin, Gradle resolves the plugin classpath, which forces the included build to **build the plugin** first. 3. Up-to-date checks and incremental compilation apply to the plugin build: only changed sources recompile; if nothing changed, those tasks report **UP-TO-DATE** and are skipped. 4. The freshly built plugin is applied; the consumer task then runs against the new behavior. ## Why no publishing or version bump With Maven-local loops you'd `publishToMavenLocal`, and you often had to change the version or invalidate caches so the consumer picked up new bytes. Substitution sidesteps all that: there's no repository round-trip and no version comparison — Gradle uses the *built output* directly, every run. ## Caching and correctness - The plugin build participates in the **build cache** and configuration as a normal build, so its tasks are cacheable/incremental. - Mark plugin tasks correctly (inputs/outputs, `@CacheableTask` where appropriate) so up-to-date checking stays accurate. - IDE imports the composite as one project, so editing the plugin and re-running from the IDE follows the same rebuild ordering. ```bash # one command, both builds: ./gradlew :app:run # builds build-logic plugin if changed, then runs app ``` ## Gotchas - Configuration-time errors in the plugin surface when the consumer *configures*, not only when it runs — because applying the plugin happens at configuration time. - If you don't see your change, suspect stale up-to-date inputs/outputs in the plugin task wiring, not the substitution itself.

  • If your plugin edit doesn't seem to take effect in the consumer, what's the most likely cause?
    Incorrect up-to-date wiring on the plugin's tasks — declared inputs/outputs that don't reflect the real source so Gradle marks them UP-TO-DATE and reuses stale output. The substitution itself is automatic; the staleness usually comes from task input/output declarations or an aggressive cache, not from includeBuild.
  • At what phase does a plugin's configuration-time bug appear in the consumer?
    During the consumer's configuration phase, because applying a plugin runs its `apply`/configuration logic then. So a broken plugin can fail the consumer before any task action executes.

saying these in an interview costs you the question

  • Saying you must republish or change the version between edits.
  • Claiming Gradle always fully recompiles the plugin every run (it's incremental / up-to-date aware).
  • Thinking the consumer and plugin run as two separate manual commands.

context