Chapter 10.  Writing Custom Tasks and Plugins

In Gradle, we can either write a simple task in a build file, where we add actions with a closure, or we can configure an existing task that is included in Gradle. The process of writing our own task is easy. There are different ways to create a custom task, which we will cover in this chapter:

  • We will see how to create a new task class in our build file and use it in our project.
  • We will discuss how to create custom tasks in a separate source file. We will also discuss how to make our task reusable in other projects.
  • We will discuss how to write a plugin for Gradle. Similar to writing custom tasks, we will cover the different ways to write a plugin. We will also see how to publish our plugin and discuss how to use it in a new project.
  • We can write our tasks and plugins in Groovy, which works very well with the Gradle API, but we can also use other languages, such as Java and Scala. As long as the code is compiled into bytecode, we are fine.

Creating a custom task

When we create a new task in a build and specify a task with the type property, we actually configure an existing task. The existing task is called enhanced task in Gradle. For example, the Copy task type is an enhanced task. We will configure the task in our build file, but the implementation of the Copy task is in a separate class file. It is good practice to separate the task usage from task implementation. It improves the maintainability and reusability of the task. In this section, we will create our own enhanced tasks.

Creating a custom task in the build file

First, let's see how to create a task to display the current Gradle version in our build by simply adding a new task with a simple action. We have seen these types of tasks earlier in other sample build files. In the following sample build, we will create a new info task:

task info(description: 'Show Gradle version') << { 
    println "Current Gradle version: $project.gradle.gradleVersion" 
} 

When we invoke the info task from the command line, we will see the following output:

$ gradle info
:info
Current Gradle version: 2.10
BUILD SUCCESSFUL
Total time: 0.829 secs

Now, we are going to create a new task definition in our build file and make it an enhanced task. We will create a new class in our build file and this class extends org.gradle.api.DefaultTask. We will write an implementation for the class by adding a new method. To indicate that the method is the action of the class, we will use the @TaskAction annotation.

After we have defined our task class, we can use it in our build file. We will add a task to the tasks project container and use the type property to reference our new task class.

In the following sample build file, we have a new InfoTask task class and the info task that uses this new task class:

/** 
* New class that defines a Gradle task. 
*/ 
class InfoTask extends DefaultTask { 
 
    /** 
    * Method that has the logic for the task. 
    * We tell this to Gradle with the @TaskAction annotation. 
    */ 
    @TaskAction 
    def info() { 
        // Show current Gradle version. 
        println "Current Gradle           
        version:$project.gradle.gradleVersion" 
    } 
} 
 
// Define new task in our Gradle build file 
// with name info and of type InfoTask. 
// InfoTask implementation is at the top. 
task info(type: InfoTask) 

Next, we will run our build file with the info task. In the following output, we can see our current Gradle version:

$ gradle info
:info
Current Gradle version:2.10
BUILD SUCCESSFUL
Total time: 0.6 secs

To customize our simple task, we can add properties to our task. We can assign values to these properties when we configure the task in our build file.

For our sample task, we will first add a prefix property. This property is used when we print the Gradle version instead of the 'Current Gradle version' text. We give it a default value, so when we use the task and don't set the property value, we still get a meaningful prefix. We can mark our property as optional because of the default value, with the @Optional annotation. This way we have documented that our property doesn't need to be configured when we use the task.

If we want another prefix in our output, we can configure the info task in our build file. We will assign the 'Running Gradle' value to the prefix property of our InfoTask:

/** 
* New class that defines a Gradle task. 
*/ 
class InfoTask extends DefaultTask { 
 
    /** 
    * An optional property for our task. 
    */ 
    @Optional 
    String prefix = 'Current Gradle version' 
 
    /** 
    * Method that has the logic for the task. 
    * We tell this to Gradle with the @TaskAction annotation. 
    */ 
    @TaskAction 
    def info() { 
        // Show current Gradle version. 
        println "$prefix: $project.gradle.gradleVersion" 
    } 
} 
 
// Define new task in our Gradle build file 
// with name info and of type InfoTask. 
// InfoTask implementation is at the top. 
// We give the optional property prefix a value. 
task info(type: InfoTask) { 
    prefix = 'Running Gradle' 
} 

