Gradle Tasks: Built-ins, ./gradlew, and Custom Tasks
Every Gradle build is a graph of tasks โ learn to run them and write your own.
In Gradle, a *task* is the atomic unit of work โ compiling sources, copying files, running tests, or zipping an artifact. A build is never a single monolithic step; it is a collection of tasks that Gradle wires together and executes in the right order. When you apply a plugin, you inherit its tasks for free. The Kotlin JVM and `application` plugins, for example, contribute the lifecycle tasks you will use every day: `build` (assemble + verify everything), `test` (compile and run the test suite), `run` (launch your `main` function), and `clean` (delete the `build/` directory so you start from scratch).
You invoke tasks through the *Gradle Wrapper*, a small script checked into every project: `./gradlew` on macOS and Linux, `gradlew.bat` on Windows. The wrapper pins an exact Gradle version (declared in `gradle/wrapper/gradle-wrapper.properties`) and downloads it on first use, so every teammate and every CI machine builds with the identical toolchain โ never run a globally installed `gradle` for project work. Typical commands are `./gradlew build`, `./gradlew test`, `./gradlew run`, and `./gradlew clean`. You can chain them (`./gradlew clean build`) and pass flags like `--info` for more logging or `-x test` to *exclude* a task from the run.
Behind the scenes Gradle builds a *task graph* โ a directed acyclic graph (DAG) where edges are dependencies between tasks. Gradle runs in two phases for this: a configuration phase, where it evaluates your build script and figures out which tasks exist and how they relate, and an execution phase, where it actually runs the selected tasks in dependency order. Because the graph is a DAG, Gradle can skip work that is already up to date (incremental builds) and safely run independent tasks in parallel. Running `build` doesn't do everything itself; it *depends on* `assemble` and `check`, which in turn depend on `compileKotlin`, `test`, and more โ Gradle resolves that whole chain automatically.
To add your own work, register a task with the lazy `tasks.register(...)` API in `build.gradle.kts`. Prefer `register` over the older eager `tasks.create` because registered tasks are configured only if they are actually part of the requested build, which keeps configuration fast. Inside the configuration block you describe the task; the actual work goes in a `doLast { }` action, which runs during the execution phase. A minimal example prints a message: `tasks.register("hello") { doLast { println("Hello from Gradle!") } }`. Run it with `./gradlew hello`.
You express ordering with `dependsOn`. Declaring `tasks.named("build") { dependsOn("hello") }` (or `finalizedBy` for cleanup steps) inserts your task into the graph so it runs whenever the dependent task runs. Keep dependencies meaningful: point them at the task that produces the input your task needs, and let Gradle compute the order rather than hard-coding sequences. A common idiom is grouping and documenting custom tasks with `group = "build"` and `description = "..."` so they appear nicely in `./gradlew tasks`.
Two more modern habits round this out. First, centralize dependency and plugin versions in `gradle/libs.versions.toml` โ the *version catalog* โ and reference them as type-safe accessors like `libs.kotlinx.coroutines.core`; this is the idiomatic Gradle 8 approach for Kotlin 2.x projects. Second, remember the difference between a *lifecycle* task (an empty aggregator like `build` that exists only to depend on others) and an *action* task (one with real `doLast`/`doFirst` work). Mastering tasks, the wrapper, the DAG, and `dependsOn` gives you everything you need to read, run, and extend any Gradle build.
// build.gradle.kts โ plugins give you build/test/run/clean for freeplugins {kotlin("jvm") version "2.1.0"application // adds the `run` task that launches your main()}application {mainClass.set("com.example.AppKt") // <file>.kt -> <File>Kt}dependencies {implementation(libs.kotlinx.coroutines.core) // from the version catalogtestImplementation(kotlin("test"))}
Applying the kotlin("jvm") and application plugins contributes the lifecycle tasks.
// build.gradle.kts โ a custom action task wired into the task graphtasks.register("hello") {group = "build" // shows under "Build tasks" in ./gradlew tasksdescription = "Prints a greeting"doLast { println("Hello from Gradle!") } // runs in the execution phase}// Make `build` depend on it: ./gradlew build now also runs `hello`tasks.named("build") { dependsOn("hello") }
register is lazy; doLast holds the work; dependsOn adds the edge to the DAG.
# gradle/libs.versions.toml โ central version catalog (Gradle 8, Kotlin 2.x)[versions]kotlin = "2.1.0"coroutines = "1.9.0"[libraries]kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }[plugins]kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Type-safe accessors: libs.kotlinx.coroutines.core and libs.plugins.kotlin.jvm.
๐ง Check your understanding
0/1 ยท 0/1 answered1. What does declaring tasks.named("build") { dependsOn("hello") } accomplish?