Chapter 4. Build script essentials

This chapter covers

  • Gradle’s building blocks and their API representation
  • Declaring new tasks and manipulating existing tasks
  • Advanced task techniques
  • Implementing and using task types
  • Hooking into the build lifecycle

In chapter 3, you implemented a full-fledged Java web application from the ground up and built it with the help of Gradle’s core plugins. You learned that the default conventions introduced by those plugins are customizable and can easily adapt to nonstandard build requirements. Preconfigured tasks function as key components of a plugin by adding executable build logic to your project.

In this chapter, we’ll explore the basic building blocks of a Gradle build, namely projects and tasks, and how they map to the classes in the Gradle API. Properties are exposed by methods of these classes and help to control the build. You’ll also learn how to control the build’s behavior through properties, as well as the benefits of structuring your build logic.

At the core of this chapter, you’ll experience the nitty-gritty details of working with tasks by implementing a consistent example. Step by step, you’ll build your knowledge from declaring simple tasks to writing custom task classes. Along the way, we’ll touch on topics like accessing task properties, defining explicit and implicit task dependencies, adding incremental build support, and using Gradle’s built-in task types.

We’ll also look at Gradle’s build lifecycle to get a good understanding of how a build is configured and executed. Your build script can respond to notifications as the build progresses through the lifecycle phases. In the last part of this chapter, we’ll show how to write lifecycle hooks as closure and listener implementations.

4.1. Building blocks

Every Gradle build consists of three basic building blocks: projects, tasks, and properties. Each build contains at least one project, which in turn contains one or more tasks. Projects and tasks expose properties that can be used to control the build. Figure 4.1 illustrates the dependencies among Gradle’s core components.

Figure 4.1. Two basic concepts of a Gradle build are projects and tasks. A project can depend on other projects in the context of a multiproject build. Similarly, tasks can form a dependency graph that guarantees their execution order.

Gradle applies the principles of domain-driven design (DDD) to model its own domain-building software. As a consequence, projects and tasks have a direct class representation in Gradle’s API. Let’s take a closer look at each component and its API counterpart.

4.1.1. Projects

In Gradle’s terminology a project represents a component you’re trying to build (for example, a JAR file), or a goal you’re trying to achieve, like deploying an application. If you’re coming from Maven, this concept should sound pretty familiar. Gradle’s equivalent to Maven’s pom.xml is the build.gradle file. Each Gradle build script defines at least one project. When starting the build process, Gradle instantiates the class org.gradle.api.Project based on your configuration in build.gradle and makes it implicitly available through the project variable. Figure 4.2 shows the API interface and its most important methods.

Figure 4.2. Main entry point of a Gradle build—the Project interface

A project can create new tasks, add dependencies and configurations, and apply plug-ins and other build scripts. Many of its properties, like name and description, are accessible via getter and setter methods.

So why are we talking about Gradle’s API early on? You’ll find that after getting to know Gradle’s basics, you’ll want to go further and apply the concepts to your real-world projects. The API is key to getting the most out of Gradle.

The Project instance gives you programmatic access to all Gradle features in your build, like task creation and dependency management. You’ll use many of these features throughout the book by invoking their corresponding API methods. Keep in mind that you’re not required to use the project variable when accessing properties and methods of your project—it’s assumed you mean the Project instance. The following code snippet illustrates valid method invocations on the Project instance:

In the previous chapters, you only had to deal with single-project builds. Gradle provides support for multiproject builds as well. One of the most important principles of software development is separation of concerns. The more complex a software system becomes, the more you want to decompose it into modularized functionality, in which modules can depend on each other. Each of the decomposed parts would be represented as a Gradle project with its own build.gradle script. For the sake of simplicity, we won’t go into details here. If you’re eager to learn more, feel free to jump to chapter 6, which is fully devoted to creating multiproject builds in Gradle. Next, we’ll look at the characteristics of tasks, another one of Gradle’s core building blocks.

4.1.2. Tasks

You already created some simple tasks in chapter 2. Even though the use cases I presented were trivial, you got to know some important capabilities of a task: task actions and task dependencies. An action defines an atomic unit of work that’s executed when the task is run. This can be as simple as printing out text like “Hello world!” or as complex as compiling Java source code, as seen in chapter 2. Many times a task requires another task to run first. This is especially true if the task depends on the produced output of another task as input to complete its own actions. For example, you’ve seen that you need to compile Java sources first before they can be packaged into a JAR file. Let’s look at Gradle’s API representation of a task, the interface org.gradle.api.Task, as shown in figure 4.3.

Figure 4.3. Task interface in Gradle’s API. Tasks can define dependencies on other tasks, a sequence of actions, and conditional execution.

The Task interface provides even more methods than are shown in the figure. You’ll use them one by one as you apply them to concrete examples throughout the book. Now that we’ve discussed projects and tasks, let’s look at different types of properties.

4.1.3. Properties

Each instance of Project and Task provides properties that are accessible through getter and setter methods. A property could be a task’s description or the project’s version. Later in this chapter, you’ll read and modify these values in the context of a practical example. Often, you’ll want to define your own properties. For example, you may want to declare a variable that references a file that’s used multiple times within the same build script. Gradle allows defining user-defined variables through extra properties.

