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 demoRemember
- 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.