Tasks, dependencies and lifecycle

Gradle · Practical · 12 min

Custom tasks automate everything beyond compilation, and the phases explain most puzzling build behavior.

A build is a graph of tasks. Running ./gradlew build executes build and everything it depends on, in dependency order, each task at most once.

Gradle runs in phases:

1. Initialization: settings script, which projects exist. 2. Configuration: every build script runs, registering tasks and wiring them. Nothing should do real work here. 3. Execution: only the requested tasks (and their dependencies) run their actions.

Register tasks lazily with tasks.register("name") { }; the block runs only when the task is needed. Actions go in doLast { } / doFirst { }; code outside them runs at configuration time on every build, which is a classic performance mistake.

Typed tasks give you ready-made behavior: Copy, Zip, Exec, Delete, JavaExec, Sync. Configure them with tasks.register<Copy>("stage") { from("docs"); into(layout.buildDirectory.dir("staging")) }.

Wiring: dependsOn("other") forces order and inclusion; mustRunAfter orders without requiring; finalizedBy runs cleanup afterwards. When one task consumes another's output, prefer passing the output provider (from(tasks.named("jar"))) so Gradle infers the dependency.

group and description make a task show up nicely in ./gradlew tasks. Use tasks.named("test") { } to modify an existing task rather than creating a duplicate.

Example

// build.gradle.kts
plugins { java }

println("configuration phase: runs on every build")   // avoid heavy work here

val stage = tasks.register<Copy>("stage") {
    group = "distribution"
    description = "Copies the jar and docs into build/staging"
    from(tasks.named("jar"))                 // implies dependsOn(jar)
    from("docs") { include("*.md") }
    into(layout.buildDirectory.dir("staging"))
}

tasks.register("hello") {
    group = "demo"
    doFirst { println("about to greet") }
    doLast { println("Hello from ${project.name}") }
}

tasks.register<Exec>("gitSha") {
    commandLine("git", "rev-parse", "--short", "HEAD")
}

tasks.named("build") { finalizedBy(stage) }  // stage runs after build
// $ ./gradlew hello     -> configuration phase: ... / about to greet / Hello from demo

Remember

  • Configuration phase runs all scripts; execution runs only requested tasks
  • Put work in doLast/doFirst, not in the configuration block
  • Typed tasks (Copy, Exec, Zip) cover common jobs
  • Wire tasks via outputs/providers so dependencies are inferred

Watch out for

  • Calling expensive code (shelling out, reading files) directly in a task's configuration block.

Check yourself

  1. Where should the code that actually copies files in a custom task go?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.gradle.org/current/userguide/more_about_tasks.html