banner
jzman

jzman

Coding、思考、自觉。
github

Gradle Series: Gradle Tasks

The previous articles covered the basics of Gradle build tasks and introduced the concepts of Project and Task. It is recommended to read the earlier articles:

The Gradle build process is accomplished through a series of Tasks. This article will provide a detailed introduction to Tasks, covering the following main topics:

  1. Various ways to create tasks
  2. Various ways to access tasks
  3. Task grouping and description
  4. Operators
  5. Task execution analysis
  6. Task ordering
  7. Enabling and disabling tasks
  8. Task's onlyIf assertion
  9. Task rules

Various Ways to Create Tasks#

In Gradle, tasks can be created in various ways. The different methods of creating tasks are ultimately reflected in the shortcut methods provided by Project and the create method offered by the built-in TaskContainer. Below are some common ways to create tasks:

/**
 * First way to create a task:
 * Method prototype: Task task(String name) throws InvalidUserDataException;
 */
// Define a Task variable to receive the Task created by the task() method and configure the created Task
def Task taskA = task(taskA)
// Configure the created Task
taskA.doFirst {
    println "First way to create a task"
}

/**task
 * Second way to create a task: relevant configurations can be made in the Map parameters, such as dependencies, task description, group, etc.
 * Method prototype: Task task(Map<String, ?> args, String name) throws InvalidUserDataException;
 */
def Task taskB = task(group: BasePlugin.BUILD_GROUP, taskB, description: "Description")
// Configure the created Task
taskB.doLast {
    println "Second way to create a task"
    println "Task taskB group: ${taskB.group}"
    println "Task taskB description: ${taskB.description}"
}

/**
 * Third way to create a task: create a Task using a closure, where the delegate object in the closure is the Task, allowing all properties and methods of the Task to be called for configuration
 * Method prototype: Task task(String name, Closure configureClosure);
 */
task taskC {
    description 'Description of taskC'
    group BasePlugin.BUILD_GROUP
    doFirst {
        println "Third way to create a task"
        println "Task taskC group: ${group}"
        println "Task taskC description: ${description}"
    }
}

/**
 * Fourth way to create a task: flexible configuration can be done in the closure, and configurations in the closure will override the same configurations in the Map parameters
 * Method prototype: Task task(Map<String, ?> args, String name, Closure configureClosure);
 */
def Task taskD = task(group: BasePlugin.BUILD_GROUP, taskD, description: "Description") {
    description 'Description of taskD'
    group BasePlugin.UPLOAD_GROUP
    doFirst {
        println "Fourth way to create a task"
        println "Task taskD group: ${group}"
        println "Task taskD description: ${description}"
    }
}

The above are four ways to create tasks; choose the appropriate method when using them. The Map mentioned above can be used to configure relevant parameters for the Task, as shown below:

type: Create based on an existing Task, similar to class inheritance, default value is DefaultTask
overwrite: Whether to replace an existing Task, generally used in conjunction with type, default value is false
dependsOn: Configure the dependencies of the current task, default value is []
action: An Action or a closure added to the task, default value is null
description: Task description, default value is null
group: Task group, default value is null

Creating a Task using a closure allows the delegate object in the closure to be the Task, enabling the invocation of all properties and methods of the Task for configuration. This closure-based task creation method is more flexible. Additionally, tasks can also be created using TaskContainer, as shown below:

// Creating a task using TaskContainer
tasks.create("taskE") {
    description 'Description of taskE'
    group BasePlugin.BUILD_GROUP
    doFirst {
        println "Third way to create a task"
        println "Task taskE group: ${group}"
        println "Task taskE description: ${description}"
    }
}

The tasks property is an attribute of Project, and its type is TaskContainer, so tasks can be created through tasks. Of course, TaskContainer also has other constructors for creating tasks. This concludes the basic introduction to task creation.

Various Ways to Access Tasks#

The created tasks (Task) belong to a property of the project (Project), and the property name is the task name. The type of this property is Task. If the task name is known, the task can be accessed and manipulated directly by its name, which can also be understood as accessing and manipulating the corresponding Task object. Refer to the following:

/**
 * First way to access a task: Task name.doLast{}
 */
task taskF {

}
taskF.doLast {
    println "First way to access a task"
}

Tasks are created through the create method of TaskContainer, and TaskContainer is a collection of tasks. In Project, the tasks property can be used to access TaskContainer, and the type of tasks is TaskContainer. Therefore, for already created tasks, their properties and methods can be accessed through element access, as shown in the following code:

/**
 * Second way to access a task: Use TaskContainer to access the task
 */
task taskG {

}
tasks['taskG'].doLast {
    println "Second way to access a task"
}

In Groovy, [] is also an operator. The true meaning of tasks['taskG'] is tasks.getAt('taskG'), and the getAt() method is a method in TaskCollection, allowing access and manipulation of related tasks by their names.

Tasks can also be accessed via path access, which involves two key methods: findByPath and getByPath. The difference is that the former returns null if the specified task is not found, while the latter throws an UnknowTaskException if the task is not found. The code is as follows:

