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:
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.
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
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 |
|
Indicates that property specifies an input value. When the value of this property changes, the task is not longer up to date. |
|
Indicates that property is an input file. Use this for properties that reference a single file of the |
|
Marks property as input files for a property that holds a collection of |
|
Indicates that property is an input directory. Use this for a |
|
Indicates that property is an output file. Use this for properties that reference a single file of the |
|
Marks property as output files for a property that holds a collection of |
|
Indicates that property is an output directory. Use this for a |
|
Marks property as an output directory Use this for a property that references a collection of |
|
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. |
|
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. |
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
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.
3.144.21.158