How do you wire dynamic, computed task dependencies inside a custom task type — for example via `TaskDependency` / `Buildable` or a `@TaskAction`-time set, and what is `getTaskDependencies()` for?
answer
- deps resolved at graph time, not in @TaskAction
- dependsOn accepts Callable/Provider for lazy sets
- Buildable.getBuildDependencies carries producers
- FileCollection/Provider are Buildable
- never add deps from doFirst/doLast
basics
~20 sTask dependencies are computed at graph-construction time, not execution time. To make them dynamic you pass a Callable/closure or Provider to dependsOn, or implement Buildable.getBuildDependencies() on an input value so the value 'carries' the tasks that build it.
solid answer
~40 sGradle resolves a task's dependencies via its `TaskDependency` (exposed through `Task.getTaskDependencies()`), which it queries while building the graph — so dependencies must be derivable *before execution*, never set from inside `@TaskAction`. For dynamic edges you have three idiomatic tools: (1) pass a `Callable`/lambda to `dependsOn` so the set of dependencies is computed lazily at graph time; (2) wire `Provider`/`Property` outputs so producers are inferred automatically (the cleanest path); and (3) for custom value types, implement `Buildable` — its `getBuildDependencies(): TaskDependency` lets a value declare which tasks must run to produce it, so passing that value into a task input transfers the dependencies. `FileCollection` and `Provider` already implement this. `getTaskDependencies()` is the read side Gradle uses internally; you typically *contribute* dependencies (via `dependsOn`/wiring/`Buildable`) rather than overriding it directly.
code
kotlin · 12 lines// Dynamic dependency set computed lazily at graph time
tasks.register("aggregateReports") {
dependsOn(provider {
tasks.withType<Test>().matching { it.name.endsWith("Test") }
})
}
// Buildable value type carrying its producer tasks
class Artifact(val file: java.io.File, val producer: TaskProvider<*>) : Buildable {
override fun getBuildDependencies(): TaskDependency =
TaskDependency { setOf(producer.get()) }
}go deeper
Just know dependencies are set up before tasks run, not during them.
Explain lazy dependsOn via Callable/Provider and that you can't add deps at execution time.
Discuss Buildable.getBuildDependencies, how FileCollection/Provider carry producers, and the configuration-vs-execution phase boundary.
Design custom value types and plugin APIs whose inputs carry build dependencies, so consumers get correct graphs without manual wiring across a large codebase.
## Dependencies are computed at graph time Gradle has two phases: **configuration** (build the task graph) and **execution** (run tasks). Task dependencies are resolved during graph construction, **before** any `@TaskAction` runs. Therefore you can **never** add a dependency from inside an action — by then the graph is fixed. This is a classic senior gotcha. ## How Gradle reads dependencies Every task exposes `getTaskDependencies(): TaskDependency`. A `TaskDependency` is a function `getDependencies(task): Set<Task>`. Gradle calls it while assembling the DAG. You rarely implement this directly; instead you *feed* it. ## Tool 1 — lazy `dependsOn` with a Callable ```kotlin tasks.register("aggregate") { // Computed lazily at graph time, not hardcoded dependsOn(provider { tasks.withType<JavaCompile>().filter { it.name.startsWith("compileFeature") } }) } ``` `dependsOn` accepts a `Callable`/`Provider`/closure; Gradle evaluates it when resolving dependencies, allowing the set to depend on configuration-time state. ## Tool 2 — Provider/Property wiring (preferred) As covered elsewhere, assigning a producer's output `Provider` into a consumer input infers the producer task. This is the cleanest dynamic mechanism: the dependency follows the data. ## Tool 3 — `Buildable` value types If you create a domain type that is *produced* by tasks, implement `Buildable`: ```kotlin class GeneratedArtifact( private val file: File, private val producer: TaskProvider<*> ) : Buildable { override fun getBuildDependencies(): TaskDependency = TaskDependency { setOf(producer.get()) } } ``` Now any task that takes a `GeneratedArtifact` as input and registers it appropriately will pull in `producer`. Gradle's own `FileCollection`, `Provider`, and `ConfigurableFileTree` implement `Buildable`, which is precisely why wiring a `FileCollection` input infers the tasks that build it. ## What `getTaskDependencies()` returns It returns the aggregate `TaskDependency` combining: explicit `dependsOn` entries, inferred provider producers, and `Buildable` build-dependencies of input values. Gradle queries it once per graph build. ## Anti-pattern to call out ```kotlin tasks.register("bad") { doFirst { dependsOn("other") } // TOO LATE — graph already built } ``` This silently has no effect (or warns) because dependencies were resolved before the action ran. The fix is to declare the dependency at configuration time via one of the three tools above. ## Summary - Resolve-before-execution: declare deps at configuration time. - Dynamic deps → `Callable`/`Provider` to `dependsOn`, or provider wiring, or `Buildable`. - `getTaskDependencies()`/`TaskDependency` is Gradle's read mechanism; you contribute to it.
- Why can't you call `dependsOn` from inside a `@TaskAction`?Because the task graph is fully resolved during configuration, before any action executes; adding a dependency at execution time is too late and has no effect.
- What standard Gradle types already implement `Buildable`?`FileCollection`, `Provider`/`Property`, and `ConfigurableFileTree` — which is why wiring them as inputs infers the tasks that build them.
- When would you pass a `Callable` to `dependsOn` instead of wiring a Provider?When the dependency set is a dynamic collection of tasks selected by name/type/filter rather than a single produced artifact you can wire by data flow.
saying these in an interview costs you the question
- Trying to mutate dependencies inside doFirst/doLast/@TaskAction.
- Hardcoding task names eagerly instead of computing dependencies lazily.
- Overriding getTaskDependencies() by hand when dependsOn/wiring/Buildable would do.