Abstract
Gradle is a build manager based upon an Ant-like task dependency graph expressed in a more human-friendly notation, with a Maven-like ability to express standard project layouts and build conventions.
In this lesson we look at how Gradle combines some of the better features of Ant
and Maven
, while providing a more convenient notation than either.
We will look at how Gradle could be applied to some of our sample projects.
Gradle devised by GradleWare, founded by Hans Dockter, released in 2012
Has become the standard build tool for Android
Tries to strike a middle ground between Ant and Maven
Keep:
Leave
“XML was an easy choice for a build tool a decade ago, when it was a new technology, developers were enthusiastic about it, and no one yet knew the pain of reading it in large quantities. It seemed to be human-readable, and it was very easy to write code to parse it. However, a decade of experience has shown that large and complex XML files are only easy for machines to read, not for humans. Also, XML’s strictly hierarchical structure limits the expressiveness of the format. It’s easy to show nesting relationships in XML, but it’s hard to express program flow and data access the way most common programming language idioms express them. Ultimately, XML is the wrong format for a build file.”
Tim Berglund, Learning and Testing with Gradle
Keep:
Dependency management
Standard directory layouts and build conventions for common project types.
Leave
XML as a build language.
Inflexibility
Inability to express simple control flow
Build files are written in Groovy
Gradle is built on top of the Ant libraries.
Hans Dockter describes Gradle, compares it to Ant and Maven, and shows lots of examples (in Eclipse).
Suppose that you are building your project with Gradle.
If you set up your project with the Gradle Wrapper, you get a simple script named gradlew
.
gradlew
checks to see if the system on which it is running has Gradle installed.
$USER_HOME/.gradle
gradle
(old installation or new), passing any parameters you supplied to the gradlew
command.This works nicely for projects that distribute their source code via any of the version control systems that we will be discussing later.
gradle looks for its instructions in a file named, by default, build.gradle
The gradle command can name any task (or list of tasks) as targets to be built, e.g.,
gradle setup compile
If no target is given, gradle can use a default task if one has been declared in the build file.
gradle Options
Some useful options:
-b filename, –build-file filename : Use filename instead of the default build.xml. Also -file
or -buildfile
gradle Tasks
Some built-in tasks that you can use as targets:
--task
, ask for a description of a specific task.The gradle build file is a Groovy script, with Java-like syntax. Many of the more common Java API packages are imported automatically.
task upper << {
String myName = "Steven Zeil";
println myName;
println myName.toUpperCase();
}
If this is placed in build.gradle
, then we can run it:
$ gradle upper
:upper
Steven Zeil
STEVEN ZEIL
BUILD SUCCESSFUL
Total time: 1.747 secs
$
The basic elements of a Gradle build file are tasks.
Gradle tasks can correspond Ant targets.
A Gradle task can perform multiple actions.
task upper << {
String myName = "Steven Zeil";
println myName;
println myName.toUpperCase();
}
Gradle tasks can correspond individiual Ant tasks.
A Gradle task can perform multiple actions.
task copyResources (type: Copy) {
from(file('src/main/resources'))
into(file('target/classes'))
}
In Ant, we would have used a <copy>
task within a larger Ant target for this purpose.
Before looking at the components of a task, we need to understand a bit about how Gradle works.
A Gradle run occurs in four specific phases.
Initialization takes care of things that affect how Gradle itself will run. The most visible activity during this phase is loading Gradle plugins that add new behaviors.
Configuration involves collecting the build’s tasks, setting the properties of those tasks, and then deciding which tasks need to be executed and the order in which that will be done.
Execution is when the tasks that need to be executed get run.
Finalization covers any needed cleanup before the Gradle run ends.
Software developers will be mainly concerned with the middle two phases. For example, if we need to compile some Java source code for a project, then we would want to configure that task by indicating the source code directory (or directories) and the destination for the generated code. With that information, Gradle can look to see if the .class
files already exist from a prior run and whether the source code has been modified since then. Gradle can then decide whether or not the compilation task actually needs to be run this time. This decision is remembered during the execution phase when the task will or will not be run, accordingly.
A Gradle task can be declared as easily as:
task myTask
Some tasks may need parameters:
task copyResources (type: copy)
You can add code to be run at configuration time by putting it in { }
brackets just after the task name:
task copyResources (type: Copy)
copyResources {
description = 'Copy resources into a directory from which they will be added to the Jar'
from(file('src/main/resources'))
into(file('target/classes'))
}
You can combine a configuration with the task declaration:
task copyResources (type: Copy) {
description = 'Copy resources into a directory from which they will be added to the Jar'
from(file('src/main/resources'))
into(file('target/classes'))
}
For example, the Copy
type copies files at execution time
task copyResources (type: Copy) {
from(file('src/main/resources'))
into(file('target/classes'))
}
The from
and into
calls are performed at configuration time.
The Copy
type actually copies the files at execution time.
gradle
will check to see, at configuration time, if the files already exist at the destination and appear to be no olderthan the ones at the source. If so, the copyResources
task will be skipped at execution time.{ }
, using the append <<
operator.
```java
task copyResources (type: Copy) {
from(file('src/main/resources'))
into(file('target/classes'))
} << {
println 'Copy has been done.'
}
```
task setupFormat
task setupGraphics
task setupSourceCode
task generatePDFs (dependsOn: 'setup') << { ➀
println 'in task generatePDFs'
}
task setup (dependsOn: ['setupFormat',
'setupGraphics', 'setupSourceCode']) << { ➁
println 'in task setup'
}
task deploy (dependsOn: generatePDFs) << {
println 'in task deploy'
}
➀ : This line shows a dependency. Note that the task on which we are depending has not been declared yet.
➁ : The [a, b, ...]
notation introduces a Groovy array value.
The dependsOn
parameter expects an array. But note that we did not use an array at ➀ . like most scripting languages, Groovy has lots of little shortcuts designed to make like easier for programmers. At ➀ , we got away due to a shortcut that allows a single element to be passed as an array of length 1.
Running this gives:
$ gradle deploy
:setupFormat UP-TO-DATE
:setupGraphics UP-TO-DATE
:setupSourceCode UP-TO-DATE
:setup
in task setup
:generatePDFs
in task generatePDFs
:deploy
in task deploy
The <<
is actually an “append” operator.
This allows us to easily extend tasks.
If we add the following to the previous script:
setup << {
println 'still in task setup'
}
Without the keyword task
in front, the setup <<
is just operating on an already declared task object.
Running this gives:
$ gradle deploy
:setupFormat UP-TO-DATE
:setupGraphics UP-TO-DATE
:setupSourceCode UP-TO-DATE
:setup
in task setup
still in task setup
:generatePDFs
in task generatePDFs
:deploy
in task deploy
At its heart, a build file is a collection of tasks and declarations.
A task is a Groovy function. It has a name, a body, and, optionally,
The body of a task can contain multiple declarations and commands.
Gradle tasks are equivalent to Ant “targets”.
task playWithFiles << {
def files = file('src/main/data').listFiles().sort() ➀
files.each { File file -> ➁
if (file.isFile()) { ➂
def size = file.length() ➃
println "** $file.name has length " + size ➄
}
}
}
➀ There’s several things going on here. Let’s take it piece by piece.
def
declares a variable. In this case, the variable is named files
.
Like most scripting languages, Groovy (and therefore Gradle) is loosely (dynamically) typed, so we generally do not bother declaring variables by giving their type, though that’s certainly possible.
file(...)
returns a value of type File
. In fact, this is a good old fashioned Java java.io.File
, so we can look to the Java documentation to see what can be done with it.
One of the things we can do with it is to call listFiles()
, which treats the File
on the left of the ‘.’ as a directory and produces an array of File
representing all the files in that directory.
And, knowing that listFiles()
produces and array of files, it’s pretty obvious what .sort()
would do.
We conclude that the variable files
will hold a sorted list of all the files in directory src/main/data
.
➁ The .each
function is a Groovy function on arrays that allows us to iterate through the elements of the array, one at a time. On each iteration, we will store the current array element in the variable file
which we have declared, for clarity, as being of type File
.
➂ Another thing we can do with File
s is to check and see if they are “regular” files as opposed to being directories, links, or other special cases.
Again, this is not a special Gradle function, but is part of the normal Java behavior of a File
class.
➃ The call to file.length()
is just an ordinary Java function call.
➄ Several interesting things happen here.
The ‘$’ inside the quoted string allows us to request the replacement of a simple expression by its value. So we won’t actually print “file.name”. Instead the expressions file.name
will be evaluated and the resulting value inserted into the string to be printed.
This “$” string substitution has been a common shortcut in scripting languages for decades.
Now, Java File
s do not have a public data member called “name”, so the expression file.name
would fail to compile in Java.
Java File
s do, however, have a public function member getName()
. And here we run into another one of those shortcuts that Groovy, as a scripting language, introduces. In Groovy, the notation x.data
is considered a shorthand for x.getData()
when we are trying to fetch data and for x.setData(..)
when we are trying to store data. So the Groovy statement
x.data = y.member;
is actually considered shorthand for
x.setData(y.getMember());
The +
operator is the conventional Java String
concatenation operator.
The ant
tasks library is included within gradle
. So any useful ant
task can be called:
task compile << {
// Compile src/.../*.java into bin/
ant.mkdir (dir: 'bin')
ant.javac (srcdir: 'src/main/java', destdir: 'bin',
debug: 'true', includeantruntime: 'false')
ant.javac (srcdir: 'src/test/java', destdir: 'bin',
debug: 'true', includeantruntime: 'false',
classpath: testCompilePath)
println 'compiled'
}
However, we probably would npot use any of these:
Most Gradle users would use Java constructs for file manipulations, e.g., instead of
ant.mkdir(dir: 'bin')
they would write
file('bin').mkdir();
Gradle provides a Java plugin to handle compilation more easily.
Moreover, if we already had the Ant build file simpleBuild,xml
, we could actually simply import it into a Gradle build:
ant.importBuild 'simpleBuild.xml'
task build (dependsOn: 'deploy') << { // "deploy" target from the Ant build file
println 'Done'
}
Like Maven, Gradle can be used to quickly create new projects with a standard directory/file layout and a standard sequence of build tasks. E.g.,
gradle init --type java-library
sets up a project with src/main/java
and src/test/java
directories.
Using the Java plugin, a basic build with unit tests is easy:
settings.gradle
// A simple Java project
This file needs to exist, but can be empty.
build.gradle
apply plugin: 'java' ➀
repositories { ➁
jcenter()
}
dependencies {
testCompile "junit:junit:4.11"
}
➀ This loads and runs the Java plugin.
As long as our source code is arranged in the Apache/Android directory style, it will be handled correctly.
➁ Everything from hwere on is concerned with configuration management (covered later)
In particular, it notes that we need the JUnit library and tells where to find it as well as any pieces of gradle
itself that need loading or updating.
You can find this entire project, with the Gradle files, here.
This project adds a stage before compilation to generate some of the source code that then needs to be compiled.
The settings.gradle
file is unchanged.
Here is the build.gradle
file:
apply plugin: 'java'
buildscript { ➀
repositories {
jcenter()
maven {
url "http://xbib.org/repository"
}
}
dependencies {
classpath 'org.xbib.gradle.plugin:gradle-plugin-jflex:1.1.0'
}
}
apply plugin: 'org.xbib.gradle.plugin.jflex' ➁
repositories {
jcenter()
}
➀ Again, lots of configuration management info, which we will cover later.
➁ Here we apply the jflex
plugin,
jflex
on code located in src/main/jflex
to generate Java source codeYou can find this entire project, with the Gradle files, here.
A multi-project build in gradle
is kept in a directory tree.
The top-most directory is the master project.
It contains the settings.gradle
file.
Each subproject directory contains its own build.gradle
file.
Any multi-project build in Gradle uses the settings.gradle
file (usually in the common root containing the subproject directories) to identify the subprojects:
rootProject.name = 'manhattan'
include "exe", "lib", "gtest"
In this case, we have three subprojects. One provides the executable, one the library of ADTs, with unit tests, constituting the bulk of the code design, and one a copy of the GoogleTest framework itself.
There is no need for build.gradle
file at this level, although it would be legal to provide one if you have project-level steps to be carried out.
Of the three subprojects, the lib
subproject is probably most interesting because it does the most. it compiles code into a library, then compiles and runs unit tests on that library code. By contrast, the exe
subproject only compiles code, and the gtest
only compiles code into a library.
Here is the build.gradle
file for the lib
subproject:
apply plugin: "cpp"
apply plugin: 'google-test'
model {
components {
main(NativeLibrarySpec)
}
binaries {
withType(GoogleTestTestSuiteBinarySpec) {
lib project: ":gtest", library: "gtest"
if (targetPlatform.operatingSystem.linux) {
cppCompiler.args '-pthread'
linker.args '-pthread'
}
}
}
}
This listing starts off with two plugins, one for C++ compilation and the other for Google Test.
The Gradle approach to dealing with compiled code has been evolving rapidly from one Gradle version to the next. Currently, (version 3.4) it focuses on a “model” of the artifacts or components being built.
In this case, we specify that we are building a component that is a native (object code) library and that we will also generate a “binary” consisting of a Google Test test suite.
* We also give some operating-system dependent
instructions. If we are compiling on Linux, Google
Test needs the `pthread` library linked into the
test driver executable.
The exe
subproject is simpler, because it only has one job to do – create an executable. Here is it
apply plugin: "cpp"
model {
components {
main(NativeExecutableSpec) {
sources {
cpp {
lib project: ':lib', library: 'main'
}
}
}
}
}
Again, we see a “model”, but the component generated this time is a native executable (a machine code program).
In the sources
section, we note a dependency on the library provided by the :lib
subproject.
The gtest
subproject is atypical. That’s because I wanted to drop the code of Google Test directly into the subproject without modifying it, and it has a somewhat quirky arrangement:
The C++ compilation units are in .cc
files instead of .cpp
.
It has a separate directory (include/
) of header (.h
) files that need to be added to the compiler’s search path, and some compilation flags that are required.
Here is build.gradle
file:
apply plugin: "cpp"
model {
components {
main(NativeExecutableSpec) {
sources {
cpp {
lib project: ':lib', library: 'main'
}
}
}
}
}
This is a trfle messier thatn what we have seen before, but still significantly simpler, IMO, than the equivalent make
or Ant
files.
You can find this entire project, with the Gradle files, here.