The Secrets of Gradle: How It Works and How to Use It Effectively

Roman Glushach
7 min readJul 7, 2023

--

How Gradle Works

Gradle is a powerful and flexible build automation tool that can help you manage complex projects with ease. It’s based on a domain-specific language (DSL) that uses Groovy or Kotlin syntax to define tasks, dependencies, plugins, and configurations. A Gradle project consists of one or more build scripts that apply plugins and configure tasks. A task is a unit of work that can perform any action, such as compiling, testing, or packaging. A plugin is a reusable component that adds functionality to a project, such as support for Java, Android, or Spring Boot.

Gradle uses a directed acyclic graph (DAG — is a mathematical algorithm for representing a graph that contains no cycles) to determine the order and execution of tasks. A DAG is a data structure that represents the dependencies between tasks, where each node is a task and each edge is a dependency. Gradle analyzes the DAG before running any task to optimize the build process and avoid unnecessary work.

One of the key features of Gradle is its incremental build support. This means that Gradle only executes the tasks that are affected by the changes in the source code or the inputs. Gradle also caches the outputs of tasks to reuse them in subsequent builds, which can significantly improve the performance and efficiency of your builds.

Another feature of Gradle is its dependency management system. Gradle can resolve dependencies from various sources, such as local files, Maven repositories, or Ivy repositories. Gradle also supports transitive dependencies, which are the dependencies of your dependencies. Gradle can automatically download and manage these dependencies for you, as well as handle conflicts and versioning issues.

Gradle starts up

Locally

After installation of Gradle on your system you should be able to see the file under $/path/to/gradle/distribution/bin/gradle file.

After opening it with any text editor you’ll see that it’s a simple shell script under the hood. At the end of the script you’ll find exec "$JAVACMD" "$@" line. The exec is a shell command that replaces the current shell process with the command specified after it. "$JAVACMD" specifies the command to be executed, where it’s an environment variable that contains the path to the Java executable. "$@" represents all the arguments passed to the script, which are then passed on to the command being executed.

On execution of the script it run the Java code in org.gradle.launcher.GradleMain class which is entry point of Gradle Client JVM. exec is lightweight JVM that doesn’t run any real build logic. It searches for a compatible Gradle Daemon and it’ll start a daemon or connects to it via a local socket. Then the Gradle Client JVM forwards input (command line arguments, environment variables and others) to the Gradle Daemon JVM. The Gradle Daemon JVM runs the build, and sends output (stdout/stderr) back to the Gradle Client JVM. After the build finishes, the Gradle Client JVM will exit, but the Gradle Daemon JVM may stay running for a while.

However, if you pass — no-deamon to the Gradle build, the client JVM may convert itself into a daemon JVM if it is compatible with build requirements. In this case, there is no daemon, no communication, no input/output forwarding at all — the build happens inside the single JVM. But if — no-deamonis present and the client JVM is not compatible with build requirements, a new disposable JVM will still be started for the build and exit at the end of the build.

Wrapper

It’will act the same way as you’ll execute it locally except the initial part.

It’ll start a tiny JVM from gradle/wrapper/gradle-wrapper.jar. This JVM will locate or download a specific version of Gradle distribution declared in gradle/wrapper/gradle-wrapper.properties, and start Gradle inside the same JVM via Java reflection.

IDE: Integrated Development Environment

The difference for IDE is the communication between IDE and Gradle Client JVM which is called Tooling API.

On Gradle Sync IntelliJ IDEA will start a special Gradle build to fetch necessary information (project structure, dependencies, tasks, etc.) of the project via the Tooling API that reads the results and return it back. All the build logic happens in a Gradle Daemon JVM.

Gradle Deamon

The Gradle Daemon is a background process that speeds up build times by caching project information, running in the background to eliminate JVM startup time, benefiting from continuous runtime optimization in the JVM, and monitoring the file system to determine what needs to be rebuilt before a build is run and it was introduced in Gradle 3.0.

Once the Gradle Client JVM establishes a connection with an idle daemon that is compatible, it transmits the required build data, such as command line arguments, project directory, and environment variables. The daemon then initiates the build process and returns the build output, including logs and standard output/error, to the client via a local socket connection.

Initialization Phase

