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.
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
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.
A gradle project is controlled by two build files
settings.gradle
in the project root directory gives project-wide info
build.gradle
, in each subproject directory, desctibes the tasks for that subprojectI’ll work through a progressive series of builds for this project.
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
plugins {
id 'java' ➀
}
repositories { ➁
jcenter()
}
➀ 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.
➁ The repositories
section loads from JCenter
(the default). Even this could probably be omitted in this build.
This is all you need to compile Java code stores in src/main/java
.
plugins {
id 'java'
}
repositories {
jcenter()
}
dependencies {
implementation 'com.opencsv:opencsv:5.1'
implementation 'commons-io:commons-io:2.6'
}
If you have unit tests, we need to add a little more info.
build.gradle
plugins {
id 'java'
}
repositories {
jcenter()
}
dependencies {
implementation 'com.opencsv:opencsv:5.1'
implementation 'commons-io:commons-io:2.6'
testCompile 'org.junit.jupiter:junit-jupiter-api:5.6.1' ➀
testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.6.1' ➁
}
test { ➂
ignoreFailures = true ➃
useJUnitPlatform()
}
The two sections at the end establish that
junit-vintage-engine
available.src/test
directory contains our JUnit tests.The settings.gradle
file is unchanged.
Here is the build.gradle
file:
build.gradle
plugins {
id 'java'
id 'project-report' ➀
id 'checkstyle'
id 'pmd'
}
repositories {
jcenter()
mavenCentral()
}
dependencies {
implementation 'com.opencsv:opencsv:5.1'
implementation 'commons-io:commons-io:2.6'
testCompile 'org.junit.jupiter:junit-jupiter-api:5.6.1'
testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.6.1'
}
test {
ignoreFailures = true
useJUnitPlatform()
}
checkstyle { ➁
ignoreFailures = true
showViolations = false
}
tasks.withType(Checkstyle) {
reports {
html.destination project.file("build/reports/checkstyle/main.html")
}
}
pmd {
ignoreFailures = true
consoleOutput = false
}
checkstyleTest.enabled = false
pmdTest.enabled = false
check.dependsOn htmlDependencyReport
task reports (dependsOn: ['htmlDependencyReport', 'javadoc', 'check', 'site']) { ➂
description 'Generate all reports for this project'
}
➀ Here we introduce reporting tasks for a gradle dependency report, CheckStyle, and PMD.
➁ The rest of this is less critical, but helps to tune the process.
ignoreFailures
is a common setting. Because these tools almost always find problems, we don’t stop the build just because we didn’t get a clean slate.Test.enabled
setting are set to false so that the analysis tools look at src/main/java
but not src/test/java
(the unit tests).
➂ This is a “target of convenience” to make it easy to run all reports at once.
plugins {
id 'java'
id 'project-report'
id 'checkstyle'
id 'pmd'
id 'org.jbake.site' version "5.0.0" ➀
}
repositories {
jcenter()
mavenCentral()
}
dependencies {
implementation 'com.opencsv:opencsv:5.1'
implementation 'commons-io:commons-io:2.6'
testCompile 'org.junit.jupiter:junit-jupiter-api:5.6.1'
testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.6.1'
}
test {
ignoreFailures = true
useJUnitPlatform()
}
checkstyle {
ignoreFailures = true
showViolations = false
}
tasks.withType(Checkstyle) {
reports {
html.destination project.file("build/reports/checkstyle/main.html")
}
}
pmd {
ignoreFailures = true
consoleOutput = false
}
checkstyleTest.enabled = false
pmdTest.enabled = false
check.dependsOn htmlDependencyReport
task reports (dependsOn: ['htmlDependencyReport', 'javadoc', 'check', 'site']) { ➂
description 'Generate all reports for this project'
}
jbake { ➁
srcDirName = "src/main/jbake/"
}
task copyJDocs (type: Copy) {
from 'build/docs'
into 'build/reports'
dependsOn 'javadoc'
}
task copyBake (type: Copy) {
from 'build/jbake'
into 'build/reports'
dependsOn 'bake'
}
task site (dependsOn: ['copyBake', 'copyJDocs']){ ➂
description "Build the project website (in build/reports)"
group "reporting"
}
build/reports
plugins {
id 'java'
id 'project-report'
id 'checkstyle'
id 'pmd'
id 'org.jbake.site' version "5.0.0"
id 'edu.odu.cs.report_accumulator' version '1.3' ➀
}
repositories {
jcenter()
mavenCentral()
}
dependencies {
implementation 'com.opencsv:opencsv:5.1'
implementation 'commons-io:commons-io:2.6'
testCompile 'org.junit.jupiter:junit-jupiter-api:5.6.1'
testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.6.1'
}
test {
ignoreFailures = true
useJUnitPlatform()
}
checkstyle {
ignoreFailures = true
showViolations = false
}
tasks.withType(Checkstyle) {
reports {
html.destination project.file("build/reports/checkstyle/main.html")
}
}
pmd {
ignoreFailures = true
consoleOutput = false
}
checkstyleTest.enabled = false
pmdTest.enabled = false
check.dependsOn htmlDependencyReport
task reports (dependsOn: ['htmlDependencyReport', 'javadoc', 'check', 'site']) { ➂
description 'Generate all reports for this project'
}
jbake {
srcDirName = "src/main/jbake/"
}
task copyJDocs (type: Copy) {
from 'build/docs'
into 'build/reports'
dependsOn 'javadoc'
}
task copyBake (type: Copy) {
from 'build/jbake'
into 'build/reports'
dependsOn 'bake'
}
task site (dependsOn: ['copyBake', 'copyJDocs']){
description "Build the project website (in build/reports)"
group "reporting"
}
reportStats.reportsURL = 'https://www.cs.odu.edu/~zeil/gitlab/' + "bcratchit" + '/reports' ➁
deployReports.deployDestination = 'rsync://zeil@linux.cs.odu.edu:bcratchit/reports/'
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 "application", "geomlib"
In this case, we have two subprojects. One provides the executable, the other a library of ADTs, with unit tests, constituting the bulk of the code design.
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 two subprojects, the geomlib
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 application
subproject only compiles code.
Here is the build.gradle
file for the lib
subproject:
plugins {
id 'cpp-library'
id 'cpp-unit-test'
}
unitTest {
binaries.whenElementKnown(CppTestExecutable) { binary ->
if (binary.targetMachine.operatingSystemFamily.linux) {
binary.linkTask.get().linkerArgs.add('-pthread')
}
}
}
This listing starts off with two plugins, one for C++ compilation (into a reusable library) and the other for unit testing.
The unit test plugin can work with a variety of frameworks. Eventually, you should be able to load these frameworks as dependencies, much as you do in Java projects.
In this case, I am using CppUnitLite because it can be easily dropped into the directory with the unit tests.
Like most unit test frameworks, this one runs the tests in a separate thread (process). The unittest
configuration here adds the required pthread
library when compiling under Linux.
The application
subproject is simpler, because it only has one job to do – create an executable. Here it is:
plugins {
id 'cpp-application'
}
dependencies {
implementation project(':geomlib')
}
cpp-application
plugin and:geomlib
subproject
This dependency guarantees that the geomlib
library will be constructed before this application is built and that it will be automatically included into the application compilation and link steps.
You can find this entire project, with the Gradle files, here.
The gradle build files are Groovy scripts, with Java-like syntax. Many of the more common Java API packages are imported automatically.
task upper {
doLast {
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 {
doLast {
String myName = "Steven Zeil";
println myName;
println myName.toUpperCase();
}
}
Gradle tasks can correspond individual 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.
A Gradle task can be declared as easily as:
task myTask
A Groovy function with parameters, e.g.,
void foo (int x, int y, int z) { ... }
can be called using positional arguments:
foo (12, 14, 0)
or by using named arguments:
foo (z: 0, x: 12, y: 14)
One advantage of the latter form is that you don’t have to know the order in which the parameters appeared in the function declaration. The named form is even more useful when all or most of the parameters have default values and, in your call, you only want to supply the value to one or two parameters for which you don’t like the defaults.
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 doLast
operation.
task copyResources (type: Copy) {
from(file('src/main/resources'))
into(file('target/classes'))
doLast {
println 'Copy has been done.'
}
}
or you can use that operation to add the code to an already-declared task object.
task copyResources (type: Copy) {
from(file('src/main/resources'))
into(file('target/classes'))
}
⋮
copyResources.doLast {
println 'Copy has been done.'
}
task setupFormat
task setupGraphics
task setupSourceCode
task generatePDFs (dependsOn: 'setup')
generatePDFs.doLast { ➀
println 'in task generatePDFs'
}
task setup (dependsOn: ['setupFormat',
'setupGraphics',
'setupSourceCode'])
setup.doLast { ➁
println 'in task setup'
}
task deploy (dependsOn: generatePDFs)
deploy.doLast {
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.