Now, if we run our build file, we can see our new prefix value in the output:

$ gradle info
:info
Running Gradle: 2.10
BUILD SUCCESSFUL
Total time: 0.588 secs

Using incremental build support

We know Gradle supports incremental builds. This means that Gradle can check whether a task has any dependencies for input or output on files, directories, and properties. If none of these have changed since the last build, the task is not executed. We will discuss how to use annotations with our task properties to make sure that our task supports Gradle's incremental build feature.

We have seen how to use the inputs and outputs properties of tasks that we have created so far. To indicate the properties of our new enhanced tasks that are input and output properties, the ones used by Gradle's incremental support, we must add certain annotations to our class definition. We can assign the annotation to the field property or the getter method for the property.

In a previous chapter, we created a task that reads a XML source file and converts the contents to a text file. Let's create a new enhanced task for this functionality. We will use the @InputFile annotation for the property that holds the value for the source XML file. The @OutputFile annotation is assigned to the property that holds the output file, as follows:

class ConvertTask extends DefaultTask { 
 
    /** 
    * Input file for this task and by 
    * using the @InputFile annotation we 
    * tell Gradle the file can be used 
    * for determining incremental build support. 
    */ 
    @InputFile 
    File source 
 
    /** 
    * Output file for this task and by 
    * using the @OutputFile annotation we 
    * tell Gradle the file can be used 
    * for determining incremental build support. 
    */ 
    @OutputFile 
    File output 
 
    /** 
    * Method with the real implementation of the task. 
    * We convert the source file and save the output. 
    */ 
    @TaskAction 
    void convert() { 
        def xml = new XmlSlurper().parse(source) 
        output.withPrintWriter { writer -> 
            xml.person.each { person -> 
                writer.println "${person.name},${person.email}" 
            } 
        } 
        println "Converted ${source.name} to ${output.name}" 
    } 
} 
 
// Configure task for this build 
task convert(type: ConvertTask) { 
    source = file("src/people.xml") 
    output = file("$buildDir/convert-output.txt") 
} 

Let's create an XML file with the name people.xml in the current directory, with the following code:

<?xml version="1.0"?> 
<people> 
    <person> 
        <name>mrhaki</name> 
        <email>[email protected]</email> 
    </person> 
</people> 

Now, we can invoke the convert task in our build file. We can see in the output that the file is converted:

$ gradle convert
:convert
Converted people.xml to convert-output.txt
BUILD SUCCESSFUL
Total time: 0.99 secs

If we look at the contents of the convert-output.txt file, we will see the following values from the source file:

$ cat build/convert-output.txt
mrhaki,[email protected]

When we invoke the convert task for the second time, we can see that Gradle's incremental build support has noticed that the input and output file haven't changed, so our task is up to date:

$ gradle convert
:convert UP-TO-DATE
BUILD SUCCESSFUL
Total time: 0.621 secs

The following table shows the annotations that we can use to indicate the input and output properties of our enhanced task:

Annotation Name

Description

@Input

Indicates that property specifies an input value. When the value of this property changes, the task is not longer up to date.

@InputFile

Indicates that property is an input file. Use this for properties that reference a single file of the File type.

@InputFiles

Marks property as input files for a property that holds a collection of File objects.

@InputDirectory

Indicates that property is an input directory. Use this for a File type property that references a directory structure.

@OutputFile

Indicates that property is an output file. Use this for properties that reference a single file of the File type.

@OutputFiles

Marks property as output files for a property that holds a collection of File objects.

@OutputDirectory

Indicates that property is an output directory. Use this for a File type property that references a directory structure. If the output directory doesn't exist, it will be created.

@OutputDirectories

Marks property as an output directory Use this for a property that references a collection of File objects, which are references to directory structures.

@Optional

If applied to any of the preceding annotations, we will mark it as optional. The value doesn't have to be applied for this property.

@Nested

We can apply this annotation to a JavaBean property. The bean object is checked for any of the preceding annotations. This way, we can use arbitrary objects as input or output properties.

Creating a task in the project source directory