Once the daemon has all the necessary information about the build, it begins to create internal representations of the build using Java objects. For instance, a Gradle instance represents the entire Gradle build invocation, while a Settings instance represents the configuration needed to set up the project hierarchy. Each project being built has a corresponding Project instance.

These objects, Gradle, Settings, and Project, are also the default delegation for init, settings, and build scripts. This allows for interaction with these objects within the build script. For example, when println(name) is called in a build script, it is actually invoking the Project.getName() method on the Project instance.

Configuration Phase

Once the required JVM objects are set up, Gradle will load and run the build scripts using the daemon. Build scripts are typically named X.gradle (Groovy DSL) or X.gradle.kts (Kotlin DSL) and are located in the project directory. Both Groovy and Kotlin are JVM languages, so they can run smoothly within a JVM, such as the daemon JVM.

For instance, the Groovy build script repositories { mavenCentral() }creates a Groovy Closure instance and passes it to the Project.repositories(Closure) method on the Project instance that was created during the previous initialization phase.

The build script fills the daemon JVM’s data structures for the build. For example, tasks.register(“hello”) { doLast { println("Hello world!") } }build script adds a hello task to Gradle’s task container data structure (TaskContainer class), which means a Task instance will be created when necessary.

Once the build script has finished running, the build data structures will be set up with all the information needed for the build.

At this stage Gradle builds a DAG of task objects.

Gradle DAG: Directed Acyclic Graph

Execution Phase

Gradle stores all the data needed for the build in the daemon JVM. Then selects and executes specific tasks based on the arguments passed to the gradle command. Each task has a list of actions, which are chunks of code that are executed. For example, the Test task has an action called executeTests that can be found by searching for TaskAction attribute in the source code.

When a task is executed, it means that the code in its actions is run in the daemon JVM. However, some actions may choose to create new JVMs and run code in them. For instance, the Gradle Worker API allows a task action to be split into smaller pieces and executed in child processes.

Another example is the Test task, which forks new JVMs to run test code and prevent interference with the daemon JVM. After the build is complete, the daemon performs additional tasks such as executing callbacks, reporting errors, and publishing build scans. The Gradle Client JVM then disconnects from the daemon and exits, leaving the daemon ready for the next build.

DSL: Domain-Specific Language

The curly brace syntax in Gradle build scripts, such as build.gradle.kts or build.gradle is actually a DSL (Domain Specific Language) built on top of either the Kotlin or Groovy programming languages for .gradle.kts and .gradle files respectively. The implicit rules of these DSLs can make them appear perplexing.

Lambda/Closure

In both Groovy and Kotlin, there is a special object called a closure or lambda, respectively. These objects are similar to function objects found in other programming languages, such as Java’s lambda or JavaScript’s function object.

The plugins { … } syntax can be thought of as a method invocation where a Kotlin lambda or Groovy closure is passed as an argument. Both languages allow the omission of parentheses in this case, so it can be written as plugins(function() { … }).

Additionally, there is a noteworthy DSL feature: if the last parameter of a function is a lambda/closure, it can be placed outside of the parentheses. For example, tasks.register(“hello”) { … doLast { … } } is equivalent to tasks.register(“hello”, function() { … doLast(function() { … }) }). The code within the function may be executed immediately or at a later time, depending on the specific method’s implementation.

Chained Method Invocation

Both the Kotlin and Groovy versions of the plugin declaration are equivalent to the chained method invocation id(“some.plugin”).version(“0.0.1”). This is because the version in id(“some.plugin”) version “0.0.1” is an infix function in Kotlin, and id “some.plugin” version “0.0.1” uses “command chains” in Groovy.

As for the method invocation id(“some.plugin”), the code inside the function is executed against this object, known as a “receiver” in Kotlin lambda or a “delegate” in Groovy closure. Gradle determines the correct this object and invokes the method against it.

External Dependencies

Gradle solves this issue by handling the plugins block specially. It extracts and executes the plugins block first, adds the resolved dependencies to the build script’s classpath, and then compiles and executes the build script.

A similar process happens with the buildscript block, where dependencies for build script compilation and execution can be explicitly specified. This allows to use libraries from the JVM ecosystem to enhance build script.

Final words

As you can see, Gradle is a versatile and powerful tool that can handle any kind of project. Whether you are working on a simple Java application or a complex multi-module project with different languages and platforms, Gradle can help you achieve your goals with less code and more productivity.

--

--