2017年8月9日 星期三

[ Gradle IA ] Ch4 - Build script essentials (Part1)

Preface 
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

Building blocks 
Every Gradle build consists of three basic building blocks: projectstasks, 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. 



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. 

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: 



A project can create new tasks, add dependencies and configurations, and apply plugins 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 realworld 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: 
  1. // Setting project’s description without explicitly using project variable  
  2. setDescription("myProject")  
  3. // Using Groovy syntax to access name and description properties with and without using project variable  
  4. println "Description of project $name: " + project.description  
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. 

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 2Many 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. 


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. 

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: 
  1. // Only initial declaration of extra property requires you to use ext namespace  
  2. project.ext.myProp = 'myValue'  
  3. ext {  
  4.     someOtherProp = 123  
  5. }  
  6.   
  7. // Using ext namespace to access extra property is optional  
  8. assert myProp == 'myValue'  
  9. println project.someOtherProp  
  10. ext.someOtherProp = 567  
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 gradle.properties under the directory /.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 /.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 gradle.properties file: 
  1. exampleProp = myValue  
  2. someOtherProp = 455  
You can access both variables in your project as follows: 
  1. assert project.exampleProp == 'myValue'  
  2. task printGradleProperty << {  
  3.     println "Second property: $someOtherProp"  
  4. }  
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 ORG_GRADLE_PROJECT_propertyName=someValue

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. 

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. 

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. 



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. 

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: 
  1. version = '0.1-SNAPSHOT'  
  2. task printVersion {  
  3.     doLast {  
  4.         println "Version: $version"  
  5.     }  
  6. }  
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
...
:printVersion
Version: 0.1-SNAPSHOT
... 

The same result could be achieved as the first action of the task by using the doFirst method instead: 
  1. task printVersion {  
  2.     doFirst {  
  3.         println "Version: $version"  
  4.     }  
  5. }  
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: 
  1. task printVersion {  
  2.     doFirst {  
  3.         println "Before reading the project version"  
  4.     }  
  5.     doLast {  
  6.         println "Version: $version"  
  7.     }  
  8. }  
  9.   
  10. printVersion.doFirst{ println "First action" }  
  11. printVersion << { println "Last action" }  
The execution result: 
# gradle printVersion
...
:printVersion
First action
Before reading the project version
Version: 0.1-SNAPSHOT
Last action
...

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. 

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
  1. task printVersion << {  
  2.     logger.quiet "Version: $version"  
  3. }  
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: 
  1. task printVersion(group: 'versioning',  
  2.                   description: 'Prints project version.') << {  
  3.     logger.quiet "Version: $version"  
  4. }  
Alternatively, you can also set the properties by calling the setter methods, as shown in the following code snippet: 
  1. task printVersion {  
  2.     group = 'versioning'  
  3.     description = 'Prints project version.'  
  4.     doLast {  
  5.         logger.quiet "Version: $version"  
  6.     }  
  7. }  
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. 

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 
  1. task first << { println "first" }  
  2. task second << { println "second" }  
  3. task printVersion(group: 'versioning',  
  4.                   description: 'Prints project version.',  
  5.                   dependsOn: [second, first]) << {  
  6.     logger.quiet "Version: $version"  
  7. }  
  8.   
  9. task third << { println "third" }  
  10. third.dependsOn('printVersion')  
You’ll execute the task dependency chain by invoking the task third from the command line: 
# gradle -q third
first
second
Version: 0.1
third

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. 

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 taskswhich 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
  1. task first << { println "first" }  
  2. task second << { println "second" }  
  3. first.finalizedBy second  
You’ll find executing the task first will automatically trigger the task named second
# gradle -q first
first
second

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

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 
  1. class ProjectVersion {  
  2.     Integer major  
  3.     Integer minor  
  4.     Boolean release  
  5.     ProjectVersion(Integer major, Integer minor) {  
  6.         this.major = major  
  7.         this.minor = minor  
  8.         this.release = Boolean.FALSE  
  9.     }  
  10.     ProjectVersion(Integer major, Integer minor, Boolean release) {  
  11.         this(major, minor)  
  12.         this.release = release  
  13.     }  
  14.     @Override  
  15.     String toString() {  
  16.         "$major.$minor${release ? '' : '-SNAPSHOT'}"  
  17.     }  
  18. }  
  19.   
  20. version = new ProjectVersion(01)  
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. 

