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.