/**
 * Third way to access a task: Use path access to access the task
 */
task taskH {
    println 'taskH'
    // Access the task by path, the parameter can be a path (not accessed successfully, written as follows)
    println tasks.findByPath(':GradleTask:taskG')
    // Access the task by path, the parameter can be the task name
    println tasks.findByPath('taskG')
    println tasks.getByPath('taskG')
}

The execution result of the above code is as follows:

PS E:\Gradle\study\GradleTask> gradle taskH

> Configure project :
taskH
null
task ':taskG'
task ':taskG'


FAILURE: Build failed with an exception.

* Where:
Build file 'E:\Gradle\study\GradleTask\build.gradle' line: 98

* What went wrong:
A problem occurred evaluating root project 'GradleTask'.
> Task with path 'test' not found in root project 'GradleTask'.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

* Get more help at https://help.gradle.org

BUILD FAILED in 2s

The process of accessing the task by path shows that if the parameter is written as a path, the specific task may not be accessed due to writing issues, which hopefully can be resolved in future studies.

Additionally, tasks can be accessed by their names, primarily using findByName and getByName methods. The difference is similar to the third way of access. The code is as follows:

/**
 * Fourth way to access a task: Use the task name to access
 */
task taskJ
tasks['taskJ'].doLast {
    println 'taskJ'
    println tasks.findByName('taskJ')
    println tasks.getByName('taskJ')
}

The above has covered four ways to access tasks. By understanding how to access tasks in Gradle, you can flexibly use the methods mentioned above in specific project builds.

Task Grouping and Description#

Task grouping and description have actually been mentioned and configured in previous articles. Here, we will briefly explain that task grouping and description are essentially configurations for grouping and describing already created tasks, as shown below:

// Task grouping and description
def Task task1 = task taskK
task1.group = BasePlugin.BUILD_GROUP
task1.description = 'Testing task grouping and description'
task1.doLast {
    println "taskK is group = ${group}, description = ${description}"
}

Below is the execution result of the above code:

PS E:\Gradle\study\GradleTask> gradle taskK

> Task :taskK
taskK is group = build, description = Testing task grouping and description


BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed

From the execution result, it can be seen that if the relevant properties of the task are configured, all information about the task can be accessed.

Operators#

Learn about an operator <<. In previous test codes, the doLast() method was called for testing, but we can use the << operator to replace the doLast method. This means that the doLast() method can be written as follows:

// << Task operator
// Shortened form, this way has been deprecated since Gradle 5.0
task taskL << {
    println "doLast"
}
// Recommended way
task taskL {
    doLast {
        println "doLast"
    }
}

The execution results of the two writing methods are as follows:

PS E:\Gradle\study\GradleTask> gradle taskL

> Configure project :
The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead.
        at build_6syzx8ks0l09hby4j6yn247u9.run(E:\Gradle\study\GradleTask\build.gradle:123)

> Task :taskL
doLast


BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
PS E:\Gradle\study\GradleTask> gradle taskL

> Task :taskL
doLast


BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
PS E:\Gradle\study\GradleTask>

From the output results, both writing methods produced the desired results. Additionally, the logs indicate that this shorthand method has been deprecated since Gradle 5.0, so it is recommended to use the doLast method for task configuration.

Task Execution Analysis#

During the execution of Gradle tasks, we can configure task-related settings before or after task execution using doFirst and doLast. When we execute a task, we are actually executing the actions owned by that Task. We can use the getActions() method to retrieve all executable actions. Below, we define a custom Task type CustomTask and use the @TaskAction annotation to mark the method doSelf, indicating the method that the Task itself will execute. The code is as follows:

// Task execution process analysis
def Task taskA = task taskB(type: CustomTask)
taskA.doFirst {
    println "Called before Task execution: doFirst"
}

taskA.doLast {
    println "Called after Task execution: doLast"
}

class CustomTask extends DefaultTask {
    @TaskAction
    def doSelf() {
        println "Called by the Task itself: doSelf"
    }
}

The execution result of the above code is as follows:

PS E:\Gradle\study\GradleTask2> gradle taskB

> Task :taskB
Called before Task execution: doFirst
Called by the Task itself: doSelf
Called after Task execution: doLast


BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed

Since the execution of a Task involves traversing the list of actions to be executed, to ensure the execution order, the action corresponding to doFirst must be placed at the front of the action list, and the action corresponding to doLast must be placed at the end of the action list, while the action corresponding to doSelf should be placed in the middle of the list to guarantee the correct execution order. Below is the pseudocode:

// When creating a task, the method marked with @TaskAction is added as the Task's own execution Task
// At this time, the actionList contains only the Task's own execution Action
actionList.add(0, doSelfAction)
// After the task is created, if doFirst is set, the corresponding action will be added to the front of the actionList
// At this time, the action corresponding to doFirst is added to the front of the actionList to ensure it executes before the task starts
actionList.add(0, doFirstAction)
// After the task is created, if doLast is set, the corresponding action will be added to the end of the actionList to ensure it executes after the task starts
actionList.add(doLastAction)

