Gradle: what is a task, and how can I make a task depend on another task?

Andy Balaam from Andy Balaam's Blog

In an insane world, Gradle sometimes seems like the sanest choice for building a Java or Kotlin project. But what on Earth does all the stuff inside build.gradle actually mean? And when does my code run? And how do you make a task? And how do you persuade a task to depend on another task?

Setting up

To use Gradle, get hold of any version of it for long enough to create a local gradlew file, and the use that.
$ mkdir gradle-experiments
$ cd gradle-experiments
$ sudo apt install gradle  # Briefly install the system version of gradle
...
$ gradle wrapper --gradle-version=5.2.1
$ sudo apt remove gradle   # Optional - uninstalls the system version
$ ./gradlew tasks
... If all is good, this should ...
... print a list of available tasks. ...
It is normal for gradlew and the whole gradle directory it creates to be checked into source control. This means everyone who fetches the code from source control will have a predictable Gradle version.

What is build.gradle?

build.gradle is a Groovy program that Gradle runs within a context that it has set up for you. That context means that you are actually calling methods of a Project object, and modifying its properties. The fact that Groovy lets you miss out a lot of punctuation makes that harder to see, but it's true. The first thing to get your head around is that Gradle actually runs your code immediately, so if your build.gradle looks like this (and only this):
println("Hello")
when you run Gradle your code runs:
$ ./gradlew -q 
Hello
... more guff ...
So that code runs even if you don't ask Gradle to run a task containing that code. It runs at "configuration time" - i.e. when Gradle is understanding your build.gradle file. Actually, "understanding" it means executing it. Remember when I said this code runs in the context of a Project? What that means is that if you have something like this in your build.gradle:
repositories {
    jcenter()
}
what it really means is something like this:
project.repositories(
    {
        it.jcenter()
    }
)
You are calling the repositories method on the project object. The argument to the repositories method is a Groovy closure, which is a blob of code that will get run later. I've used the magic it name above to demonstrate that jcenter is just a method being called on the object that is the context for the closure when it is run. When does it run? Let's find out:
println("before")
project.repositories( {
    println("within")
    jcenter()
})
println("after")
$ ./gradlew -q
before
within
after
... more guff ...
This surprised me - it means the closure you pass in to repositories is actually run immediately, as part of running repositories, before execution gets to the line after that call. As we'll see later, some closures you create do not run immediately like this one. Once you know that build.gradle is actually modifying a Project object, you have starting point for understanding the Gradle reference documentation.

How do you make a task

You probably shouldn't do it very often, but it was instructive for me to understand how to make my own custom task. Here's an example:
tasks.register("mytask") {
    doLast {
        println("running mytask")
    }
}
This creates a new task by calling the register method on the tasks property of the Project object. Register takes two arguments: a name for the task ("mytask" here), and a closure with some code in it to run when we decide we need this task. That closure gets run in a context that can't see the Project object, but instead can see a Task object which it is helping to make. That Task object has a doLast method that we call, and passing it a closure that will be run when the task is actually executed (not immediately). If we remove some of the syntactic sugar the above build.gradle looks like this:
tasks.register(
    "mytask",
    {
        it.doLast {
            println("running mytask")
        }
    }
)
Above we can see that register really does take two arguments as I said above - the first version uses a Groovy feature where if you miss out the last argument and write a closure immediately afterwards the closure is passed as the last argument. Confusing, eh? Again, notice that doLast is a method on the Task object that is implicitly available when the closure is run. So we have created a task that we can run:
 ./gradlew -q mytask
running mytask

How do you make a task depend on another task?

If I want to run my code formatting before my compile (for example) I sometimes need to modify a task to make it depend on another one. This can done for tasks you create or for pre-existing ones. Here's an example:
plugins {
    id "java"
}
tasks.register("mytask") {
    doLast {
        println("running mytask")
    }
}
compileJava {
    dependsOn tasks.named("mytask")
}
So, calling the plugins on the Project at the top with a closure that ran the id method on something modified the Project so that it had a new method called compileJava which we called at the bottom, passing it a closure to run. That closure ran in the context of a Task object (similar to when we created a task, but now allow us to modify a pre-existing one). We called the dependsOn method of the Task object, passing in another Task object which we had got by calling the named method on the tasks object. [Side note: the register method actually returns a Task object that we could have passed to dependsOn without looking it up again using named, but Groovy doesn't provide a very convenient way of holding on to that reference, so we did do it. The Kotlin example below shows that this is quite simple in Kotlin.]

How do I do all this in Kotlin?

Because one DSL that hides what's really going on wasn't enough for you, Gradle now provides a second DSL that hides what's going on in subtly different ways, which is a program written in Kotlin instead of Groovy. This is marginally better, because Kotlin doesn't let you do quite so many stupid tricks as Groovy does. Below are all our examples in Kotlin. You get started exactly the same way, by following "Setting up" above. Remember to name your build file build.gradle.kts.

Say hello in Gradle Kotlin

println("Hello")
This is identical to the Groovy version.)

Use jcenter repo in Gradle Kotlin

repositories {
    jcenter()
}
This is identical to the Groovy version, and with the same meaning: repositories is a method on the implicitly-available Project object. The "unsugared" version looks like this in Kotlin:
this.repositories(
    {
        this.jcenter()
    }
)
[Note that the word this is used to access the implicit context. The word it has a different meaning in Kotlin from in Groovy. In Groovy it means the implicit context, but in Kotlin it means the first argument. We didn't pass any arguments to jcenter when we called it, so we can't use it, but we were being run in a context, which we can refer to using this. Simple. huh?]

Execution order in Gradle Kotlin

We this build.gradle.kts:
println("before")
project.repositories( {
    println("within")
    jcenter()
})
println("after")
We see this behaviour:
$ ./gradlew -q
before
within
after
which is all identical to the Groovy version.

Making a new task in Gradle Kotlin

tasks.register("mytask") {
    doLast {
        println("running mytask")
    }
}
This is identical to the Groovy version, but slightly different when unsugared:
tasks.register(
    "mytask",
    {
        this.doLast(
            {
                println("running mytask")
            }
        )
    }
)
Notice that Kotlin lets you do the same trick as Groovy: providing an extra argument to a function that is a closure by writing it immediately after it looks like you've finished calling it. It's good for people who dislike closing brackets hanging around longer than they're welcome. As someone who likes Lisp, I'm OK with it, but what do I know?

One task depending on another in Gradle Kotlin

plugins {
    java
}
val mytask = tasks.register("mytask") {
    doLast {
        println("running mytask")
    }
}
tasks.compileJava {
    dependsOn(mytask)
}
This differs slightly from the Groovy version, even though the meaning is the same: we start off in the context of a Project object that we call methods on. The code to make one task depend on another gets hold of the Task object called compileJava from inside the tasks property of the Project, and calls it (because it's a callable object). We pass in a closure that runs in the context of this Task object, calling its dependsOn method, and passing in a reference to the mytask object, which is a Task and was created in the code above.

Corrections and clarifications welcome

The above is what I have worked out by experimentation and trying to read the Gradle documentation. Please add comments that clear up confusions and correct mistakes.