With the introduction of Android Studio at Google I/O in 2013, many developers were seeing Gradle for the first time. This build system, while relatively young, replaced Ant as the defacto build system for Android apps. But, just like Ant and other build systems, Gradle is constructed to be a generic build platform, not just for Android apps. Gradle is different, though, in that it uses convention over configuration coupled with scripting via Groovy. It isn’t as daunting it sounds, once you understand the basics.
Groovy
The first thing to understand about Gradle is the language used by the scripts themselves: Groovy. Groovy, also a fairly new technology, is a dynamic language based on Java. The syntax is very Java like, though it makes the line ending semi-colons optional and adds some new syntax twists. In essence, Java code can be treated like valid Groovy code, but not the other way around. In fact, you can utilize standard Java libraries in Groovy code!
One of the most heavily used features of Groovy within Gradle is the closure. A closure is an executable set of instructions which can be assigned to a field or variable, is contained within the declaring lexical scope and can also access non-local variables outside of its own lexical scope. It’s similar in concept to an anonymous class in Java, but with some subtle differences. This wikipedia article does a great job of explaining the subtle differences. Often you will use a Groovy closure within Gradle to override or specify parameters for objects. With regards to function closures, you can think of them like Java 8’s lambda functions (in face the syntax is essentially the same.) For example, let’s take a very simple Groovy function to calculate the force of an object of a given mass:
ACCEL_GRAVITY = 9.8
def calcForce(mass) { return mass * ACCEL_GRAVITY }
Using a closure we can actually create an alternate definition and show off another feature: eliminating the explicit “return” statement. The result of the last line of execution in the closure is the return value.
def calcForce2 = { mass -> mass * ACCEL_GRAVITY }
You can imagine that for more involved functions, such as Gradle task actions, these features can make the code much more compact yet still readable.
Gradle Build Files
Gradle uses a small set of files to determine what to build and how to build it. Similar to other systems, the files have specific names. However, the purpose of the files is very specific and used during certain phases of its operation. There are two primary files used during the build process: settings.gradle
and build.gradle
. The first specifies the project or projects to be build in the current tree. It is completely optional for single project builds. The syntax is straightforward:
include ":app"
This tells Gradle what projects make up the build. In this case it is assuming there is an “app” subdirectory beneath the directory where settings.gradle
is located. This “app” example was taken from a simple Android Studio based project which has a single module. Note the nomenclature difference here: Gradle calls build targets projects whereas Android Studio (IntelliJ) calls them modules. It’s subtle, but worth understanding.
The build.gradle
file is located at the top of a given Gradle project tree and specifies what is to be done for the build target. Gradle is declarative, building by convention. What this means is that you only need to tell Gradle what you are building, assuming there is a plugin available, and any customizations you need to make. There are plugins for numerous languages, such as Java, Scala and Groovy. Android gets its own plugin which ships with Android Studio. For now, let’s keep it simple and look at a build file for a Java HelloWord app.
apply plugin: "java"
That’s it. The Java plugin automatically builds our sources, assuming we have followed the convention it expects. The convention for Java source files being located under a directory structure matching the package hierarchy is enforced, but it also introduces the notion of a “source set”. Source sets can be used to group files together for building together. The Java plugin automatically defines two source sets: main and test. The main source set contains the application source and the test source set contains test code, such as JUnit test cases. By default the main source set is defined to be the src/main
subdirectory beneath our project. If we were creating a HelloWorld type application with a class of com.hiqes.example.HellowWord
, our HelloWorld.java file is located at src/main/java/com/hiqes/example/HellowWorld.java
beneath our app project tree.
Of course, Gradle is extensible and so are the plugins. In the case of the Java plugin, you can actually remap where the source sets are located, if needed. This is a good example of using a configuration closure, so let’s just make our main source set point java files at “java” and XML resources to “xml” under our app module directory rather than src/main
:
sourceSets { main { java { srcDir "java" } resources { srcDir "xml" } } }
Gradle Phases
The final topic to cover in this primer on Gradle is the phases of operation. Gradle performs our builds by creating a directed acyclic graph, or DAG, containing the various build tasks and their actions to be performed to get the end result. The DAG takes into account our project’s dependencies to get our end build result. Understanding the phases will help prevent subtle issues and many lost hours troubleshooting your build scripts.
Gradle has three phases of operation: initialization, configuration and execution. The initialization phase determines whether we are building a single or multiproject build and what projects need to be evaluated. Other global type settings are also setup in this phase. That is to say that during the initialization phase our settings.gradle
file is executed. The configuration phase is used to establish the “what” tasks are to be performed to build a project. The execution phase executes the actions for each task, or the “how” of each item to be built for a project. Our primary interests will be the configuration and execution phases.
The configuration phase is where our build.gradle
files are executed. Any closures, properties or functions we define here are executed and results are delegated to the Project object used internal to Gradle. When a plugin is applied, it has its own set of configuration items being executed, including adding new tasks. Tasks are what Gradle actually uses to perform the build by way of actions associated with a given task.
Here’s a quick example of a custom build.gradle
file and (shortened) build output, illustrating the configuration phase and execution phases:
Formenos:gradle_intro larrys$ cat build.gradle println("---> config phase print") task testTask { println("---> testTask config closure") doLast { println("---> testTask built-in action closure, execution phase") } } testTask << { println("---> testTask add-on action closure, execution phase") } Formenos:gradle_intro larrys$ gradle -info testTask Starting Build ... Included projects: [root project 'gradle_intro', project ':app'] ... ---> config phase print ---> testTask config closure All projects evaluated. Selected primary task 'testTask' Tasks to be executed: [task ':app:testTask'] :app:testTask ---> testTask built-in action closure, execution phase ---> testTask add-on action closure, execution phase :app:testTask (Thread[main,5,main]) completed. Took 0.003 secs. BUILD SUCCESSFUL Total time: 3.836 secs
Here you can see that our println is executed during the configuration phase, as is the println done within the task’s configuration closure. The other two prints are output during the execution phase as both are within action closures tied to the task. The add-on action sample shown here uses the “<<” overloaded operator for the “doLast” method. It appends the closer to the end of the action list for the task.
Final Thoughts
Gradle’s declarative approach along with the power of a dynamic language for scripting makes it a highly configurable build system. Getting your arms around these basics will go a long way in your understanding and application of the system. You can also quickly see that while Gradle is getting a lot of exposure via its use in Android Studio, it can be used to build just about anything. Has your organization started to use Gradle for more than just Android projects? What kinds of success (or failures) have you encountered?
Nice intro to Gradle. A few typos I noticed: “hierarchy is enforaced” and “The exeution phase executes”
Cheers,
Peter.
Thanks, Peter for the compliment and for the correction. I feel sheepish and now a sore forehead after the good smack I gave myself. LOL.