Most of the content in this article comes from Gradle official documentation. Students who are OK with English can read the official documentation directly. The sample code for this article has been placed on GitHub of ZJXStar.

preface

In the last article, we learned about Project in Gradle, and we also covered the definition of a simple Task. There are multiple tasks in a Project, and Gradle owes a number of operations to tasks. In this article, you will learn more about tasks and how to create, access, and configure a Task.

What is a Task

Tasks represent individual atomic tasks in the build process, such as compiling classes or generating Javadoc. Each task belongs to a Project from which it can be accessed by task name or TaskContainer. Each task has a fully qualified path that is unique across all tasks in all projects. The path is a concatenation of the project path and the task name, separated by: characters.

When Gradle executes a task, it can label the task with different results in the console UI and Tooling API. These labels are based on whether the task has actions to perform, whether they should be performed, whether they have been performed, and whether any changes have been made to those actions.

There are five types of tags :(add -i or –info when running Task)

  • No label or EXECUTED: The task performed its action.
    1. Tasks have actions, and Gradle has determined that they should be executed as part of the build.
    2. The task has no action, but some dependencies, and some dependencies are executed.
  • UP-TO-DATE: The output of the task has not changed.
    1. Tasks have outputs and inputs, but they don’t change.
    2. The task has actions, but the task tells Gradle that it does not change its output.
    3. Tasks have no actions and dependencies, but all dependencies are up-to-date, skipped, or from the cache.
    4. Tasks have no actions and no dependencies.
  • From-cache: The output of a task can be found FROM previous executions. The task has output recovered from the build cache.
  • SKIPPED: The task did not perform its action.
    1. Tasks have been explicitly excluded from the command line.
    2. The task has a onlyIf assertion that returns false.
  • No-source: The task does not need to perform its action. Tasks have inputs and outputs, but no source.

Create a Task

In Gradle, we have several ways to create tasks. The task method provided by Project and the Create method provided by TaskContainer (Tasks) are used.

Method 1: Create a Task using a string as the Task name. For example, run gradlew -q helloA

task('helloA') {
    doLast {
        println("helloA task(name)")}}Copy the code

Method 2: Use the Create method for Tasks, for example, gradlew -q helloB

tasks.create('helloB') {
    doFirst {
        println("helloB tasks.create(name)")}}Copy the code

Method 3: Use the special syntax of DSL. Example: gradlew -q helloC

task helloC {
    doLast {
        println("helloC task name")}}// Run gradlew -q helloD
task(helloD) {
    doLast {
        println("helloD task(name)")}}Copy the code

When creating a Task, you can pass in a Map to simply configure the Task. Common configuration items include:

Configuration items describe The default value
type Created based on an existing Task, similar to inheritance DefaultTask
overwrite Whether to replace an existing Task. This parameter is used with type false
dependsOn Used to configure task dependencies []
action An Action or closure added to a task null
description Description of the configuration task null
group Used to configure groups of tasks null

Example:

// Add configuration items using Map
task copy(type: Copy)

// Overwrites the original copy task
task copy(overwrite: true) {
    doLast {
        println('I am the new one.')}}// Use gradlew tasks to view the configuration of helloE
task helloE {
    description 'i am helloE'
    group BasePlugin.BUILD_GROUP
    doLast {
        println('this is helloE')}}Copy the code

This is the basic approach to creating a Task. But in build.gradle scripts, we can take advantage of the powerful features of the Groovy language to create multiple tasks on the fly. For example, run gradlew -q task1

// Create 4 tasks simultaneously: task0, task1, task2, task3
4.times { counter ->
    task "task$counter" {
        doLast {
            println "I'm task number $counter"}}}Copy the code

Access to the Task

When tasks are created, they can be accessed for configuration or dependencies. So how do you access a defined Task? There are three main approaches.

Method 1: Access using Groovy’s DSL special syntax.

// Follow up on the example above
// Take the helloE task as an example
println 'Task helloE name:' + helloE.name
println 'Description of task helloE:' + project.helloE.description
Copy the code

Method 2: Use Tasks to access the task set.

