skip to content

How do you make a task in your root build depend on a task in an included build programmatically?

level: middleimportance: must knowfreq 50%

answer

  1. `gradle.includedBuild("name")`
  2. .task(":absolute:path")
  3. returns lazy TaskReference
  4. path rooted in the included build
  5. works with dependsOn/finalizedBy/mustRunAfter

basics

~10 s

Use the gradle.includedBuild('name') API to get a handle, then resolve the task: dependsOn(gradle.includedBuild("lib").task(":publishToMavenLocal")). The task path is relative to that included build's root.

solid answer

~40 s

From the CLI you address included-build tasks with a `:name:task` prefix, but **inside build scripts** you wire dependencies through the `Gradle` object. `gradle.includedBuild("lib")` returns an `IncludedBuild` handle; `.task(":path")` returns a `TaskReference` you can pass to `dependsOn`, `finalizedBy`, or `mustRunAfter`: ```kotlin tasks.register("smokeTest") { dependsOn(gradle.includedBuild("lib").task(":publishToMavenLocal")) } ``` The task path passed to `.task()` is **absolute within the included build** — it starts with `:` and is rooted at *that* build's root project, not yours. The returned reference is lazy: Gradle resolves the actual task during configuration of the composite. This is how you orchestrate cross-build ordering (e.g. publish the lib locally before integration-testing the consumer) without publishing to a remote repository.

code

kotlin · 8 lines
kotlin
// build.gradle.kts in the root (consumer) build
tasks.register("integrationTest") {
    // ensure the library is published locally first
    dependsOn(gradle.includedBuild("lib").task(":publishToMavenLocal"))
    doLast {
        println("running integration tests against freshly published lib")
    }
}

go deeper

for a junior

Recall that there is a gradle.includedBuild(...).task(...) API for cross-build dependencies.

for a middle

Write the wiring correctly, knowing the path is rooted in the included build and the reference is lazy.

for a senior

Distinguish orchestration via dependsOn from automatic dependency substitution, and choose the right mechanism.

for a principal

Set conventions for cross-build orchestration so large composites stay deterministic and avoid hidden ordering coupling.

## Two ways to reach included-build tasks 1. **CLI addressing** — `./gradlew :lib:build` — for humans invoking tasks. 2. **Programmatic addressing** — inside a build script, you cannot use the `:lib:build` string with `tasks.named(...)` because that task lives in a *different* build. Instead Gradle gives you an API on the `Gradle` object. ## The IncludedBuild API The `gradle` property (type `Gradle`) exposes: - `gradle.includedBuilds` — a collection of all `IncludedBuild` handles. - `gradle.includedBuild(name)` — look up one by its build name. An `IncludedBuild` has: - `getName()` — the build name. - `getProjectDir()` — the build's directory. - `task(path)` — returns a `TaskReference` for a task **inside that build**, where `path` is an absolute path *within the included build* (`":sub:task"`). ## Wiring a dependency ```kotlin tasks.register("integrationTest") { dependsOn(gradle.includedBuild("lib").task(":publishToMavenLocal")) } ``` Key points: - The path `":publishToMavenLocal"` is rooted at the **included** build's root project. `":core:test"` would mean the `test` task in the `core` subproject of `lib`. - The reference is **lazy** — it's a `TaskReference`, resolved when the composite's task graph is assembled, so you don't have to worry about configuration ordering between the builds. - The same reference works with `finalizedBy(...)`, `mustRunAfter(...)`, etc. ## When to prefer dependency substitution instead If the goal is "my consumer should compile against the *source* of the included library," you usually rely on Gradle's automatic **dependency substitution** (matching `group:name` coordinates to the included build) rather than a manual task `dependsOn`. Manual `dependsOn` on an included-build task is for **orchestration** — running a publish, a code-gen, or a packaging step in the other build at the right time. (Dependency substitution is a separate concern from task addressing.) ## Gotcha: build name must match `gradle.includedBuild("lib")` throws if no build named `lib` is included. Keep the `name` you set in `includeBuild(...)` consistent with what you reference here.

  • Is the path passed to `.task()` rooted in your build or the included build?
    In the included build. `":core:test"` means the `test` task of the `core` subproject of that included build, regardless of where the calling script lives.
  • What happens if you pass a build name that isn't included?
    `gradle.includedBuild(name)` fails because no matching `IncludedBuild` exists. The name must match what was set (or defaulted) in `includeBuild`.
  • Why not just use `tasks.named(":lib:build")`?
    `tasks` only addresses tasks in the *current* build. The included build is a separate build, so you must go through the `IncludedBuild` handle.

saying these in an interview costs you the question

  • Passing a path rooted in the calling build instead of the included build.
  • Trying to resolve included-build tasks via the local `tasks` container.
  • Eagerly creating tasks across builds and hitting configuration-ordering issues — the API is intentionally lazy.

context