Extra properties

Many of Gradle’s domain model classes provide support for ad-hoc properties. Internally, these properties are stored as key-value pairs in a map. To add properties, you’re required to use the ext namespace. Let’s look at a concrete example. The following code snippet demonstrates that a property can be added, read, and modified in many different ways:

Similarly, additional properties can be fed through a properties file.

Gradle properties

Properties can be directly injected into your project by declaring them in a properties file named under the directory <USER_HOME>/.gradle or a project’s root directory. They can be accessed via the project instance. Bear in mind that there can only be one Gradle property file per user under <USER_HOME>/.gradle, even if you’re dealing with multiple projects. This is currently a limitation of Gradle. Any property declared in the properties file will be available to all of your projects. Let’s assume the following properties are declared in your file:

exampleProp = myValue
someOtherProp = 455

You can access both variables in your project as follows:

assert project.exampleProp == 'myValue'

task printGradleProperty << {
   println "Second property: $someOtherProp"
Other ways to declare properties

Extra properties and Gradle properties are the mechanisms you’ll probably use the most to declare custom variables and their values. Gradle offers many other ways to provide properties to your build, such as

  • Project property via the –P command-line option
  • System property via the –D command-line option
  • Environment property following the pattern

I won’t show you concrete examples for these alternative ways of declaring properties, but you can use them if needed. The online Gradle user guide provides excellent usage examples if you want to go further. For the rest of this chapter, you’ll make extensive use of tasks and Gradle’s build lifecycle.

4.2. Working with tasks

By default, every newly created task is of type org.gradle.api.DefaultTask, the standard implementation of org.gradle.api.Task. All fields in class DefaultTask are marked private. This means that they can only be accessed through their public getter and setter methods. Thankfully, Groovy provides you with some syntactic sugar, which allows you to use fields by their name. Under the hood, Groovy calls the method for you. In this section, we’ll explore the most important features of a task by example.

4.2.1. Managing the project version

To demonstrate properties and methods of the class DefaultTask in action, I’m going to explain them in the context of the To Do application from chapter 3. Now that you have the general build infrastructure in place, features can easily be added. Often, feature sets are grouped into releases. To identify each release, a unique version number is added to the deliverable.

Many enterprises or open source projects have their own versioning strategy. Think back to some of the projects you’ve worked on. Usually, you assign a specific version numbering scheme (for example, a major and minor version number separated by a dot, like 1.2). You may also encounter a project version that appends a SNAPSHOT designator to indicate that the built project artifact is in the state of development. You’ve already assigned a version to your project in chapter 3 by setting a string value to the project property version. Using a String data type works great for simple use cases, but what if you want to know the exact minor version of your project? You’ll have to parse the string value, search for the dot character, and filter out the substring that identifies the minor version. Wouldn’t it be easier to represent the version by an actual class?

You could easily use the class’s fields to set, retrieve, and modify specific portions of your numbering scheme. You can go even further. By externalizing the version information to persistent data storage, such as a file or database, you’ll avoid having to modify the build script itself to change the project version. Figure 4.4 illustrates the interaction among the build script, a properties file that holds the version information, and the data representation class. You’ll create and learn how to use all of these files in the upcoming sections.

Figure 4.4. The project version is read from a properties file during runtime of the build script. The ProjectVersion data class is instantiated. Each of the version classifiers is translated into a field value of the data class. The instance of ProjectVersion is assigned to the version property of the project.

Being able to control the versioning scheme programmatically will become a necessity the more you want to automate your project lifecycle. Here’s one example: your code has passed all functional tests and is ready to be shipped. The current version of your project is 1.2-SNAPSHOT. Before building the final WAR file, you’ll want to make it a release version 1.2 and automatically deploy it to the production server. Each of these steps can be modeled by creating a task: one for modifying the project version and one for deploying the WAR file. Let’s take your knowledge about tasks to the next level by implementing flexible version management in your project.

4.2.2. Declaring task actions

An action is the appropriate place within a task to put your build logic. The Task interface provides you with two relevant methods to declare a task action: doFirst(Closure) and doLast(Closure). When a task is executed, the action logic defined as closure parameter is executed in turn.

You’re going to start easy by adding a single task named printVersion. The task’s purpose is to print out the current project version. Define this logic as the last action of this task, as shown in the following code snippet:

version = '0.1-SNAPSHOT'

task printVersion {
   doLast {
      println "Version: $version"

In chapter 2, I explained that the left shift operator (<<) is the shortcut version of the method doLast. Just to clarify: they do exactly the same thing. When executing the task with gradle printVersion, you should see the correct version number:

$ gradle printVersion
Version: 0.1-SNAPSHOT

The same result could be achieved as the first action of the task by using the doFirst method instead:

task printVersion {
   doFirst {
      println "Version: $version"
Adding actions to existing tasks

So far, you’ve only added a single action to the task printVersion, either as the first or last action. But you’re not limited to a single action per task. In fact, you can add as many actions as you need even after the task has been created. Internally, every task keeps a list of task actions. At runtime, they’re executed sequentially. Let’s look at a modified version of your example task:

As shown in the listing, an existing task can be manipulated by adding actions to them. This is especially useful if you want to execute custom logic for tasks that you didn’t write yourself. For example, you could add a doFirst action to the compileJava task of the Java plugin that checks if the project contains at least one Java source file.

4.2.3. Accessing DefaultTask properties

Next you’ll improve the way you output the version number. Gradle provides a logger implementation based on the logging library SLF4J. Apart from implementing the usual range of logging levels (DEBUG, ERROR, INFO, TRACE, WARN), it adds some extra levels. The logger instance can be directly accessed through one of the task’s methods. For now, you’re going to print the version number with the log level QUIET:

task printVersion << {
   logger.quiet "Version: $version"

See how easy it is to access one of the task properties? There are two more properties I want to show you: group and description. Both act as part of the task documentation. The description property represents a short definition of the task’s purpose, whereas the group defines a logic grouping of tasks. You’ll set values for both properties as arguments when creating the task:

task printVersion(group: 'versioning',
                   description: 'Prints project version.') << {
   logger.quiet "Version: $version"

Alternatively, you can also set the properties by calling the setter methods, as shown in the following code snippet:

task printVersion {
   group = 'versioning'
   description = 'Prints project version.'
   doLast {
      logger.quiet "Version: $version"

When running gradle tasks, you’ll see that the task shows up in the correct task bucket and is able to describe itself:

gradle tasks

Versioning tasks
printVersion - Prints project version.


Even though setting a task’s description and grouping is optional, it’s always a good idea to assign values for all of your tasks. It’ll make it easier for the end user to identify the task’s function. Next, we’ll review the intricacies of defining dependencies between tasks.

4.2.4. Defining task dependencies

The method dependsOn allows for declaring a dependency on one or more tasks. You’ve seen that the Java plugin makes extensive use of this concept by creating task graphs to model full task lifecycles like the build task. The following listing shows different ways of applying task dependencies using the dependsOn method.

Listing 4.1. Applying task dependencies

You’ll execute the task dependency chain by invoking the task third from the command line:

$ gradle -q third
Version: 0.1-SNAPSHOT

If you take a close look at the task execution order, you may be surprised by the outcome. The task printVersion declares a dependency on the tasks second and first. Wouldn’t you have expected that the task second would get executed before first? In Gradle, the task execution order is not deterministic.

Task dependency execution order

It’s important to understand that Gradle doesn’t guarantee the order in which the dependencies of a task are executed. The method call dependsOn only defines that the dependent tasks need to be executed beforehand. Gradle’s philosophy is to declare what should be executed before a given task, not how it should be executed. This concept is especially hard to grasp if you’re coming from a build tool that defines its dependencies imperatively, like Ant does. In Gradle, the execution order is automatically determined by the input/output specification of a task, as you’ll see later in this chapter. This architectural design decision has many benefits. On the one hand, you don’t need to know the whole chain of task dependencies to make a change, which improves code maintainability and avoids potential breakage. On the other hand, because your build doesn’t have to be executed strictly sequentially, it’s been enabled for parallel task execution, which can significantly improve your build execution time.

4.2.5. Finalizer tasks

In practice, you may find yourself in situations that require a certain resource to be cleaned up after a task that depends on it is executed. A typical use case for such a resource is a web container needed to run integration tests against a deployed application. Gradle’s answer to such a scenario is finalizer tasks, which are regular Gradle tasks scheduled to run even if the finalized task fails. The following code snippet demonstrates how to use a specific finalizer task using the Task method finalizedBy:

You’ll find executing the task first will automatically trigger the task named second:

$ gradle -q first

Chapter 7 covers the concept of finalizer tasks in more depth with the help of a real-world example. In the next section, you’ll write a Groovy class to allow for finer-grained control of the versioning scheme.

4.2.6. Adding arbitrary code

It’s time to come back to my statement about Gradle’s ability to define general-purpose Groovy code within a build script. In practice, you can write classes and methods the way you’re used to in Groovy scripts or classes. In this section, you’ll create a class representation of the version. In Java, classes that follow the bean conventions are called plain-old Java objects (POJOs). By definition, they expose their fields through getter and setter methods. Over time it can become very tiresome to write these methods by hand. POGOs, Groovy’s equivalent to POJOs, only require you to declare properties without an access modifier. Their getter and setter methods are intrinsically added at the time of bytecode generation and therefore are available at runtime. In the next listing, you assign an instance of the POGO ProjectVersion. The actual values are set in the constructor.

Listing 4.2. Representing the project version by a POGO

When running the modified build script, you should see that the task printVersion produces exactly the same result as before. Unfortunately, you still have to manually edit the build script to change the version classifiers. Next, you’ll externalize the version to a file and configure your build script to read it.

4.2.7. Understanding task configuration

Before you get started writing code, you’ll need to create a properties file named alongside the build script. For each of the version categories like major and minor, you’ll create an individual property. The following key–value pairs represent the initial version 0.1-SNAPSHOT:

major = 0
minor = 1
release = false
Adding a task configuration block

Listing 4.3 declares a task named loadVersion to read the version classifiers from the properties file and assign the newly created instance of ProjectVersion to the project’s version field. At first sight, the task may look like any other task you defined before. But if you look closer, you’ll notice that you didn’t define an action or use the left shift operator. Gradle calls this a task configuration.

Listing 4.3. Writing a task configuration

If you run printVersion now, you’ll see that the new task loadVersion is executed first. Despite the fact that the task name isn’t printed, you know this because the build output prints the logging statement you added to it:

$ gradle printVersion
Reading the version file.
Version: 0.1-SNAPSHOT

You may ask yourself why the task was invoked at all. Granted, you didn’t declare a dependency on it, nor did you invoke the task on the command line. Task configuration blocks are always executed before task actions. The key to fully understanding this behavior is the Gradle build lifecycle. Let’s take a closer look at each of the build phases.

Gradle’s build lifecycle phases

Whenever you execute a Gradle build, three distinct lifecycle phases are run: initialization, configuration, and execution. Figure 4.5 visualizes the order in which the build phases are run and the code they execute.

Figure 4.5. Order of build phases in Gradle’s build lifecycle

During the initialization phase, Gradle creates a Project instance for your project. Your given build script only defines a single project. In the context of a multiproject build, this build phase becomes more important. Depending on which project you’re executing, Gradle figures out which of the project dependencies need to participate in the build. Note that none of your currently existing build script code is executed in this build phase. This will change in chapter 6 when you modularize the To Do application into a multiproject build.

The build phase next in line is the configuration phase. Internally, Gradle constructs a model representation of the tasks that will take part in the build. The incremental build feature determines if any of the tasks in the model are required to be run. This phase is perfect for setting up the configuration that’s required for your project or specific tasks.

Keep in mind that any configuration code is executed with every build of your project—even if you just execute gradle tasks.

In the execution phase tasks are executed in the correct order. The execution order is determined by their dependencies. Tasks that are considered up to date are skipped. For example, if task B depends on task A, then the execution order would be A → B when you run gradle B on the command line.

As you can see, Gradle’s incremental build feature is tightly integrated in the lifecycle. In chapter 3 you saw that the Java plugin made heavy use of this feature. The task compileJava will only run if any of the Java source files are different from the last time the build was run. Ultimately, this feature can improve a build’s performance significantly. In the next section, I’ll show how to use the incremental build feature for your own tasks.

4.2.8. Declaring task inputs and outputs

Gradle determines if a task is up to date by comparing a snapshot of a task’s inputs and outputs between two builds, as shown in figure 4.6. A task is considered up to date if inputs and outputs haven’t changed since the last task execution. Therefore, the task only runs if the inputs and outputs are different; otherwise, it’s skipped.

Figure 4.6. Gradle determines if a task needs to be executed though its inputs/outputs.

An input can be a directory, one or more files, or an arbitrary property. A task’s output is defined through a directory or 1...n files. Inputs and outputs are defined as fields in class DefaultTask and have a direct class representation, as shown in figure 4.7.

Figure 4.7. The class DefaultTask defines task inputs and outputs.

Let’s see this feature in action. Imagine you want to create a task that prepares your project’s deliverable for a production release. To do so, you’ll want to change the project version from SNAPSHOT to release. The following listing defines a new task that assigns the Boolean value true to the version property release. The task also propagates the version change to the property file.

Listing 4.4. Switching the project version to production-ready

As expected, running the task will change the version property and persist the new value to the property file. The following output demonstrates the behavior:

$ gradle makeReleaseVersion

$ gradle printVersion
Version: 0.1

The task makeReleaseVersion may be part of another lifecycle task that deploys the WAR file to a production server. You may be painfully aware of the fact that a deployment can go wrong. The network may have a glitch so that the server cannot be reached. After fixing the network issues, you’ll want to run the deployment task again. Because the task makeReleaseVersion is declared as a dependency to your deployment task, it’s automatically rerun. Wait, you already marked your project version as production-ready, right? Unfortunately, the Gradle task doesn’t know that. To make it aware of this, you’ll declare its inputs and outputs, as shown in the next listing.

Listing 4.5. Adding incremental build support via inputs/outputs

You moved the code you wanted to execute into a doLast action closure and removed the left shift operator from the task declaration. With that done, you now have a clear separation between the configuration and action code.

Task inputs/outputs evaluation

Remember, task inputs and outputs are evaluated during the configuration phase to wire up the task dependencies. That’s why they need to be defined in a configuration block. To avoid unexpected behavior, make sure that the value you assign to inputs and outputs is accessible at configuration time. If you need to implement programmatic output evaluation, the method upToDateWhen(Closure) on TaskOutputs comes in handy. In contrast to the regular inputs/outputs evaluation, this method is evaluated at execution time. If the closure returns true, the task is considered up to date.

Now, if you execute the task twice you’ll see that Gradle already knows that the project version is set to release and automatically skips the task execution:

$ gradle makeReleaseVersion

$ gradle makeReleaseVersion
:makeReleaseVersion UP-TO-DATE

If you don’t change the release property manually in the properties file, any subsequent run of the task makeReleaseVersion will be marked up to date.

So far you’ve used Gradle’s DSL to create and modify tasks in the build script. Every task is backed by an actual task object that’s instantiated for you during Gradle’s configuration phase. In many cases, simple tasks get the job done. However, sometimes you may want to have full control over your task implementation. In the next section, you’ll rewrite the task makeReleaseVersion in the form of a custom task implementation.

4.2.9. Writing and using a custom task

The action logic within the task makeReleaseVersion is fairly simple. Code maintainability is clearly not an issue at the moment. However, when working on your projects you’ll notice that simple tasks can grow in size quickly the more logic you need to add to them. The need for structuring your code into classes and methods will arise. You should be able to apply the same coding practices as you’re used to in your regular production source code, right? Gradle doesn’t suggest a specific way of writing your tasks. You have full control over your build source code. The programming language you choose, be it Java, Groovy, or any other JVM-based language, and the location of your task is up to you.

Custom tasks consist of two components: the custom task class that encapsulates the behavior of your logic, also called the task type, and the actual task that provides the values for the properties exposed by the task class to configure the behavior. Gradle calls these tasks enhanced tasks.

Maintainability is only one of the advantages of writing a custom task class. Because you’re dealing with an actual class, any method is fully testable through unit tests. Testing your build code is out of the scope of this chapter. If you want to learn more, feel free to jump to chapter 7. Another advantage of enhanced tasks over simple tasks is reusability. The properties exposed by a custom task can be set individually from the build script. With the benefits of enhanced tasks in mind, let’s discuss writing a custom task class.

Writing the custom task class

As mentioned earlier in this chapter, Gradle creates an instance of type DefaultTask for every simple task in your build script. When creating a custom task, you do exactly that—create a class that extends DefaultTask. The following listing demonstrates how to express the logic from makeReleaseVersion as the custom task class Release-VersionTask written in Groovy.

Listing 4.6. Custom task implementation

In the listing, you’re not using the DefaultTask’s properties to declare its inputs and outputs. Instead, you use annotations from the package org.gradle.api.tasks.

Expressing inputs and outputs through annotations

Task input and output annotations add semantic sugar to your implementation. Not only do they have the same effect as the method calls to TaskInputs and TaskOutputs, they also act as automatic documentation. At first glance, you know exactly what data is expected as input and what output artifact is produced by the task. When exploring the Javadocs of this package, you’ll find that Gradle provides you with a wide range of annotations.

In your custom task class, you use the @Input annotation to declare the input property release and the annotation @OutputFile to define the output file. Applying input and output annotations to fields isn’t the only option. You can also annotate the getter methods for a field.

Task input validation

The annotation @Input will validate the value of the property at configuration time. If the value is null, Gradle will throw a TaskValidationException. To allow null values, mark the field with the @Optional annotation.

Using the custom task

You implemented a custom task class by creating an action method and exposed its configurable properties through fields. But how do you actually use it? In your build script, you’ll need to create a task of type ReleaseVersionTask and set the inputs and outputs by assigning values to its properties, as shown in the next listing. Think of it as creating a new instance of a specific class and setting the values for its fields in the constructor.

Listing 4.7. Task of type ReleaseVersionTask

As expected, the enhanced task makeReleaseVersion will behave exactly the same way as the simple task if you run it. One big advantage you have over the simple task implementation is that you expose properties that can be assigned individually.

Applied custom task reusability

Let’s assume you’d like to use the custom task in another project. In that project, the requirements are different. The version POGO exposes different fields to represent the versioning scheme, as shown in the next listing.

Listing 4.8. Different version POGO implementation
class ProjectVersion {
   Integer min
   Integer maj
   Boolean prodReady

   String toString() {
      "$maj.$min${prodReady? '' : '-SNAPSHOT'}"

Additionally, the project owner decides to name the version file project-version .properties instead of How does the enhanced task adapt to these requirements? You simply assign different values to the exposed properties, as shown in the following listing. Custom task classes can flexibly handle changing requirements.

Listing 4.9. Setting individual property values for task makeReleaseVersion

Gradle ships with a wide range of out-of-the-box custom tasks for commonly used functionality, like copying and deleting files or creating a ZIP archive. In the next section we’ll take a closer look at some of them.

4.2.10. Gradle’s built-in task types

Do you remember the last time a manual production deployment went wrong? I bet you still have a vivid picture in your mind: angry customers calling your support team, the boss knocking on your door asking about what went wrong, and your coworkers frantically trying to figure out the root cause of the stack trace being thrown when starting up the application. Forgetting a single step in a manual release process can prove fatal.

Let’s be professionals and take pride in automating every aspect of the build lifecycle. Being able to modify the project’s versioning scheme in an automated fashion is only the first step in modeling your release process. To be able to quickly recover from failed deployments, a good rollback strategy is essential. Having a backup of the latest stable application deliverable for redeployment can prove invaluable. You’ll use some of the task types shipped with Gradle to implement parts of this process for your To Do application.

Here’s what you’re going to do. Before deploying any code to production you want to create a distribution. It’ll act as a fallback deliverable for future failed deployments. A distribution is a ZIP file that consists of your web application archive, all source files, and the version property file. After creating the distribution, the file is copied to a backup server. The backup server could either be accessible over a mounted shared drive or you could transfer the file over FTP. Because I don’t want to make this example too complex to grasp, you’ll just copy it to the subdirectory build/backup. Figure 4.8 illustrates the order in which you want the tasks to be executed.

Figure 4.8. Task dependencies for releasing the project

Using task types

Gradle’s built-in task types are derived classes from DefaultTask. As such, they can be used from an enhanced task within the build script. Gradle provides a broad spectrum of task types, but for the purposes of this example you’ll use only two of them. The following listing shows the task types Zip and Copy in the context of releasing the production version of your software. You can find the complete task reference in the DSL guide.

Listing 4.10. Using task types to back up a zipped release distribution

In this listing there are different ways of telling the Zip and Copy tasks what files to include and where to put them. Many of the methods used here come from the superclass AbstractCopyTask, as shown in figure 4.9. For a full list of available options, please refer to the Javadocs of the classes.

Figure 4.9. Inheritance hierarchy for the task types Zip and Copy

The task types you used offer far more configuration options than those shown in the example. Again, for a full list of available options, please refer to the DSL reference or the Javadocs. Next, we’ll take a deeper look at their task dependencies.

Task dependency inference

You may have noticed in the listing that a task dependency between two tasks was explicitly declared through the dependsOn method. However, some of the tasks don’t model a direct dependency to other tasks (for example, createDistribution to war). How does Gradle know to execute the dependent task beforehand? By using the output of one task as input for another task, dependency is inferred. Consequently, the dependent task is run automatically. Let’s see the full task execution graph in action:

$ gradle release
:processResources UP-TO-DATE
Releasing the project...

After running the build, you should find the generated ZIP file in the directory build/distributions, which is the default output directory for archive tasks. You can easily assign a different distribution output directory by setting the property destinationDir. The following directory tree shows the relevant artifacts generated by the build:

├── build
│   ├── backup
│   │   └──
│   ├── distributions
│   │   └──
│   └── libs
│       └── todo-webapp-0.1.war
├── build.gradle
├── src

Task types have incremental build support built in. Running the tasks multiple times in a row will mark them as up-to-date if you don’t change any of the source files. Next, you’ll learn how to define a task on which the behavior depends on a flexible task name.

4.2.11. Task rules

Sometimes you may find yourself in a situation where you write multiple tasks that do similar things. For example, let’s say you want to extend your version management functionality by two more tasks: one that increments the major version of the project and another to do the same work for the minor version classifier. Both tasks are also supposed to persist the changes to the version file. If you compare the doLast actions for both tasks in the following listing, you can tell that you basically duplicated code and applied minor changes to them.

Listing 4.11. Declaring tasks for incrementing version classifiers

If you run gradle incrementMajorVersion on a project with version 0.1-SNAPSHOT, you’ll see that the version is bumped up to 1.1-SNAPSHOT. Run it on the INFO log level to see more detailed output information:

$ gradle incrementMajorVersion –i
Incrementing major project version: 0.1-SNAPSHOT -> 1.1-SNAPSHOT
[ant:propertyfile] Updating property file: /Users/benjamin/books/

Having two separate tasks works just fine, but you can certainly improve on this implementation. In the end, you’re not interested in maintaining duplicated code.

Task rule-naming pattern

Gradle also introduces the concept of a task rule, which executes specific logic based on a task name pattern. The pattern consists of two parts: the static portion of the task name and a placeholder. Together they form a dynamic task name. If you wanted to apply a task rule to the previous example, the naming pattern would look like this: increment<Classifier>Version. When executing the task rule on the command line, you’d specify the classifier placeholder in camel-case notation (for example, incrementMajorVersion or incrementMinorVersion).

Task rules in practice

Some of Gradle’s core plugins make good use of task rules. One of the task rules the Java plugins define is clean<TaskName>, which deletes the output of a specified task. For example, running gradle cleanCompileJava from the command line deletes all production code class files.

Declaring a task rule

You just read about defining a naming pattern for a task rule, but how do you actually declare a task rule in your build script? To add a task rule to your project, you’ll first need to get the reference to TaskContainer. Once you have the reference, you can call the method addRule(String, Closure). The first parameter provides a description (for example, the task name pattern), and the second parameter declares the closure to execute to apply the rule. Unfortunately, there’s no direct way of creating a task rule through a method from Project as there is for simple tasks, as illustrated in figure 4.10.

Figure 4.10. Simple tasks can be directly added by calling methods of your project instance. Task rules can only be added through the task container, so you’ll need to get a reference to it first by invoking the getTasks() method.

With a basic understanding of how to add a task rule to your project, you can get started writing the actual closure implementation for it. The next listing demonstrates how applying a task rule becomes a very expressive tool to implement task actions with similar logic.

Listing 4.12. Merging similar logic into a task rule

After adding the task rule in your project, you’ll find that it’s listed under a specific task group called Rules when running the help task tasks:

$ gradle tasks
Pattern: increment<Classifier>Version - Increments project version type

Task rules can’t be grouped individually as you can do with any other simple or enhanced task. A task rule, even if it’s declared by a plugin, will always show up under this group.

4.2.12. Building code in buildSrc directory

You’ve seen how quickly your build script code can grow. In this chapter you already created two Groovy classes within your build script: ProjectVersion and the custom task ReleaseVersionTask. These classes are perfect candidates to be moved to the buildSrc directory alongside your project. The buildSrc directory is an alternative location to put build code and a real enabler for good software development practices. You’ll be able to structure the code the way you’re used to in any other project and even write tests for it.

Gradle standardizes the layout for source files under the buildSrc directory. Java code needs to sit in the directory src/main/java, and Groovy code is expected to live under the directory src/main/groovy. Any code that’s found in these directories is automatically compiled and put into the classpath of your regular Gradle build script. The buildSrc directory is a great way to organize your code. Because you’re dealing with classes, you can also put them into a specific package. You’ll make them part of the package com.manning.gia. The following directory structure shows the Groovy classes in their new location:

├── build.gradle
├── buildSrc
│   └── src
│       └── main
│           └── groovy
│               └── com
│                   └── manning
│                       └── gia
│                           ├── ProjectVersion.groovy
│                           └── ReleaseVersionTask.groovy
├── src
│   └── ...

Keep in mind that extracting the classes into their own source files requires some extra work. The difference between defining a class in the build script versus a separate source file is that you’ll need to import classes from the Gradle API. The following code snippet shows the package and import declaration for the custom task ReleaseVersionTask:

package com.manning.gia

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

class ReleaseVersionTask extends DefaultTask {

In turn, your build script will need to import the compiled classes from buildSrc (for example, com.manning.gia.ReleaseVersionTask). The following console output shows the compilation tasks that are run before the task you invoked on the command line:

$ gradle makeReleaseVersion
:buildSrc:compileJava UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:makeReleaseVersion UP-TO-DATE

The buildSrc directory is treated as its own Gradle project indicated by the path :buildSrc. Because you didn’t write any unit tests, the compilation and execution tasks for tests are skipped. Chapter 7 is fully dedicated to writing tests for classes in buildSrc.

In the previous sections, you learned the ins and outs of working with simple tasks, custom task classes, and specific task types provided by Gradle’s API. We examined the difference between task action and configuration code, as well as their appropriate use cases. An important lesson you learned is that action and configuration code is executed during different phases of the build lifecycle. The rest of this chapter will talk about how to write code that’s executed when specific lifecycle events are fired.

4.3. Hooking into the build lifecycle

As a build script developer, you’re not limited to writing task actions or configuration logic, which are evaluated during a distinct build phase. Sometimes you’ll want to execute code when a specific lifecycle event occurs. A lifecycle event can occur before, during, or after a specific build phase. An example of a lifecycle event that happens after the execution phase would be the completion of a build.

Suppose you want to get feedback about failed builds as early as possible in the development cycle. A typical reaction to a failed build could be that you send an email to all developers on the team to restore the sanity of your code. There are two ways to write a callback to build lifecycle events: within a closure, or with an implementation of a listener interface provided by the Gradle API. Gradle doesn’t steer you toward one of the options to listen to lifecycle events. The choice is up to you. The big advantage you have with a listener implementation is that you’re dealing with a class that’s fully testable by writing unit tests. To give you an idea of some of the useful lifecycle hooks, see figure 4.11.

Figure 4.11. Examples of build lifecycle hooks

An extensive list of all available lifecycle hooks is beyond the scope of this book. Many of the lifecycle callback methods are defined in the interfaces Project and Gradle. Gradle’s Javadocs are a great starting point to find the appropriate event callback for your use case.

Don’t be afraid of making good use of lifecycle hooks. They’re not considered a secret backdoor to Gradle’s API. Instead, they’re provided intentionally because Gradle can’t predict the requirements for your enterprise build.

In the following two sections, I’ll demonstrate how to receive notifications immediately after the task execution graph has been populated. To fully understand what’s happening under the hood when this graph is built, we’ll first look at Gradle’s inner workings.

Internal task graph representation

At configuration time, Gradle determines the order of tasks that need to be run during the execution phase. As noted in chapter 1, the internal structure that represents these task dependencies is modeled as a directed acyclic graph (DAG). Each task in the graph is called a node, and each node is connected by directed edges. You’ve most likely created these connections between nodes by declaring a dependsOn relationship for a task or by leveraging the implicit task dependency interference mechanism. It’s important to note that DAGs never contain a cycle. In other words, a task that has been executed before will never be executed again. Figure 4.12 demonstrates the DAG representation of the release process modeled earlier.

Figure 4.12. Task dependencies represented as Directed Acyclic Graph

Now that you have a better idea of Gradle’s internal task graph representation, you’ll write some code in your build script to react to it.

4.3.1. Hooking into the task execution graph

Recall the task makeReleaseVersion you implemented that was automatically executed as a dependency of the task release. Instead of writing a task to change the project’s version to indicate production-readiness, you could also achieve the same goal by writing a lifecycle hook. Because the build knows exactly which tasks will take part in the build before they get executed, you can query the task graph to check for its existence. Figure 4.13 shows the relevant interfaces and their methods to access the task execution graph.

Figure 4.13. TaskExecutionGraph provides the method whenReady that’s called when the task graph has been populated.

Next you’ll put the lifecycle hook in place. Listing 4.13 extends the build script by the method call whenReady to register a closure that’s executed immediately after the task graph has been populated. Because you know that the logic is run before any of the tasks in the graph are executed, you can completely remove the task make-Release-Version and omit the dependsOn declaration from createDistribution.

Listing 4.13. Release version functionality implemented as lifecycle hook

Alternatively, you can implement this logic as a listener, which you’ll do next.

4.3.2. Implementing a task execution graph listener

Hooking into the build lifecycle via a listener is done in two simple steps. First, you implement the specific listener interface by writing a class within your build script. Second, you register the listener implementation with the build.

The interface for listening to task execution graph events is provided by the interface TaskExecutionGraphListener. At the time of writing, you only need to implement one method: graphPopulate(TaskExecutionGraph). Figure 4.14 shows the listener implementation named ReleaseVersionListener.

Figure 4.14. Ways to register a TaskExecutionGraphListener. A listener can be registered through the generic addListener method or through a specific method that only takes an instance of a specialized listener type.

Keep in mind that you don’t have direct access to the Project instance if you add the listener to your build script. Instead, you can work Gradle’s API to its fullest. The following listing shows how to access the project by calling the getProject() method on the release task.

Listing 4.14. Release version functionality implemented as lifecycle listener

You’re not limited to registering a lifecycle listener in your build script. Lifecycle logic can be applied to listen to Gradle events even before any of your project’s tasks are executed. In the next section, we’ll explore options for hooking into the lifecycle via initialization scripts to customize the build environment.

4.3.3. Initializing the build environment

Let’s say you want to be notified about the outcome of a build. Whenever a build finishes, you’ll want to know whether it was successful or failed. You also want to be able to identify how many tasks have been executed. One of Gradle’s core plugins, the build-announcements plugin, provides a way to send announcements to a local notification system like Snarl (Windows) or Growl (Mac OS X). The plugin automatically picks the correct notification system based on your OS. Figure 4.15 shows a notification rendered by Growl.

Figure 4.15. Build announcement sent by Growl on Mac OS X

You could apply the plugin to every project individually, but why not use the powerful mechanisms Gradle provides? Initialization scripts are run before any of your build script logic has been evaluated and executed. You’ll write an initialization script that applies the plugin to any of your projects without manual intervention. Create the initialization script under <USER_HOME>/.gradle/init.d, as shown in the following directory tree:

└── .gradle
    └── init.d
        └── build-announcements.gradle

Gradle will execute every initialization script it finds under init.d as long as the file extension matches .gradle. Because you want to apply the plugin before any other build script code is executed, you’ll pick the lifecycle callback method that’s most appropriate for handling this situation: Gradle#projectLoaded(Closure). The following code snippet shows how to apply the build-announcements plugin to the build’s root project:

An important lesson to learn in this context is that some lifecycle events are only fired if they’re declared in the appropriate location. For example, the closure for the lifecycle hook Gradle#projectsLoaded(Closure) wouldn’t get fired if you declared it in your build.gradle, because the project creation happens during the initialization phase.

4.4. Summary

Every Gradle build script consists of two basic building blocks: one or more projects and tasks. Both elements are deeply rooted in Gradle’s API and have a direct class representation. At runtime, Gradle creates a model from the build definition, stores it in memory, and makes it accessible for you to access through methods. You learned that properties are a means of controlling the behavior of the build. A project exposes standard properties out of the box. Additionally, you can define extra properties on many of Gradle’s domain model objects (for example, on the project and task level) to declare arbitrary user data.

Later in the chapter, you learned the ins and outs of tasks. As an example, you implemented build logic to control your project’s version numbering scheme stored in an external properties file. You started out by adding simple tasks to the build script. Build logic can be defined directly in the action closure of a task. Every task is derived from the class org.gradle.api.DefaultTask. As such, it comes loaded with functionality accessible through methods of its superclass.

Understanding the build lifecycle and the execution order of its phases is crucial to beginners. Gradle makes a clear distinction between task actions and task configurations. Task actions, defined through the closures doFirst and doLast or its shortcut notation <<, are run during the execution phase. Any other code defined outside of a task action is considered a configuration and therefore executed beforehand during the configuration phase.

Next, we turned our attention to implementing nonfunctional requirements: build execution performance, code maintainability, and reusability. You added incremental build support to one of your existing task implementations by declaring its input and output data. If the data doesn’t change between the initial and subsequent builds task, execution is skipped. Implementing incremental build support is easy and cheap. If done right, it can significantly improve the execution time of your build. Complex build logic is best structured in custom task classes, which give you all the benefits of object-oriented programming. You practiced writing a custom task class by transferring the existing logic into an implementation of DefaultTask. You also cleaned up your build script by moving compilable code under the buildSrc directory. Gradle comes with a whole range of reusable task types like Zip and Copy. You incorporated both types by modeling a chain of task dependencies for releasing your project.

Access to Gradle’s internals is not limited to the model. You can register build lifecycle hooks that execute code whenever the targeted event is fired. As an example, you wrote a task execution graph lifecycle hook as a closure and listener implementation. Initialization scripts can be used to apply common code like lifecycle listeners across all of your builds.

You already got a first taste of the mechanisms that enable you to declare a dependency on an external library. In the next chapter, we’ll deepen your knowledge with a detailed discussion of working with dependencies and how dependency resolution works under the hood.

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

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