/ / use the tasks
println tasks.named('helloD').get().name
println tasks.copy.doLast {
    println 'configure by tasks.copy.doLast'
}
println tasks['helloC'].name
println tasks.getByName('helloB').name
Copy the code

Method 3: Access tasks using a path

println tasks.getByPath('helloE').path // Failed to find UnknownTaskException
println tasks.getByPath(':app:helloE').path
def ehelloE = tasks.findByPath("EhelloE") // Return null if not found;
println ehelloE == null
Copy the code

There are two ways to navigate a pathfinding task, one is get and the other is find. The difference is that the GET method throws UnknownTaskException if the task cannot be found, while the find method returns NULL.

Now that you have access to the Task, you can perform operations on the Task that involve the properties and methods of the Task, which are briefly described here.

Task has a range of four attributes. You can access specified properties either through the property name or the task.property (java.lang.string) method. The property value can also be modified using the task.setProperty (java.lang.String, java.lang.object) method. The four areas are as follows:

  • Properties of the Task object itself. This includes any properties declared in the Task implementation class with getters and setters methods. Properties of this scope are read and write based on the presence of corresponding getter and setter methods.
  • Plug-ins added to the extension of the task. Each extension can be used as a read-only property with the same name as the extension.
  • Convention properties that are added to the task through plug-ins. The plug-in adds properties and methods to the task through the Convention object. The readability of the properties of this scope depends on the convention object.
  • Additional properties. Each task object maintains a mapping of additional attributes, yesName - > valueYes, it can be used to dynamically add properties to a task object. Properties of this scope are read and write.

Common attributes are:

The property name describe
actions The sequence of actions to be performed by the task
dependsOn Returns the task that the task depends on
description Task Description
enabled Whether the task is enabled
finalizedBy Returns the task after completion of this task
group Grouping of tasks
mustRunAfter Returns the task after which the task must run
name Mission name
path Path of task
project The Project to which the task belongs

Let’s take an example of an additional attribute:

// Define additional attributes for Task
task myTask {
    ext.myProperty = "myValue"
}
// Access the property
task printTaskProperties {
    doLast {
        println myTask.myProperty
    }
}
Copy the code

The other attributes are described in more detail in the following sections.

As for the methods of Task, they are briefly listed here:

Method name (no arguments listed) describe
dependsOn Set dependency tasks for tasks
doFirst Add a Task action to the Task to start executing the previous action
doLast Add a Task action after the Task action has finished executing
finalizedBy Add a terminating task to a task, that is, a task to be executed after the task is finished
hasProperty Checks whether the task has the specified properties
mustRunAfter State that the task must be executed after certain tasks
onlyIf Add an assertion to a task that can be executed only if the condition is met
property Returns the value of the specified property
setProperty Modifies the value of the specified attribute

DoFirst and doLast are often seen in the previous examples, and are described in more detail below.

DefaultTask

Gradle also provides us with a DefaultTask base class, which can be inherited to use custom tasks, used in custom Gradle plug-ins. Here is a simple example to illustrate the use of DefaultTask.

/ / app build. Gradle
class CustomTask extends DefaultTask {
    final String message
    final int number

    def content // Configure parameters

    // Add construction parameters
    @Inject
    CustomTask(String message, int number) {
        this.message = message
        this.number = number
    }

    // Add the action to perform
    @TaskAction
    def greet() {
        println content
        println "message is $message , number is $number !"}}// Create tasks using tasks
// Two arguments need to be passed, not null
tasks.create('myCustomTask1', CustomTask, 'hahaha'.110)
myCustomTask1.content = 'i love you'
myCustomTask1.doFirst {
    println 'my custom task do first'
}
myCustomTask1.doLast {
    println 'my custom task do last'
}

// Create using task, constructorArgs are used as constructorArgs and cannot be null
task myCustomTask2(type: CustomTask, constructorArgs: ['xixi'.120])
myCustomTask2.content = 'i hate you'
Copy the code

The CustomTask class in the example is defined in the build.gradle file and inherits DefaultTask directly. If you want the Task to have an executable action, you need to annotate the action method with @TaskAction so that Gradle adds the action to the Task’s action list. We can configure attributes for tasks in the class, such as Content, Message, and Number in the example. Message and number are set as construction parameters via the @javax.inject.Inject annotation, and these two parameters need to be passed when creating the custom task.