The task execution process is generally as described above, and it is advisable to experience it in practical applications.

Task Ordering#

In Gradle, task ordering uses two methods of Task: shouldRunAfter and mustRunAfter, which can conveniently control which of the two tasks should execute first:

/**
 * Task order
 * taskC.shouldRunAfter(taskD): Indicates that taskC should execute after taskD
 * taskC.mustRunAfter(taskD): Indicates that taskC must execute after taskD
 */
task taskC {
    doFirst {
        println "taskC"
    }
}
task taskD {
    doFirst {
        println "taskD"
    }
}
taskC.shouldRunAfter(taskD)

The execution result of the above code is as follows:

PS E:\Gradle\study\GradleTask2> gradle taskC taskD

> Task :taskD
taskD

> Task :taskC
taskC


BUILD SUCCESSFUL in 2s
2 actionable tasks: 2 executed

Enabling and Disabling Tasks#

The Task has an enabled property, which can be used to enable or disable a task. Setting it to true enables the task, while setting it to false disables it. The default value of this property is true, as shown below:

taskA.enabled = true

Task's onlyIf Assertion#

An assertion is a conditional expression. The Task object has an onlyIf method, which can accept a closure as a parameter. If the parameter in the closure returns true, the task will execute; otherwise, it will not. This allows controlling which tasks need to be executed through the task's assertion. Below is a packaging example to learn about task assertions, as shown in the code:

// Task's onlyIf assertion
final String BUILD_ALL = 'all'
final String BUILD_FIRST = 'first'
final String BUILD_OTHERS = 'others'

task taskTencentRelease {
    doLast {
        println "Packaging for Tencent App Store"
    }
}

task taskBaiduRelease {
    doLast {
        println "Packaging for Baidu Mobile Assistant"
    }
}

task taskMiuiRelease {
    doLast {
        println "Packaging for Xiaomi App Store"
    }
}

task buildTask {
    group BasePlugin.BUILD_GROUP
    description "Packaging for app stores"
}

// Add specific task dependencies to buildTask
buildTask.dependsOn taskTencentRelease, taskBaiduRelease, taskMiuiRelease

taskTencentRelease.onlyIf {
    if (project.hasProperty("buildApp")) {
        Object buildApp = project.property("buildApp")
        return BUILD_ALL == buildApp || BUILD_FIRST == buildApp
    } else {
        return true
    }
}

taskBaiduRelease.onlyIf {
    if (project.hasProperty("buildApp")) {
        Object buildApp = project.property("buildApp")
        return BUILD_ALL == buildApp || BUILD_FIRST == buildApp
    } else {
        return true
    }
}

taskMiuiRelease.onlyIf {
    if (project.hasProperty("buildApp")) {
        Object buildApp = project.property("buildApp")
        return BUILD_OTHERS == buildApp || BUILD_ALL == buildApp
    } else {
        return true
    }
}

Below is the execution result of the above code:

PS E:\Gradle\study\GradleTask2> gradle -PbuildApp=first buildTask

> Task :taskBaiduRelease
Packaging for Baidu Mobile Assistant

> Task :taskTencentRelease
Packaging for Tencent App Store


BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
PS E:\Gradle\study\GradleTask2> gradle -PbuildApp=others buildTask

> Task :taskMiuiRelease
Packaging for Xiaomi App Store


BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
PS E:\Gradle\study\GradleTask2> gradle -PbuildApp=all buildTask

> Task :taskBaiduRelease
Packaging for Baidu Mobile Assistant

> Task :taskMiuiRelease
Packaging for Xiaomi App Store

> Task :taskTencentRelease
Packaging for Tencent App Store


BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed

It can be seen that when executing buildTask, the Project is configured with the property buildApp. By using different values for buildApp, the onlyIf method allows for different packaging strategies to be implemented. This can be referenced and used in actual development.

Additionally, note the command format for executing the above code, as shown below:

// Here, buildApp and the value after = are key-value pairs. When executing tasks, the -P command can be used for shorthand.
// -P is used to specify K-V property pairs for the current Project, i.e., -PK=V
gradle -PbuildApp=others buildTask

Task Rules#

The created tasks are in TaskContainer. We can retrieve the desired task from the tasks property of Project based on the task name. We can add corresponding task rules using the addRule method of TaskContainer, as shown in the following code:

// Task rules
tasks.addRule("A description of this rule") {
    // In the closure, -> is often used as a separator between parameters and code blocks
    String taskName ->
        task(taskName) {
            doLast {
                println "${taskName} does not exist"
            }
        }
}

task taskTest {
    dependsOn taskX
}

The execution result of the above code is as follows:

PS E:\Gradle\study\GradleTask2> gradle taskTest

> Task :taskX
taskX does not exist


BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

If no special handling is specified for certain tasks, an error will be reported. If handled, a relevant prompt message will be output. This concludes the understanding and learning of Gradle tasks.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.