Inside of a Gradle build file, the fundamental unit of build activity is the task. Tasks are named collections of build instructions that Gradle executes as it performs a build. You’ve already seen examples of tasks in Chapter 1, and they may seem like a familiar abstraction compared to other build systems, but Gradle provides a richer model than you may be used to. Rather than bare declarations of build activity tied together with dependencies, Gradle tasks are first-class objects available to you to program if you desire.
Let’s take a look at the different ways of defining a task, the two key aspects of task definitions, and the task API we can use to perform our own customization.
In the introduction, we saw how to create a task and assign it behavior all at the same time. However, there’s an even simpler way to create a task. All you need is a task name (Example 2-1).
You can see the results of this by running gradle tasks
(Example 2-2).
------------------------------------------------------------ Root Project ------------------------------------------------------------ Help tasks ---------- dependencies - Displays the dependencies of root project 'task-lab'. help - Displays a help message projects - Displays the subprojects of root project 'task-lab'. properties - Displays the properties of root project 'task-lab'. tasks - Displays the tasks in root project 'task-lab'. Other tasks ----------- hello
However, executing this task with gradle
hello
will not produce any result, because we haven’t yet
assigned the task an action. Previously, we assigned an action to a task
with the left-shift operator (Example 2-3).
In Groovy, operators like <<
(the “left-shift” operator from
Java) can be overloaded to have different meanings depending upon the
types of the objects they operate on. In this case, Gradle has
overloaded <<
to append a
code block to the list of actions a task performs. This is equivalent
to the doLast() method we’ll cover
later in the chapter.
However, we now have the flexibility of accruing action code in the task by referring to the task object we’ve created (Example 2-4).
Now we can recover our familiar build output (Example 2-5).
$ gradle hello hello, world $
This is again trivial build behavior, but it exposes a powerful insight: tasks are not one-off declarations of build activity, but are first-class objects in the Gradle programming environment. And if we can accrue build actions to them over the course of the build file, there’s probably more we can do. Let’s keep exploring.
New users of Gradle commonly stumble over the configuration syntax when trying to define task actions. Continuing our previous example, we can expand it to include a configuration block (Example 2-6).
Running this build file, we get what may seem to be a counterintuitive result (Example 2-7).
$ gradle -b scratch.gradle initializeDatabase configuring database connection :initializeDatabase connect to database update database schema $
Groovy uses the term “closure” to refer to a block of code between two curly braces. A closure functions like an object that can be passed as a parameter to a method or assigned to a variable, then executed later. You’ll see closures all over in Gradle, since they’re a perfect fit for holding blocks of configuration code and build actions.
If the third closure had been just another snippet of build action, then we’d expect its message to print last, not first. It turns out that the closure added to the task name without the left-shift operator doesn’t create additional task action code at all. Instead, it is a configuration block. The configuration block of a task is run during Gradle’s configuration lifecycle phase, which runs before the execution phase, when task actions are executed.
Every time Gradle executes a build, it runs through three lifecycle phases: initialization, configuration, and execution. Execution is the phase in which build tasks are executed in the order required by their dependency relationships. Configuration is the phase in which those task objects are assembled into an internal object model, usually called the DAG (for directed acyclic graph). Initialization is the phase in which Gradle decides which projects are to participate in the build. The latter phase is important in multiproject builds.
Configuration closures are additive just like action closures, so we could have written the previous build file like so, and we would see the same output (Example 2-8).
The configuration block is the place to set up variables and data structures that will be needed by the task action when (and if) it runs later on in the build. The configuration structure gives you the opportunity to turn your build’s tasks into a rich object model populated with information about the build, rather than a mere set of build actions to be executed in some sequence. Without this distinction between configuration and action, you’d have to build additional complexity into your task dependency relationships, resulting in a more fragile build and a much less expressive means of communicating the build’s essential data structures.
All build configuration code runs every time you run a Gradle build file, regardless of whether any given task runs during execution.
It may have occurred to you by now that Gradle is creating an internal object model of your build before executing it. This is, in fact, explicitly what Gradle is doing. Every task you declare is actually a task object contained within the overall project. A task object has properties and methods just like any other object. We can even control the type of each task object, and access unique, type-specific functionality accordingly. A few examples will help make this clear.
By default, each new task receives the type of DefaultTask
. Like java.lang.Object
in Java code, every Gradle
task descends from this object type—even tasks that extend the DefaultTask
type with a type of their own.
DefaultTasks don’t actually do anything like
compile code or copy files, but they do contain the functionality
required for them to interface with the Gradle project model. Let’s take
a look at the methods and properties each and every Gradle task
has.
Adds a task as a dependency of the calling task. A depended-on
task will always run before the task that depends on it. There are
several ways to invoke this method. If task world
depends on task hello
, we could use the code shown in
Example 2-9.
// Declare that world depends on hello
// Preserves any previously defined dependencies as well
task
loadTestData
{
dependsOn
createSchema
}
// An alternate way to express the same dependency
task
loadTestData
{
dependsOn
<<
createSchema
}
// Do the same using single quotes (which are usually optional)
task
loadTestData
{
dependsOn
'
createSchema
'
}
// Explicitly call the method on the task object
task
loadTestData
loadTestData
.
dependsOn
createSchema
// A shortcut for declaring dependencies
task
loadTestData
(
dependsOn:
createSchema
)
A task can depend on more than one task. If task loadTestData
depends on tasks createSchema
and compileTestClasses
, we could use the code
shown in Example 2-10.
// Declare dependencies one at a time
task
loadTestData
{
dependsOn
<<
compileTestClasses
dependsOn
<<
createSchema
}
// Pass dependencies as a variable-length list
task
world
{
dependsOn
compileTestClasses
,
createSchema
}
// Explicitly call the method on the task object
task
world
world
.
dependsOn
compileTestClasses
,
createSchema
// A shortcut for dependencies only
// Note the Groovy list syntax
task
world
(
dependsOn:
[
compileTestClasses
,
createSchema
])
Adds a block of executable code to the beginning of a task’s
action. During the execution phase, the action block of every
relevant task is executed. The doFirst
method allows you to add a bit of
behavior to the beginning of an existing action, even if that action
is defined by a build file or a plug-in you don’t control. Calling
doFirst
multiple times keeps
appending new blocks of action code to the beginning of the task’s
execution sequence.
You can invoke the doFirst
method directly on the task object, passing a closure to the method.
The closure contains the code to run before the task’s existing
action.
As we’ve already mentioned, a closure is a block of Groovy code inside a pair of curly braces. You can pass a closure around just like any other object. Passing closures to methods is a common Groovy idiom.
$ gradle setupDatabaseTests :setupDatabaseTests create schema load test data $
You can also invoke doFirst
from within the task’s configuration block. Recall that the
configuration block is a piece of executable code that runs before
any task’s action runs, during the configuration phase of the build.
In our earlier discussion of task configuration, you may have been
wondering how you might practically use the configuration block.
This example shows how you can call task methods from inside the
configuration block, which makes a potentially very expressive
format for modifying task behavior (Example 2-13).
Repeated calls to the doFirst
method are additive. Each previous
call’s action code is retained, and the new closure is appended to
the start of the list to be executed in order. If we had to set up a
database for integration testing (and wanted to do it a piece at a
time), we might use the code shown in Example 2-14.
$ gradle world :setupDatabaseTests drop database schema create database schema load test data $
Of course, it’s somewhat contrived to break one initialization
sequence into three separate closures and calls to doFirst()
, as we’ve done here. However,
sometimes the initial definition of a task isn’t immediately
available to change as you see fit—for example, in cases in which
the task is defined in another build file that is impossible or
impractical for you to modify. This kind of programmatic
modification of that otherwise inaccessible build logic can be very
powerful.
So far, our examples have used a very simple syntax, which
makes the mechanics of Gradle more obvious, though at the expense of
a lot of repetition. In a real-world build (still relying on
println
statements in place of
actual testing actions), we would be more likely to structure the
task as done in Example 2-16.
Note that we gather together the multiple calls to doFirst
inside a single configuration
block, and this occurs after the initial action is added to the
world task.
The doLast
method is very
similar to the doFirst()
method, except that it
appends behavior to the end of an action, rather than before it. If
there was a block of code you wanted to run after an existing task
was done executing, you might do as shown in Example 2-17:
Just like doFirst
, repeated
calls to doLast
are additive.
Each succeeding call appends its closure to the end of the list to
be executed in order (Example 2-18).
As discussed in Task Action, the <<
operator is another way of
expressing a call to the doLast()
method.
The onlyIf
method allows
you to express a predicate which determines whether a task should be
executed. The value of the predicate is the value returned by the
closure. Using this method, you can disable the execution of a task
which might otherwise run as a normal part of the build’s dependency
chain.
In Groovy, the last statement of a closure is the closure’s
return value, even if no return
statement is given. A Groovy method containing a single expression
is a function that returns the value of that expression.
$ build loadTestData create database schema :loadTestData SKIPPED $ gradle -Dload.data=true loadTestData :createSchema create database schema :loadTestData load test data $
Using the onlyIf
method,
you can switch individual tasks on and off using any logic you can
express in Groovy code, not just the simple System property tests
we’ve used here. You can read files, call web services, check
security credentials, or just about anything else.
A boolean property indicating whether the task completed
successfully. Not all tasks may set didWork
upon completion, but some built-in
tasks like Compile
, Copy
, and Delete
do set it to reflect the success or
failure of their actions. The evaluation of a task having worked is
task-specific. For example, the current implementation of the
JavaCompiler
returns didWork
of true if at least one file
successfully compiled. You are able to set the didWork
property in your own task actions
to reflect the results of build code you write.
A boolean property indicating whether the task will execute.
You can set any task’s enabled
property to false to cause it not to run. Its dependencies will
still execute the way they would if the task were enabled.
$ gradle -b enabled.gradle sendEmails :templates process email templates :sendEmails SKIPPED $
The -b
command line
switch points Gradle to a nondefault build file. By default, it
looks for a file called build.gradle
, but this switch lets us
point it at a different file.
A string property containing the fully qualified path of a task. By default, a task’s path is simply the name of the task with a leading colon. The following build file illustrates this.
$ gradle -b path.gradle echoMyPath THIS TASK'S PATH IS :echoMyPath $
The leading colon indicates that the task is located in the
top-level build file. However, for a given build, not all tasks must
be present in the top-level build file, since Gradle supports
dependent subprojects, or nested builds. If the task existed in a
nested build called subProject
,
then the path would be :subProject:echoMyPath
. For more details
on nested builds, see Chapter 6.
A reference to the internal Gradle logger
object. The Gradle logger
implements the org.slf4j.Logger
interface, but with a few extra logging levels added. The logging
levels supported by the logger
are as follows. Setting the log level to one enables log output from
all succeeding log levels, with the exception of WARN
and QUIET
as noted:
DEBUG
. For high-volume
logging messages which are of interest to the build developer,
but should be suppressed during normal build execution. When
this log level is selected, Gradle automatically provides a
richer log formatter, including the timestamp, log level, and
logger name of each message. All other log levels emit only the
undecorated log message.
INFO
. For lower-volume
informative build messages which may be of optional interest during build
execution.
LIFECYCLE
. Low-volume
messages, usually from Gradle itself, about changes in the build
lifecycle and the execution of the build tool. When Gradle is
executed without the -q
command line option, this is
the default logging level. Calls to the println
method emit log statements at
this level.
WARN
. Low-volume but
important messages, alerting the executor of the build of
potential problems. When the log level is set to WARN
, QUIET
-level messages are not
emitted.
QUIET
. Messages which
should appear even if the quiet
switch was specified on the Gradle command line.
(Executing Gradle with the -q
command line option causes
this to be the default log level.) System.out.println
is directed to the
logger at this log level. When the log level is set to QUIET
, WARN
-level messages are not
emitted.
ERROR
. Rare but
critically important log messages which should be emitted in all
cases. Intended to communicate build failures. If the log level
is set to ERROR
, calls to
System.out.println
will not
show up in the console.
task
logLevel
<<
{
def
levels
=
[
'
DEBUG
'
,
'
INFO
'
,
'
LIFECYCLE
'
,
'
QUIET
'
,
'
WARN
'
,
'
ERROR
'
]
levels
.
each
{
level
->
logging
.
level
=
level
def
logMessage
=
"SETTING LogLevel=${level}"
logger
.
error
logMessage
logger
.
error
'-'
*
logMessage
.
size
()
logger
.
debug
'
DEBUG
ENABLED
'
logger
.
info
'
INFO
ENABLED
'
logger
.
lifecycle
'
LIFECYCLE
ENABLED
'
logger
.
warn
'
WARN
ENABLED
'
logger
.
quiet
'
QUIET
ENABLED
'
logger
.
error
'
ERROR
ENABLED
'
println
'
THIS
IS
println
OUTPUT
'
logger
.
error
' '
}
}
$ gradle -b logging.gradle logLevel 16:02:34.883 [ERROR] [org.gradle.api.Task] SETTING LogLevel=DEBUG 16:02:34.902 [ERROR] [org.gradle.api.Task] ---------------------- 16:02:34.903 [DEBUG] [org.gradle.api.Task] DEBUG ENABLED 16:02:34.903 [INFO] [org.gradle.api.Task] INFO ENABLED 16:02:34.904 [LIFECYCLE] [org.gradle.api.Task] LIFECYCLE ENABLED 16:02:34.904 [WARN] [org.gradle.api.Task] WARN ENABLED 16:02:34.905 [QUIET] [org.gradle.api.Task] QUIET ENABLED 16:02:34.905 [ERROR] [org.gradle.api.Task] ERROR ENABLED 16:02:34.906 [ERROR] [org.gradle.api.Task] SETTING LogLevel=INFO --------------------- INFO ENABLED LIFECYCLE ENABLED WARN ENABLED QUIET ENABLED ERROR ENABLED SETTING LogLevel=LIFECYCLE -------------------------- LIFECYCLE ENABLED WARN ENABLED QUIET ENABLED ERROR ENABLED SETTING LogLevel=QUIET ---------------------- QUIET ENABLED ERROR ENABLED SETTING LogLevel=WARN --------------------- WARN ENABLED ERROR ENABLED SETTING LogLevel=ERROR ---------------------- ERROR ENABLED $
The logging
property gives
us access to the log level. As illustrated in the discussion of the
logger property, the logging.level
property can be read and
written to change the logging level in use by the build.
The description
property is
just what it sounds like: a small piece of human-readable metadata
to document the purpose of a task. There are several ways of setting
a description, as shown in
Example 2-29 and Example 2-30.
The temporaryDir
property
returns a File
object pointing to
a temporary directory belonging to this build file. This
directory is generally available to a task needing a temporary place
in which to store intermediate results of any work, or to stage
files for processing inside the task.
As we’ve seen, tasks come with a set of intrinsic properties which are indispensable to the Gradle user. However, we can also assign any other properties we want to a task. A task object functions like a hash map, able to contain whatever other arbitrary property names and values we care to assign to it (as long as the names don’t collide with the built-in property names).
Leaving our familiar “hello, world” example, let’s suppose we
had a task called create
Artifact
that depended on a task called
copyFiles
. The job of the copyFiles
is to collect files from several
sources and copy them into a staging directory, which the createArtifact
task will later assemble into
a deployment artifact. The list of files may change depending on the
parameters of the build, but the artifact must contain a manifest
listing them, to satisfy some requirement of the deployed application.
This is a perfect occasion to use a dynamic property (Example 2-31 and Example 2-32).
task
copyFiles
{
// Find files from wherever, copy them
// (then hardcode a list of files for illustration)
fileManifest
=
[
'
data
.
csv
'
,
'
config
.
json
'
]
}
task
createArtifact
(
dependsOn:
copyFiles
)
<<
{
println
"FILES IN MANIFEST: ${copyFiles.fileManifest}"
}
As we discussed in Tasks Are Objects, every task
has a type. Besides the DefaultTask
,
there are task types for copying, archiving, executing programs, and
many more. Declaring a task type is a lot like extending a base class in
an object-oriented programming language: you can get certain methods and
properties available in your task for free. This makes for very concise
task definitions that can accomplish a lot.
A complete task reference is beyond the scope of this volume, but here are a few important types with an example of how to use each.
A copy task copies files from one place into another (Example 2-33). In its most basic form, it copies files from one directory into another, with optional restrictions on which file patterns are included or excluded.
task
copyFiles
(
type:
Copy
)
{
from
'
resources
'
into
'
target
'
include
'
**
/*.xml', '**/
*.
txt
'
,
'
**/*.
properties
'
}
The copy task will create the destination directory if it
doesn’t already exist. In this case, the copyFiles task will copy any
files with the .xml, .properties, or .txt extensions from the
resources directory to the target directory. Note that the from
, into
, and include
methods are inherited from the
Copy
.
A Jar task creates a Jar file from source files (Example 2-34). The Java plug-in creates a task of this
type, called unsurprisingly jar
. It
packages the main source set and resources together with a trivial
manifest into a Jar bearing the project’s name in the build/libs
directory. The task is highly
customizable.
apply
plugin:
'
java
'
task
customJar
(
type:
Jar
)
{
manifest
{
attributes
firstKey:
'
firstValue
'
,
secondKey:
'
secondValue
'
}
archiveName
=
'
hello
.
jar
'
destinationDir
=
file
(
"${buildDir}/jars"
)
from
sourceSets
.
main
.
classes
}
Note that the archive name and destination directory are easily
configurable. Likewise, the manifest can be populated with custom
attributes using a readable Groovy map syntax. The contents of the JAR
are identified by the from
sourceSets.main.classes
line, which specifies that the
compiled .class files of the main Java sources are to be included. The
from
method is identical to the one
used in the CopyTask
example, which
reveals an interesting insight: the Jar task extends the Copy task.
Even before we’ve seen exhaustive documentation of the Gradle object
model and DSL, these details hint at the richness and order of the
underlying structure.
The expression being assigned to destinationDir
is worth noting. It would be
natural just to assign a string to destinationDir
, but the property expects an
argument compatible with java.io.File
. The file()
method, which is always available
inside a Gradle build file, converts the string to a File
object.
Remember, you can always open the docs/dsl/index.html
file for documentation
on standard Gradle features like the Jar task. Complete
documentation of the Jar task and its companion tasks is beyond the
scope of this book.
A JavaExec task runs a Java class with a main() method. Command-line Java can be a hassle, but this task tries to take the hassle away and integrate command-line Java invocations into your build.
apply
plugin:
'
java
'
repositories
{
mavenCentral
()
}
dependencies
{
runtime
'
commons
-
codec:
commons
-
codec:
1.5
'
}
task
encode
(
type:
JavaExec
,
dependsOn:
classes
)
{
main
=
'
org
.
gradle
.
example
.
commandline
.
MetaphoneEncoder
'
args
=
"The rain in Spain falls mainly in the plain"
.
split
().
toList
()
classpath
sourceSets
.
main
.
classesDir
classpath
configurations
.
runtime
}
The classpath property in the encode
task is set to something called
configuration.runtime
. A
configuration is a collection of dependencies that have something in
common. In this case, the runtime
configuration holds all dependencies which must be available to the
program at runtime. This is in contrast to dependencies which are
needed only during compilation, or only while tests are running, or
which are needed at compile time and runtime, but which are provided
by a runtime environment like an application server. The configuration
property in Gradle is a
collection of all configurations defined by the build, each of which
is a collection of actual dependencies.
This build file declares an external dependency: the Apache
Commons Codec library. Normally, we’d have to compile our Java file,
then concoct a java
command line
including the path to the
compiled class files and the JAR dependency. In this build file,
however, we simply identify the main class to be run (org.gradle.example.commandline.MetaphoneEncoder
),
provide it with some command line arguments in the form of a List
, and point it at the Gradle classpath
elements it needs. In this case, we may symbolically refer to the
classes of the main sourceSet and all of the dependencies declared in the compile
configuration. If we had a complex
set of dozens of dependencies
from multiple repositories—including even some statically managed
dependencies in the project
directory—this simple task would still work.
There will be occasions in which Gradle’s built-in task types will not quite do the job, and instead, the most expressive way to develop your build will be to create a custom task. Gradle has several ways of doing this. We will discuss the two most common ways here.
Suppose your build file needs to issue arbitrary queries against a MySQL database. There are several ways to accomplish this in Gradle, but you decide a custom task is the most expressive way to do it. The easiest way to introduce the task is simply to create it in your build script as shown here:
task
createDatabase
(
type:
MySqlTask
)
{
sql
=
'
CREATE
DATABASE
IF
NOT
EXISTS
example
'
}
task
createUser
(
type:
MySqlTask
,
dependsOn:
createDatabase
)
{
sql
=
"GRANT ALL PRIVILEGES ON example.*
TO exampleuser@localhost IDENTIFIED BY 'passw0rd'"
}
task
createTable
(
type:
MySqlTask
,
dependsOn:
createUser
)
{
username
=
'
exampleuser
'
password
=
'
passw0rd
'
database
=
'
example
'
sql
=
'
CREATE
TABLE
IF
NOT
EXISTS
users
(
id
BIGINT
PRIMARY
KEY
,
username
VARCHAR
(
100
))
'
}
class
MySqlTask
extends
DefaultTask
{
def
hostname
=
'
localhost
'
def
port
=
3306
def
sql
def
database
def
username
=
'
root
'
def
password
=
'
password
'
@TaskAction
def
runQuery
()
{
def
cmd
if
(
database
)
{
cmd
=
"mysql -u ${username} -p${password} -h ${hostname}
-P ${port} ${database} -e "
}
else
{
cmd
=
"mysql -u ${username} -p${password} -h ${hostname} -P ${port} -e "
}
project
.
exec
{
commandLine
=
cmd
.
split
().
toList
()
+
sql
}
}
}
The custom task, MySqlTask
,
extends the DefaultTask
class. All
custom tasks must extend this
class or one of its descendants. (A custom task can extend any task
types other than DefaultTask
. See
Task Types for a description of the
most important built-in task types.) The task declares properties
(i.e., hostname
, database
, sql
, etc.) in conventional Groovy idiom. It then
declares a single method, runQuery()
, which is annotated with the
@TaskAction
annotation. This method
will run when the task runs.
The actual build tasks at the top of the build file all declare
themselves to be of the MySqlTask
type. By doing this, they automatically inherit the properties and
action of that task class. Because most of the properties have
defaults (some of which, like username and password, are obviously
specific to the build), each invocation of the task has very little to
configure. The createDatabase
and
createUser
tasks are able to
configure just a single SQL query, and allow the defaults to take over
from there.
The createTable
task
overrides the username
, password
, and database
properties, since its task
dependencies have created a new database and username separate from
the default administrative settings. The pattern of providing a useful
default configuration which can be overridden when necessary is a
recurring theme in Gradle.
Significant custom task logic will not fit well into a build
file. A few simple lines of scripting can pragmatically be inserted
into a short task, as in the custom-task
example. However, at some point,
a sophisticated task will develop a class hierarchy of its own, might
develop a reliance on external APIs, and will need automated testing.
The build is code, and complex build code should be treated as a
first-class citizen of the development world. Gradle makes this
easy.
When custom task logic outgrows the build file, we can migrate
it to the buildSrc
directory at the
project root. This directory is automatically compiled and added to
the build classpath. Here is how we would alter the previous example
to use the buildSrc
directory.
task
createDatabase
(
type:
MySqlTask
)
{
sql
=
'
CREATE
DATABASE
IF
NOT
EXISTS
example
'
}
task
createUser
(
type:
MySqlTask
,
dependsOn:
createDatabase
)
{
sql
=
"GRANT ALL PRIVILEGES ON example.*
TO exampleuser@localhost IDENTIFIED BY 'passw0rd'"
}
task
createTable
(
type:
MySqlTask
,
dependsOn:
createUser
)
{
username
=
'
exampleuser
'
password
=
'
passw0rd
'
database
=
'
example
'
sql
=
'
CREATE
TABLE
IF
NOT
EXISTS
users
(
id
BIGINT
PRIMARY
KEY
,
username
VARCHAR
(
100
))
'
}
import
org.gradle.api.DefaultTask
import
org.gradle.api.tasks.TaskAction
class
MySqlTask
extends
DefaultTask
{
def
hostname
=
'
localhost
'
def
port
=
3306
def
sql
def
database
def
username
=
'
root
'
def
password
=
'
password
'
@TaskAction
def
runQuery
()
{
def
cmd
if
(
database
)
{
cmd
=
"mysql -u ${username} -p${password} -h ${hostname}
-P ${port} ${database} -e "
}
else
{
cmd
=
"mysql -u ${username} -p${password} -h ${hostname} -P ${port} -e "
}
project
.
exec
{
commandLine
=
cmd
.
split
().
toList
()
+
sql
}
}
}
Note that the task definition in the buildSrc
directory is very similar to the
code included in the build script in the previous example. However, we
now have a robust platform for elaborating on that simple task
behavior, growing an object model, writing tests, and doing everything
else we normally do when developing software.
You have four options for where to put your custom Gradle
build code. The first is in the build script itself, in a task
action block. The second is the buildSrc
directory as we’ve shown here.
The third is in a separate build script file imported into the main
build script. The fourth is in a custom plug-in written in Java or
Groovy. Programming Gradle with custom plug-ins will be the topic of
a separate volume.
So far, we’ve been creating tasks by coding them directly, either
inside Gradle build scripts or in the buildSrc
directory as Groovy code. This is a
great way to learn about tasks, because it’s easy to see all of the
moving parts in great detail. However, many of the tasks you use won’t
be tasks you write; they’ll come from plug-ins.
You’ve already seen an example of this in the section on building Java code. By applying the
Java plug-in, the build script automatically inherits a set of tasks
whose code you never directly see. You can modify the behavior of these
tasks using the task
configuration, doFirst()
, and doLast()
methods we’ve covered in this chapter, but you don’t have to code them
youself. The fact that Gradle is providing you with rich, extensible
task functionality whose code you never have to look at—code you invoke
through the Gradle DSL, not through lots of literal Groovy code—is core
to Gradle’s strategy of provding high extensibility with low
complexity.
Gradle also has a few built-in tasks, like tasks
and properties
. These aren’t provided by any
plug-in or any imperative code you write, but are just a standard part
of the Gradle DSL. They are covered in the section on the Gradle command line.
We’ve had a pretty thorough look at tasks in this chapter. We’ve looked at how to configure them and how to script them, and gotten an idea of how Gradle divides up the work of configuration and execution between two lifecycle phases. We’ve seen that tasks are first-class Groovy objects with a rich API of their own. We’ve explored that API just enough to show you how to think about tasks as programmable entities. We also looked at some standard class types that provide real functionality out of the box.
Finally we looked at how to write tasks of your own. Gradle’s built-in tasks and plug-ins are enough for many users to script their builds without any custom code, but not always. One of Gradle’s fundamental sensibilities is that it should be easy for you to extend your build without cluttering your build scripts with a lot of unmaintainable Groovy code. The custom task examples we looked at illustrated this.
Tasks are the basic unit of build activity in Gradle. There is more to their story than an introduction can cover, but with this chapter under your belt, you’re well prepared to start using them and to continue learning about them.