Task execution analysis

Now that we have looked at Task creation and use, it is necessary to do a general analysis of Task execution to help us understand tasks in depth.

When we execute a Task, we execute a List of actions that it owns. This List is stored in the Actions member variable of the Task instance and is of type List:

private List<ContextAwareTaskAction> actions;
Copy the code

So how do you add execution actions to this list? In the previous example code, you should notice that both doFirst and doLast methods are used to create tasks. Yes, these two methods add the action to the actions list.

We have a rough look at the source of the doFirst and doLast implementation: (these two methods in the class org. Gradle. API. In the internal. AbstractTask)

@Override
public Task doFirst(final String actionName, finalAction<? super Task> action) { ... Taskmutator.mutate (" task.dofirst (Action)", new Runnable() {public void run() {// Add to the list header getTaskActions().add(0, wrap(action, actionName)); }}); return this; } @Override public Task doLast(final String actionName, final Action<? super Task> action) { ... taskMutator.mutate("Task.doLast(Action)", New Runnable() {public void run() {getTaskActions().add(wrap(action, actionName)); }}); return this; }Copy the code

As you can see, doFirst adds actions to the head of the table, while doLast adds actions to the tail.

So how do you add actions to the middle of the list? Do you write actions directly in Task? We can try:

// Run gradlew -q greetC
task greetC { // Can display declaration doFirst doLast
    // Execute in the configuration phase
    println 'i am greet C, in configure'

    // Execute in the execution phase
    doFirst {
        println 'i am greet C, in doFirst'
    }

    // Execute in the execution phase
    doLast {
        println 'i am greet C, in doLast'}}Copy the code

After executing this task, you will find that the sentence “I am greet C, in configure” is printed not between doFirst and doLast, but in the configuration phase of the task. Actions written in the Task are not added to the Task action list, but are executed as Task configuration information. Is there any other way?

The answer is yes, using the DefaultTask class mentioned in the previous section. The actions we mark with the @taskAction annotation are added to the middle of the Task’s action list. We directly execute the myCustomTask1 task (gradlew -q myCustomTask1) from the previous section. The results are as follows:

my custom task do first // doFirst
i love you // @TaskAction
message is hahaha , number is 110 ! // @TaskAction
my custom task do last // doLast
Copy the code

At this point, the order of execution of tasks is basically clear.

One thing to note here is that when we create a task, we can use the left-shift symbol << to indicate that we want to assign a doFirst action to the task.

task greetB << { // << is equivalent to doFirst
    println 'i am greet B, in doFirst'

    // doLast cannot be configured
// doLast {
// println 'i am greet B, in doLast'
/ /}
}
// Can only be configured separately
greetB.doLast {
    println 'i am greet B, in doLast'
}
Copy the code

Moving the symbol to the left is equivalent to doFirst, and the task can no longer be configured with doLast actions, only separately.

Task dependencies and order

As we know, a Project has multiple tasks and the relationships between these tasks are maintained by DAG diagrams. The DAG graph is generated during the configuration process of the build, which can be monitored using gradle. TaskGraph.

Example: This method is often used to assign different values to certain variables. ( gradlew -q greetD )

def versionD = '0.0'
task greetD {
    doLast {
        println "i am greet D, versionD is $versionD"}}// Task DAG graphs are generated during configuration
// The task is ready
gradle.taskGraph.whenReady {taskGraph ->
    if (taskGraph.hasTask('greetC')) {
        versionD = '1.0'
    } else {
        versionD = '1.0-alpha'}}// Before the task is executed
gradle.taskGraph.beforeTask { Task task ->
    println "executing $task ..."
}
// After the task is executed
gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (state.failure) {
        println "FAILED"
    } else {
        println "$task done"}}Copy the code

We can customize specific tasks by listening for callbacks to DAG diagrams.

Since tasks are maintained by DAG diagram, there must be dependencies and execution order between tasks. Can we add dependencies or execution order to tasks when defining tasks? DependsOn, mustRunAfter, etc.

  • DependsOn: Assign a task to a dependsOn task.

    task dependsA { // Define a base Task
        doLast {
            println 'i am depends A task'}}// When B is executed, its dependent task A is executed first
    task dependsB {
        dependsOn dependsA // Set by method
        doLast {
            println 'i am depends B task'}}// Rely on task A with the Map parameter
    task dependsC(dependsOn: dependsA) {
        doLast {
            println 'i am depends C task'}}// Task D relies on task E
    // After task E is defined
    task dependsD {
        dependsOn 'dependsE'
        doLast {
            println 'i am depends D task'
        }
    }
    
    task dependsE {
        doLast {
            println 'i am depends E task'
        }
    }
    
    task dependsF {
        doLast {
            println 'i am depends F task'}}DependsOn on task E and task A
    dependsF.dependsOn dependsE, dependsA
    Copy the code

    As you can see from the example, a dependsOn task is executed first when the task is executed. In addition, multiple dependent tasks can be set for a task at the same time. Lazy dependencies are also possible, where you rely on tasks that have not yet been defined.

  • FinalizedBy: Sets the end task for a task.

    task taskX { // Define task X
        doLast {
            println 'i am task X'
        }
    }
    
    task taskY { // Define task Y
        doLast {
            println 'i am task Y'
        }
    }
    
    task taskZ { // Define task Z
        doLast {
            println 'i am task Z'}}// Execute tasks Y and Z immediately after task X is executed
    taskX.finalizedBy taskY, taskZ
    
    task taskM { // Define task M
        doLast {
            println 'i am task M'
        }
    }
    
    task taskN {
        finalizedBy taskM // Set task M to the final task of task N
        doLast {
            println 'i am task N'}}Copy the code

    FinalizedBy configuration for a task is similar to dependsOn, but has the opposite effect. After a task is executed, the terminating task is executed.

  • MustRunAfter: If tasKB. mustRunAfter(taskA), taskB must be executed after taskA. This rule is strict.

    task taskA {
        doLast {
            println 'i am task A'
        }
    }
    
    task taskB {
        doLast {
            println 'i am task B'}}Task A must be executed after task B
    taskA.mustRunAfter taskB
    Copy the code

    Run gradlew taskA taskB and you will see that taskB is executed first.

  • ShouldRunAfter: If tasKb.shouldrunAfter (taskA) then taskB should be executed after taskA, but this is not required and the tasks may not be executed in the preset order.

    task shouldTaskC << {
        println 'i am task C'
    }
    
    task shouldTaskD << {
        println 'i am task D'
    }
    
    shouldTaskC.shouldRunAfter shouldTaskD
    Copy the code

    Run the gradlew shouldTaskC shouldTaskD command to check the result.

    Here’s an example of a shouldRunAfter failure:

    task shouldTaskX {
        doLast {
            println 'taskX'
        }
    }
    task shouldTaskY {
        doLast {
            println 'taskY'
        }
    }
    task shouldTaskZ {
        doLast {
            println 'taskZ'
        }
    }
    shouldTaskX.dependsOn shouldTaskY
    shouldTaskY.dependsOn shouldTaskZ
    shouldTaskZ.shouldRunAfter shouldTaskX // This is actually invalid
    Copy the code

    Run gradlew -q shouldTaskX to see that task Z is executed before task X.

Here’s another whenTaskAdded method for Tasks. This callback is triggered if a task is added to project during the build process. We can listen to this callback to configure some task dependencies or modify some variables, as shown in the following example (gradlew HelloSecond).

// Define task greetE
tasks.create('greetE') {
    doLast {
        println 'i am greetE'}}// This callback is triggered when tasks are added during the build process and is often used to configure some task dependencies or assignments
// As tested, this callback is valid only for tasks in the plug-in
project.tasks.whenTaskAdded { task ->
    println "task ${task.name} add"
    if (task.name == 'HelloSecond') { // When executing the HelloSecond task, greetE is executed first
        task.dependsOn 'greetE'}}// Define a custom plug-in
class SecondPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println 'Hello Second Gradle Plugin'
        // Add a task to project
        project.task('HelloSecond', {
            println '===== SecondPlugin HelloSecond Task ====='
            logger.quiet('hello')
        })
    }
}

