Chapter 2. Gradle Tasks

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.

Declaring a Task

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).

Example 2-1. Declaring a task by name only

task hello

You can see the results of this by running gradle tasks (Example 2-2).

Example 2-2. Gradle’s report of the newly created task
 ------------------------------------------------------------
 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

Task Action

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).

Example 2-3. Giving a task a trivial action to perform

task hello << {
  println 'hello, world'
}

Note

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).

Example 2-4. Appending a task’s actions one at a time

task hello

hello << {
  print 'hello, '
}

hello << {
  println 'world'
}

Now we can recover our familiar build output (Example 2-5).

Example 2-5. The output of the build with actions appended a piece at a time
$ 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.

Task Configuration

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).

Example 2-6. A mix of task configuration and task action definition

task initializeDatabase
initializeDatabase << { println 'connect to database' }
initializeDatabase << { println 'update database schema' }
initializeDatabase { println 'configuring database connection' }

Running this build file, we get what may seem to be a counterintuitive result (Example 2-7).

Example 2-7. The output of the preceding build file
$ gradle -b scratch.gradle initializeDatabase
configuring database connection
:initializeDatabase
connect to database
update database schema
$

Note

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.

Note

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).

Example 2-8. Appending configuration blocks

task initializeDatabase
initializeDatabase << { println 'connect to database' }
initializeDatabase << { println 'update database schema' }
initializeDatabase { print 'configuring ' }
initializeDatabase { println 'database connection' }

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.

Note

All build configuration code runs every time you run a Gradle build file, regardless of whether any given task runs during execution.

Tasks Are Objects

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.

Methods of DefaultTask

dependsOn(task)

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.

Example 2-9. Different ways of calling the dependsOn method

// 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.

Example 2-10. Different ways of calling the dependsOn method for multiple dependencies

// 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 ])

doFirst(closure)

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.

Note

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.

Example 2-11. Calling the doFirst method on the task object

task setupDatabaseTests << {
  // This is the task's existing action
  println 'load test data'
}

setupDatabaseTests.doFirst {
  println 'create schema'
}
Example 2-12. The results of the preceding build file
$ 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).

Example 2-13. Calling the doFirst method inside the task’s configuration block

task setupDatabaseTests << {
  println 'load test data'
}

setupDatabaseTests {
  doFirst {
    println 'create schema'
  }
}

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.

Example 2-14. Repeated calls to doFirst are cumulative

task setupDatabaseTests << {
  println 'load test data'
}

setupDatabaseTests.doFirst {
  println 'create database schema'
}

setupDatabaseTests.doFirst {
  println 'drop database schema'
}
Example 2-15. The output of the preceding example
$ 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.

Example 2-16. Repeated calls to doFirst, refactored

// Initial task definition (maybe not easily editable)
task setupDatabaseTests << {
  println 'load test data'
}

// Our changes to the task (in a place we can edit them)
setupDatabaseTests {
  doFirst {
    println 'create database schema'
  }
  doFirst {
    println 'drop database schema'
  }
}

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.

doLast(closure)

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:

Example 2-17. An example of the doLast method

task setupDatabaseTests << {
  println 'create database schema'
}

setupDatabaseTests.doLast {
  println 'load test data'
}

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).

Example 2-18. Repeated calls to doLast are additive

task setupDatabaseTests << {
  println 'create database schema'
}

setupDatabaseTests.doLast {
  println 'load test data'
}

setupDatabaseTests.doLast {
  println 'update version table'
}

Note

As discussed in Task Action, the << operator is another way of expressing a call to the doLast() method.

onlyIf(closure)

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.

Note

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.

Example 2-19. A build file making use of the onlyIf method.

task createSchema << {
  println 'create database schema'
}

task loadTestData(dependsOn: createSchema) << {
  println 'load test data'
}

loadTestData.onlyIf {
  System.properties['load.data'] == 'true'
}
Example 2-20. Two invocations of the preceding build file. Note differing results.
$ 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.

Properties of DefaultTask

didWork

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.

Example 2-21. Send an email upon successful compilation

apply plugin: 'java'

task emailMe(dependsOn: compileJava) << {
  if(tasks.compileJava.didWork) {
    println 'SEND EMAIL ANNOUNCING SUCCESS'
  }
}
Example 2-22. The results of the didWork build
$ gradle -b didWork.gradle emailMe
SEND EMAIL ANNOUNCING SUCCESS
$

enabled

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.

Example 2-23. Disabling a task

task templates << {
  println 'process email templates'
}

task sendEmails(dependsOn: templates) << {
  println 'send emails'
}

sendEmails.enabled = false
Example 2-24. The build with a task disabled. Note that the dependency still runs.
$ gradle -b enabled.gradle sendEmails
:templates
process email templates
:sendEmails SKIPPED
$

Note

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.

path

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.

Example 2-25. A single-level build file that echoes its only task’s path

task echoMyPath << {
  println "THIS TASK'S PATH IS ${path}"
}
Example 2-26. The results of the previous build file
$ 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.

logger

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.

Example 2-27. A task illustrating the effects of each logging level. This slightly trickier Groovy code sets the log level to each of the valid options, attempting to emit a log message at each log level each time.

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 ' '
  }
}
Example 2-28. The output generated by the preceding build file.
$ 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
$

logging

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.

description

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.

Example 2-29. Setting the description and task behavior all in one

task helloWorld(description: 'Says hello to the world') << {
  println 'hello, world'
}
Example 2-30. The two ways of declaring task behavior and description separately

task helloWorld << {
  println 'hello, world'
}

helloWorld {
  description = 'Says hello to the world'
}

// Another way to do it
helloWorld.description = 'Says hello to the world'

temporaryDir

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.

Dynamic Properties

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 createArtifact 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).

Example 2-31. Build file showing a dynamic task property
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}"
}
Example 2-32. The output of the above build file
$ gradle -b dynamic.gradle createArtifact
 FILES IN MANIFEST: [data.csv, config.json]
$

Task Types

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.

Copy

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.

Example 2-33. A simple example of the copy task
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.

Jar

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.

Example 2-34. A simple example of the Jar task in the jar-task example project
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.

Note

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.

JavaExec

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.

Example 2-35. A Gradle task executing a command-line Java program (from the javaexec-task example)

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
}

Note

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.

Custom Task Types

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.

Custom Tasks Types in the Build File

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:

Example 2-36. A custom task to perform queries against a MySQL database (from the custom-task example)

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.

Custom Tasks in the Source Tree

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.

Example 2-37. A build file using a custom task not defined in the build script

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))'
}
Example 2-38. The definition of the custom task under the buildSrc directory

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.

Note

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.

Example 2-39. The structure of a Gradle project with custom code in the buildSrc directory.
.
├── build.gradle
├── buildSrc
│   └── src
│       └── main
│           └── groovy
│               └── org
│                   └── gradle
│                       └── example
│                           └── task
│                               └── MySqlTask.groovy

Where Do Tasks Come From?

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.

Conclusion

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset