skip to content

Why can you address tasks in an included build but not in `buildSrc`, and what does that imply about how composites expose tasks?

level: seniorimportance: nice to knowfreq 18%

answer

  1. buildSrc = implicit classpath build
  2. included build = named composite member
  3. only included builds are addressable
  4. convert buildSrc → includeBuild(build-logic)
  5. addressability is the dividing line

basics

~20 s

buildSrc is an implicit, automatically-built helper that contributes classes/plugins — its tasks aren't part of the addressable composite graph. An included build is a first-class member of the composite, so its tasks are reachable via :buildName:task and gradle.includedBuild(...).

solid answer

~40 s

Both `buildSrc` and `includeBuild` pull external Gradle code into your build, but they participate differently. `buildSrc` is a **special, implicit** build that Gradle compiles *before* everything else and puts on the build-script classpath; you never address its tasks (`:buildSrc:test` isn't how you run them) — it's an implementation detail. An **included build** is a genuine member of the **composite**: it has a build name, its tasks are addressable from the CLI (`:lib:build`) and programmatically (`gradle.includedBuild("lib").task(":build")`), and you can wire cross-build dependencies to them. The modern guidance even is to convert `buildSrc` into an included build (`includeBuild("build-logic")`) precisely so its tasks become addressable, independently testable, and not rebuilt-on-every-invocation. So 'addressability' is the dividing line: composites expose included builds' task graphs as named, reachable nodes; `buildSrc` deliberately does not.

code

kotlin · 4 lines
kotlin
// settings.gradle.kts — make build logic addressable
includeBuild("build-logic")
// now you can run:  ./gradlew :build-logic:test
// and wire:        dependsOn(gradle.includedBuild("build-logic").task(":check"))

go deeper

for a junior

Know buildSrc is automatic and its tasks aren't addressed like an included build's.

for a middle

Explain that included builds are named composite members with reachable task graphs, unlike buildSrc.

for a senior

Articulate the addressability contract and why converting buildSrc to an included build unlocks it.

for a principal

Decide org-wide whether build logic lives in buildSrc or an included build-logic, factoring testability, caching, and addressability.

## Two ways to share build logic - **`buildSrc/`** — a conventionally-named directory at the root. Gradle *automatically* treats it as a build, compiles it first, and adds its output to the build-script classpath of every project. It exists to hold plugins, conventions, and helper classes. - **`includeBuild(...)`** — explicitly wires a standalone build into a **composite build**. ## The addressability difference The key distinction for *task addressing*: | Aspect | `buildSrc` | Included build | |---|---|---| | Has a build name you address by | No (implicit) | Yes (`lib`, default = dir name) | | CLI: `:name:task` | Not the intended interface | `./gradlew :lib:build` | | Programmatic: `gradle.includedBuild(name)` | Not listed | Yes, returns an `IncludedBuild` | | Rebuilt | On essentially every invocation | Only when its inputs change / when addressed | `buildSrc` is an **implementation detail of the build's classpath**, not a participant whose tasks you orchestrate. Its tasks run as part of building the classpath, not as addressable nodes you depend on with `gradle.includedBuild(...)`. ## Why convert buildSrc → included build Because an included build's tasks *are* addressable, you gain: ```kotlin // settings.gradle.kts includeBuild("build-logic") ``` ```bash ./gradlew :build-logic:test # run the convention plugins' tests directly ./gradlew :build-logic:check ``` That is impossible with `buildSrc`, where you can't cleanly target `:buildSrc:test` as part of your normal task addressing. You also avoid `buildSrc`'s tendency to invalidate caches and be reconfigured frequently. ## Implication for composites A composite build's contract is: **each included build contributes a named, fully-addressable task graph**. That's what makes `:lib:publishToMavenLocal` and `gradle.includedBuild("lib").task(":publishToMavenLocal")` work. `buildSrc` opts out of that contract by design — it's there to *provide build logic*, not to be *driven* as a build. ## Nuance This isn't about capability of the code inside; both can host plugins. It's specifically about whether the build's **tasks are exposed as reachable composite nodes**. Only included builds are.

  • Practically, why do teams migrate from `buildSrc` to an included `build-logic`?
    To make the build logic's own tasks (tests, checks) addressable and independently runnable, to avoid frequent reconfiguration/cache invalidation, and to gain composite-style orchestration.
  • Does `gradle.includedBuilds` ever contain `buildSrc`?
    No. `buildSrc` is an implicit special build, not part of the included-builds collection, so it isn't addressable through that API.

saying these in an interview costs you the question

  • Claiming you address `buildSrc` tasks the same way as included builds.
  • Believing `buildSrc` appears in `gradle.includedBuilds`.

context