apply plugin: SecondPlugin
Copy the code

This callback only applies to tasks defined in the plug-in (plug-ins will be covered in a future article). In the example, a greetE task dependency is added to the HelloSecond task in the plug-in. GreetE is therefore executed when HelloSecond is executed.

Skip the Task

You may have a requirement that certain tasks are forbidden to be performed or that certain conditions are met. Gradle provides several ways to skip tasks.

Method 1: Each task has a Enabled attribute to enable or disable the task. The default value is True, indicating that the task is enabled. 2. To be SKIPPED. 3. To be deflected. The task is deflected or SKIPPED.

// Run with gradlew disableMe
// Output: Task :app:disableMe deflector
task disableMe {
    doLast {
        println 'This should not be printed if the task is disabled.'
    }
}
disableMe.enabled = false // Disable this task
Copy the code

Method 2: Use the onlyIf method. The task can be executed onlyIf true is returned in onlyIf.

// Run with gradlew sayBye -pskipSaybye
// add a parameter
task sayBye {
    doLast {
        println 'i am sayBye task'}}// The task can be executed only if there is no skipSayBye attribute in the projectsayBye.onlyIf { ! project.hasProperty('skipSayBye')}Copy the code

Method 3: Use StopExecutionException. If a task throws this exception, Gradle skips the task and moves on to the next task.

// Run gradlew nextTask
task byeTask {
    doLast {
        println 'We are doing the byeTask.'}}// Does not affect the execution of subsequent tasks
byeTask.doFirst {
    // Here you would put arbitrary conditions in real life.
    // But this is used in an integration test so we want defined behavior.
    if (true) { throw new StopExecutionException() }
}
task nextTask {
    dependsOn('byeTask') // byeTask is executed first, and the byeTask is interrupted unexpectedly
    doLast { // Does not affect the execution of nextTask
        println 'I am not affected'}}Copy the code

Method 4: Use the timeout attribute of the Task to limit the execution time of the Task. Once a task times out, its execution is interrupted and the task is marked as failing. Gradle has built-in tasks that respond to timeouts.

// Intentionally timed out
task hangingTask() {
    doLast {
        Thread.sleep(100000)
    }
    timeout = Duration.ofMillis(500)}Copy the code

Although there are four methods, methods 1 and 2 are commonly used.

Task rules

If we are about to execute a task that does not exist, Gradle will raise an exception telling us that the task cannot be found. In fact, we can do custom processing by adding rules. This requires using the addRule method of TaskContainer.

// The first argument is a description of the rule
// The second closure argument is the action to be performed by the rule
tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')}}}}Copy the code

The example adds a rule that if the running task starts with ping, the task (which did not exist before it was run) is created and doLast is assigned. This can be done using gradlew pingServer1.

We can use rules not only from the command line, but also create dependsOn relationships on rules-based tasks.

// gradlew groupPing
task groupPing {
    dependsOn pingServer1, pingServer2
}
Copy the code

Life cycle task

Lifecycle tasks are tasks that cannot be completed by themselves, and they usually do not have any execution actions. These tasks are mainly reflected in:

  • Workflow tasks such as: check;
  • A buildable thing, such as debug32MainExecutable;
  • Convenience tasks that execute multiple logical tasks such as compileAll.

Unless a lifecycle task has an operation, its outcome is determined by its dependencies. If any of the task’s dependencies are executed, the lifecycle task is considered executed. Life cycle tasks are considered to be up to date if all tasks’ dependencies are up to date, skipped, or from the cache.

conclusion

By the end of this article, you have learned how to create and use tasks. Tasks are important as the main execution skeleton of Gradle. You can flexibly configure and adjust the dependencies, execution order, and execution rules of tasks through various attributes and methods of tasks. Write a few more examples to help you understand how Gradle works and to lay the foundation for future custom plug-ins.

The resources

  1. Gradle official documentation
  2. The Definitive Guide to Android Gradle
  3. Gradle for Android