skip to content

You maintain an internal plugin that should add an integration-test source set and a JaCoCo report wiring, but only if both the java and jacoco plugins are present. How do you wire this reliably?

level: seniorimportance: should knowfreq 30%

answer

  1. nest withPlugin for 'both present'
  2. inner closes over outer's source set
  3. order-independent either way
  4. tasks.register/named stay lazy
  5. beats afterEvaluate hasPlugin&&

basics

~20 s

Nest reactions: react to java with withPlugin("java") to add the source set, and inside (or separately) react to jacoco with withPlugin("jacoco") to wire the report. Each block runs only when its plugin is present, so both conditions must hold.

solid answer

~40 s

Use composed reactions rather than a single end-of-configuration check. React to the `java` plugin to create the integration-test source set and its configurations/tasks; react to the `jacoco` plugin to wire coverage. To require **both**, nest the reactions: `pluginManager.withPlugin("java") { ... pluginManager.withPlugin("jacoco") { ... } }`. The inner block runs only when jacoco is applied *and* you're already in the java reaction — so the JaCoCo wiring touches the java-created source set safely, regardless of which plugin was applied first. Because each `withPlugin` fires immediately-or-later, application order between java and jacoco doesn't matter. Keep all task references lazy (`tasks.register`, `tasks.named`) so nothing is eagerly created and the configuration cache stays happy. This is far more robust than `afterEvaluate { if (hasPlugin(...) && hasPlugin(...)) }`, which is order-fragile and runs too late.

code

kotlin · 13 lines
kotlin
pluginManager.withPlugin("java") {
    val ss = extensions.getByType<SourceSetContainer>()
    val itSet = ss.create("integrationTest")
    val itTask = tasks.register<Test>("integrationTest") {
        testClassesDirs = itSet.output.classesDirs
        classpath = itSet.runtimeClasspath
    }
    pluginManager.withPlugin("jacoco") {
        tasks.named<JacocoReport>("jacocoTestReport") {
            executionData(itTask.get())
        }
    }
}

go deeper

for a junior

Recognize that you react to each plugin; deep nesting details optional.

for a middle

Explain nesting to require both plugins and why order doesn't matter.

for a senior

Design the composed reactions with lazy task wiring and justify over afterEvaluate.

for a principal

Generalize to a pattern library for cross-capability wiring across an org's build platform, ensuring config-cache compatibility and no apply-order coupling.

## The requirement You want behaviour that exists **only at the intersection** of two plugins: add an `integrationTest` source set (needs `java`) and wire its coverage into JaCoCo reports (needs `jacoco`). Neither plugin should be forced; both must be present for the wiring to appear. ## Compose reactions, don't poll The robust approach nests the two reactions so the inner action requires both: ```kotlin pluginManager.withPlugin("java") { val sourceSets = extensions.getByType<SourceSetContainer>() val integrationTest = sourceSets.create("integrationTest") { compileClasspath += sourceSets["main"].output runtimeClasspath += sourceSets["main"].output } val integrationTestTask = tasks.register<Test>("integrationTest") { testClassesDirs = integrationTest.output.classesDirs classpath = integrationTest.runtimeClasspath useJUnitPlatform() } // Only when jacoco is ALSO applied: pluginManager.withPlugin("jacoco") { tasks.named<JacocoReport>("jacocoTestReport") { executionData(integrationTestTask.get()) mustRunAfter(integrationTestTask) } } } ``` ### Why nesting works - The outer block runs only if/when `java` is applied; inside it the java source sets exist. - The inner block is registered *from within* the outer block, so it only fires if `jacoco` is also applied — and it can close over `integrationTest`/`integrationTestTask` created by the outer block. - Because each `withPlugin` is immediate-or-later, **application order is irrelevant**: java-then-jacoco, jacoco-then-java, or interleaved all converge to the same result. ### Independent siblings vs nesting If the jacoco wiring did **not** need anything from the java reaction, you could register two independent top-level reactions. Nest only when the inner action depends on artifacts the outer one created — that captures the 'both present' condition and the data dependency in one structure. ## Keep it lazy and config-cache safe - Use `tasks.register` (lazy create) and `tasks.named` (lazy configure) — never `tasks.create`/`tasks.getByName`, which realize tasks eagerly. - Don't reach into other projects' internals; wire via tasks/providers. ## Why not afterEvaluate ```kotlin afterEvaluate { if (plugins.hasPlugin("java") && plugins.hasPlugin("jacoco")) { /* ... */ } } ``` This runs late, competes with other `afterEvaluate` blocks for ordering, and is easy to get wrong when other plugins also register late callbacks. Composed reactions express the intent precisely and run at the right time.

  • Does it matter whether java or jacoco is applied first?
    No. Each withPlugin fires immediately-or-later, so the nested reactions converge to the same result regardless of application order.
  • When would you use two independent top-level reactions instead of nesting?
    When the second reaction doesn't depend on anything the first created — then they're independent and don't need to capture a 'both present' condition jointly.
  • Why prefer tasks.register over tasks.create here?
    register is lazy — the task is only realized if needed, keeping configuration time and the configuration cache efficient; create realizes eagerly.

saying these in an interview costs you the question

  • Using afterEvaluate with a hasPlugin && hasPlugin check and calling it robust.
  • Eagerly realizing tasks with create/getByName inside the reactions.
  • Assuming a fixed apply order between the two plugins.

context