How Gradle Works Part 3 - Build Script
Table of Contents
Introduction
Previously on How Gradle Works:
This is the third blog of the series How Gradle Works. In this blog we’ll explain what happens during build script execution.
Kotlin & Groovy DSL #
If you are a Java developer, when you open any Gradle build script (for example build.gradle.kts
or build.gradle
), the first thing that might confuse you is the special syntax of curly braces:
// Kotlin DSL:
plugins {
id("some.plugin") version "0.0.1"
}
// or Groovy DSL:
plugins {
id "some.plugin" version "0.0.1"
}
What’s this? What happens when Gradle executes these kinds of scripts?
The short answer is: they are DSLs (Domain Specific Language) on top of the Kotlin programming language for .gradle.kts
files, or on top of the Groovy programming language for .gradle
files.
These DSLs have some implicit rules, making them appear very confusing.
Implicit Rule 1: Lambda/Closure #
First of all, { ... }
is a special object in both Groovy and Kotlin.
This object is called a lambda in Kotlin, or a closure in Groovy.
They’re similar to function objects in other programming language, like Java’s lambda, or JavaScript’s function object.
You can think of plugins { ... }
as a method invocation in which a Kotlin lambda object or Groovy closure object is passed as the argument, because both Groovy and Kotlin allow you to omit parentheses:
plugins(function() {
...
})
Also, there is one noteworthy DSL: in Kotlin/Groovy, if the last parameter of a function is a lambda/closure, it’s allowed to be put outside of parentheses. For example, the following code snippet:
tasks.register("myTask") {
...
doLast {
...
}
}
is equivalent to:
tasks.register("myTask", function() {
...
doLast(function() {
...
})
})
The code inside the function may be executed immediately or later, depending on the implementation of the specific method.
Implicit Rule 2: Chained Method Invocation #
In the plugins { }
example above, the Kotlin version: id("some.plugin") version "0.0.1"
and the Groovy version: id "some.plugin" version "0.0.1"
are both equivalent to the chained method invocation id("some.plugin").version("0.0.1")
.
Wait, why?
Because the version
in id("some.plugin") version "0.0.1"
is actually an infix function in Kotlin, which is defined here,
and id "some.plugin" version "0.0.1"
is “command chains” in Groovy.
We won’t explain all the implicit rules in Groovy and Kotlin DSLs (as that would require another full blog series :-P), you should simply understand that they are mapped to Gradle API methods in some ways. See Kotlin DSL Primer and Groovy DSL Primer.
But what is this
of the method invocation id("some.plugin")
?
The code inside the function is executed against a this
object, which is called a “receiver” in Kotlin lambda, or a “delegate” in Groovy closure.
Gradle determines the correct this
object and invokes the method against that this
object.
In this example, the this
object is of type PluginDependenciesSpec
.
Build Script Execution #
Once we have unveiled all the mechanics of the DSL, a Gradle build script is simply a few Gradle API calls built on top of the DSLs. Like almost all other programming languages, Gradle executes the build script line by line, top to bottom.
Of course, the build script must be compiled into bytecode before being executed in the JVM. Gradle does this transparently, giving the impression that the build script is being interpreted and executed.
External Dependencies in Build Script #
Consider the following build script:
// build.gradle.kts
import com.android.build.gradle.tasks.LintGlobalTask
plugins {
id("com.android.application") version "7.4.0"
}
tasks.withType<LintGlobalTask>().configureEach {
...
}
How can this build script be compiled without specifying the dependency of com.android.build.gradle.tasks.LintGlobalTask
?
You may say, “doesn’t it come from the plugins { }
block below?”
But remember, to execute the plugins { }
block, the build script must be compiled first.
Now it’s a chicken-and-egg issue: to get the dependency of LintGlobalTask
, we must compile and run the build script, but to compile the build script, we must get the dependency of LintGlobalTask
.
Gradle handles plugins { }
specially as follows:
- The
plugins
block is extracted and executed first; - The resolved dependencies are added to the whole build script’s classpath;
- The build script is compiled and executed.
A similar thing happens to buildscript { }
block, too: you can explicitly specify the dependencies for build script compilation and execution.
In this way, you can leverage the many available libraries in the JVM ecosystem to empower your build script.
What’s Next #
The build script invokes Gradle APIs to configure the build, where amazing things happen. It’s possible to pack the build script into a Gradle plugin for better reusability and performance.
In the next blog of the series, we’ll explain what happens under the hood of Gradle plugins.