Understanding task configuration 
Before you get started writing code, you’ll need to create a properties file named version.properties 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: 
- version.properties 
  1. major = 0  
  2. minor = 1  
  3. 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 
  1. ext.versionFile = file('version.properties')  
  2.   
  3. task loadVersion {  
  4.     project.version = readVersion()  
  5. }  
  6.   
  7. ProjectVersion readVersion() {  
  8.     logger.quiet 'Reading the version file.'  
  9.     if(!versionFile.exists()) {  
  10.         throw new GradleException("Required version file does not exist: \  
  11.                                    $versionFile.canonicalPath")  
  12.     }  
  13.     Properties versionProps = new Properties()  
  14.     versionFile.withInputStream { stream ->  
  15.         versionProps.load(stream)  
  16.     }  
  17.     new ProjectVersion(versionProps.major.toInteger(),  
  18.                        versionProps.minor.toInteger(),  
  19.                        versionProps.release.toBoolean())  
  20. }  
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.
> Task :printVersion
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. 



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 6when 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. 
Notes. 
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. 

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. 


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: 



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 
  1. task makeReleaseVersion(group: 'versioning',  
  2.                         description: 'Makes project a release version.') << {  
  3.     version.release = true  
  4.     ant.propertyfile(file: versionFile) {  
  5.         entry(key: 'release', type: 'string', operation: '=', value: 'true')  
  6.     }  
  7. }  
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
...
> Task :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 
  1. task makeReleaseVersion(group: 'versioning',  
  2.                         description: 'Makes project a release version.') {  
  3.     inputs.property('release', version.release)   // Declaring version release property as input  
  4.     outputs.file versionFile   // As the version file is going to be modified it’s declared as output file property  
  5.     doLast{  
  6.         version.release = true  
  7.         ant.propertyfile(file: versionFile) {  
  8.             entry(key: 'release', type: 'string', operation: '=', value: 'true')  
  9.         }  
  10.     }  
  11. }  
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
...
1 actionable task: 1 executed


# gradle makeReleaseVersion
...
1 actionable task: 1 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. 

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 ReleaseVersionTask written in Groovy. 
- Listing 4.6 Custom task implementation 
  1. class ReleaseVersionTask extends DefaultTask {  
  2.     @Input Boolean release  
  3.     @OutputFile File destFile  
  4.     ReleaseVersionTask() {  
  5.         group = 'versioning'  
  6.         description = 'Makes project a release version.'  
  7.     }  
  8.     @TaskAction  
  9.     void start() {  
  10.         project.version.release = true  
  11.         ant.propertyfile(file: destFile) {  
  12.             entry(key: 'release', type: 'string', operation: '=', value: 'true')  
  13.         }  
  14.     }  
  15. }  
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, youknow 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 
  1. task makeReleaseVersion(type: ReleaseVersionTask) {  
  2.     release = version.release  
  3.     destFile = versionFile  
  4. }  
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 
  1. class ProjectVersion {  
  2.     Integer min  
  3.     Integer maj  
  4.     Boolean prodReady  
  5.     @Override  
  6.     String toString() {  
  7.         "$maj.$min${prodReady? '' : '-SNAPSHOT'}"  
  8.     }  
  9. }  
Additionally, the project owner decides to name the version file project-version.properties instead of version.properties. 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 
  1. task makeReleaseVersion(type: ReleaseVersionTask) {  
  2.     release = version.prodReady  
  3.     destFile = file('project-version.properties')  
  4. }  
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. 

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. 



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 
  1. task createDistribution(type: Zip, dependsOn: makeReleaseVersion) {  
  2.     // Implicit reference to Takes all source files output of War task  
  3.     from war.outputs.files  
  4.   
  5.     // Takes all source files output of War task and puts them into src directory of ZIP file  
  6.     from(sourceSets*.allSource) {  
  7.         into 'src'  
  8.     }  
  9.   
  10.     // Adds version file to ZIP  
  11.     from(rootDir) {  
  12.         include versionFile.name  
  13.     }  
  14. }  
  15.   
  16. task backupReleaseDistribution(type: Copy) {  
  17.     from createDistribution.outputs.files  
  18.     into "$buildDir/backup"  
  19. }  
  20. task release(dependsOn: backupReleaseDistribution) << {  
  21.     logger.quiet 'Releasing the project...'  
  22. }  
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. 



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. 

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
:makeReleaseVersion
:compileJava
:processResources UP-TO-DATE
:classes
:war
:createDistribution
:backupReleaseDistribution
:release
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: 



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. 

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 
  1. task incrementMajorVersion(group: 'versioning', description: 'Increments project major version.') << {  
  2.     String currentVersion = version.toString()  
  3.     ++version.major  
  4.     String newVersion = version.toString()  
  5.     logger.info "Incrementing major project version: $currentVersion -> $newVersion"  
  6.     ant.propertyfile(file: versionFile) {  
  7.         entry(key: 'major', type: 'int', operation: '+', value: 1)  
  8.     }  
  9. }  
  10. task incrementMinorVersion(group: 'versioning', description: 'Increments project minor version.') << {  
  11.     String currentVersion = version.toString()  
  12.     ++version.minor  
  13.     String newVersion = version.toString()  
  14.     logger.info "Incrementing minor project version: $currentVersion -> $newVersion"  
  15.     ant.propertyfile(file: versionFile) {  
  16.         entry(key: 'minor', type: 'int', operation: '+', value: 1)  
  17.     }  
  18. }  
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
...
:incrementMajorVersion (Thread[Daemon worker Thread 2,5,main]) started.

> Task :incrementMajorVersion
Putting task artifact state for task ':incrementMajorVersion' into context took 0.0 secs.
Executing task ':incrementMajorVersion' (up-to-date check took 0.0 secs) due to:
Task has not declared any outputs.
Incrementing major project version: 0.1-SNAPSHOT -> 1.1-SNAPSHOT
[ant:propertyfile] Updating property file: /root/GradleIA/version.properties

:incrementMajorVersion (Thread[Daemon worker Thread 2,5,main]) completed. Took 0.051 secs.
...

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. 



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: 
- Listing 4.12 Merging similar logic into a task rule 
  1. tasks.addRule("Pattern: incrementVersion – Increments the project version classifier.") { String taskName ->  
  2.     if(taskName.startsWith('increment') && taskName.endsWith('Version')) {  
  3.         task(taskName) << {  
  4.             String classifier = (taskName - 'increment' - 'Version').toLowerCase()  
  5.             String currentVersion = version.toString()  
  6.             switch(classifier) {  
  7.                 case 'major': ++version.major  
  8.                     break  
  9.                 case 'minor': ++version.minor  
  10.                     break  
  11.                 defaultthrow new GradleException("Invalid version \  
  12.                                                     type '$classifier. Allowed types: ['Major', 'Minor']")  
  13.             }  
  14.             String newVersion = version.toString()  
  15.             logger.info "Incrementing $classifier project version: $currentVersion -> $newVersion"  
  16.             ant.propertyfile(file: versionFile) {  
  17.                 entry(key: classifier, type: 'int', operation: '+', value: 1)  
  18.             }  
  19.         }  
  20.     }  
  21. }  
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
...
Rules
-----

Pattern: clean: Cleans the output files of a task.
Pattern: build: Assembles the artifacts of a configuration.
Pattern: upload: Assembles and uploads the artifacts belonging to a configuration.
Pattern: incrementVersion – Increments the project version classifier.
...

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. 

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/groovyAny 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: 



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
- ReleaseVersionTask.groovy 
  1. package com.manning.gia  
  2.   
  3. import org.gradle.api.DefaultTask  
  4. import org.gradle.api.tasks.Input  
  5. import org.gradle.api.tasks.OutputFile  
  6. import org.gradle.api.tasks.TaskAction  
  7.   
  8. class ReleaseVersionTask extends DefaultTask {  
  9.     @Input Boolean release  
  10.     @OutputFile File destFile  
  11.     ReleaseVersionTask() {  
  12.         group = 'versioning'  
  13.         description = 'Makes project a release version.'  
  14.     }  
  15.     @TaskAction  
  16.     void start() {  
  17.         project.version.release = true  
  18.         ant.propertyfile(file: destFile) {  
  19.             entry(key: 'release', type: 'string', operation: '=', value: 'true')  
  20.         }  
  21.     }  
  22. }  
In turn, your build script will need to import the compiled classes from buildSrc (for example, com.manning.gia.ReleaseVersionTask): 
  1. import com.manning.gia.*  
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:compileGroovy
...

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

Supplement 
認識 Gradle - (5)Gradle Task 觀念導讀 

沒有留言:

張貼留言

[ FP In Python ] Ch1. (Avoiding) Flow Control

Preface   In typical imperative Python programs—including those that make use of classes and methods to hold their imperative code—a block...