In the previous section, we defined and used our own enhanced task in the same build file. Now we are going to extract the class definition from the build file and put it in a separate file. We are going to place the file in the buildSrc project source directory.

Let's move our InfoTask to the buildSrc directory of our project. We will first create the buildSrc/src/main/groovy/sample directory. We will create an InfoTask.groovy file in this directory, with the following code:

package sample 
 
import org.gradle.api.DefaultTask 
import org.gradle.api.tasks.TaskAction 
 
class InfoTask extends DefaultTask { 
    /** 
    * Task property can be changed by user 
    * of this task. 
    */ 
    String prefix = 'Current Gradle version' 
 
    /** 
    * Method with actual implementation for this task. 
    */ 
    @TaskAction 
    def info() { 
        println "$prefix: $project.gradle.gradleVersion" 
    } 
} 

Notice that we must add import statements for the classes of the Gradle API. These imports are implicitly added to a build script by Gradle; but if we define the task outside the build script, we must add theimport statements ourselves.

In our project build file, we have to only create a new info task of the InfoTask type. Notice that we must use the package name to identify our InfoTask class or add an import sample.InfoTask statement:

// Define new task of type sample.InfoTask. 
task info(type: sample.InfoTask) { 
    // Set task property/ 
    prefix = "Running Gradle" 
} 

If we run the build, we can see that Gradle first compiles the InfoTask.groovy source file, as follows:

$ gradle info
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes
:buildSrc:jar
:buildSrc:assemble
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build
:info
Running Gradle: 2.10
BUILD SUCCESSFUL
Total time: 1.751 secs

As a matter of fact, the build task of the buildSrc directory is executed. We can customize the build of the buildSrc directory by adding a build.gradle file. In this file, we can configure tasks, add new tasks, and practically do anything we can in a normal project build file. The buildSrc directory can even be a multi-project build.

Let's add a new build.gradle file in the buildSrc directory. We will add a simple action to the build task, which prints the 'Done building buildSrc' value:

// File: buildSrc/build.gradle 
 
// Add new action to the build task 
// for the buildSrc directory. 
build.doLast { 
    println 'Done building buildSrc' 
} 

If we run our project build, we can see the following output:

$ gradle info
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build
Done building buildSrc
:info
Running Gradle: 2.10
BUILD SUCCESSFUL
Total time: 0.699 secs

Writing tests

As the buildSrc directory is similar to any other Java/Groovy project, we can also create tests for our task. We have the same directory structure as that of a Java/Groovy project and we can also define extra dependencies in the build.gradle file.

If we want to access a Project object in our test class, we can use the org.gradle.testfixtures.ProjectBuilder class. With this class, we can configure a Project object and use it in our test case. We can optionally configure the name, parent, and project directory before using the build() method to create a new Project object. We can use the Project object, for example, to add a new task with the type of our new enhanced task and see if there are any errors. ProjectBuilder is meant for low-level testing. The actual tasks are not executed.

In the following JUnit test, we will test whether the property value can be set. We have a second test to check whether the task of the InfoTask type is added to the task container of a project:

package sample 
 
import org.junit.* 
import org.gradle.api.* 
import org.gradle.testfixtures.ProjectBuilder 
 
class InfoTaskTest { 
 
    @Test 
    void createTaskInProject() { 
        final Task newTask = createInfoTask() 
        assert newTask instanceof InfoTask 
    } 
 
    @Test 
    void propertyValueIsSet() { 
        final Task newTask = createInfoTask() 
        newTask.configure { 
            prefix = 'Test' 
        } 
        assert newTask.prefix == 'Test' 
    } 
 
    private Task createInfoTask() { 
        // We cannot use new InfoTask() to create a new instance, 
        // but we must use the Project.task() method. 
        final Project project = ProjectBuilder.builder().build() 
        project.task('info', type: InfoTask) 
    } 
} 

In our build.gradle file in the buildSrc directory, we must add a Maven repository and the dependency on the JUnit libraries using the following lines of code:

repositories { 
    jcenter() 
} 
 
dependencies { 
    testCompile 'junit:junit:4.12' 
} 

Our test is automatically executed as the test task is part of the build process for the buildSrc directory.

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

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