banner
jzman

jzman

Coding、思考、自觉。
github

Gradle Series: Gradle Tasks

In the previous articles, we learned the basics of Gradle build tasks and understood the concepts of Project and Task. It is recommended to read the earlier articles:

Gradle's build work is accomplished through a series of Tasks. This article will provide a detailed introduction to Tasks, covering the following main content:

  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, there are multiple ways to create tasks. The various methods for creating tasks are ultimately reflected in the shortcut methods provided by the Project and the create method offered by the built-in TaskContainer. Here are a few common ways to create tasks:

/**
 * First way to create a task:
 * Method prototype: Task task(String name) throws InvalidUserDataException;
 */
// Define Task variable to receive the Task created by task() method, 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 shows four ways to create tasks; you can choose the appropriate method when using them. The Map mentioned above can be used to configure relevant parameters for the Task, as follows:

type: Create based on an existing Task, similar to class inheritance, default value DefaultTask
overwrite: Whether to replace an existing Task, generally used in conjunction with type, default value false
dependsOn: Configure dependencies for the current task, default value []
action: An Action or a closure added to the task, default value null
description: Task description, default value null
group: Task group, default value 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 method of task creation is more flexible. Additionally, you can create tasks 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 it. 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 an attribute of the project (Project), and the attribute name is the task name. The type of this attribute is Task. If the task name is known, it can be accessed and manipulated directly by the task name. This can also be understood as accessing and manipulating the Task object corresponding to that task, as shown below:

/**
 * 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 geometric 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. This allows access and manipulation of related tasks by task name.

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'
    // Accessing the task by path, the parameter can be a path (not accessed successfully, written as follows)
    println tasks.findByPath(':GradleTask:taskG')
    // Accessing the task by path, the parameter can be the task name
    println tasks.findByPath('taskG')
    println tasks.getByPath('taskG')
}

The execution results of the above code are 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

In the process of using path access to tasks, if the parameters are written as paths and do not access specific tasks, it may be a writing issue that we hope to resolve in future studies.

Additionally, tasks can be accessed by task name, primarily using findByName and getByName methods. The difference is the same as the third way of access. The code is as follows:

/**
 * Fourth way to access a task: use 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 above methods in specific project builds.

Task Grouping and Description#

Task grouping and description have actually been mentioned and configured in previous articles. Here is a brief explanation: 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 = 'Test task grouping and description'
task1.doLast {
    println "taskK is group = ${group}, description = ${description}"
}

The execution result of the above code is as follows:

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

> Task :taskK
taskK is group = build, description = Test 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, to test, we would call the task.doLast() method. We can use the << operator to replace the doLast method, meaning that the doLast() method can be written as follows:

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

The execution results of the above 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, it can be seen that both writing methods output 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 use doFirst and doLast to configure tasks before or after execution. 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 Task type CustomTask and use the @TaskAction annotation to mark the method doSelf, indicating the method that the Task itself should execute. The code is as follows:

// Task execution flow 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 "Task execution itself called: 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
Task execution itself called: doSelf
Called after Task execution: doLast


BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed

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

// When creating the task, use the method marked with @TaskAction as the Task's own execution task
// At this point, the actionList contains only the Action for the Task itself
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 point, 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, ensuring it executes after the task starts
actionList.add(doLastAction)

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

Task Ordering#

In Gradle, task ordering uses two methods of Task: shouldRunAfter and mustRunAfter, which can conveniently control which task executes 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 that can be used to enable or disable a task. Setting it to true enables the task, while setting it to false disables it. This property defaults to true, as shown below:

taskA.enabled = true

Task's onlyIf Assertion#

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

// 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 tasks"
}

// Add specific tasks that buildTask depends on
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
    }
}

The execution results of the above code are as follows:

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

As can be seen, when we execute buildTask and configure the property buildApp for the Project, different values for buildApp allow for different packaging strategies through the use of onlyIf, which can be referenced and utilized in actual development.

Additionally, note the writing of the execution command for the above code, as follows:

// Where buildApp and the value after = 'others' are key-value pairs. When executing tasks, you can use the shorthand -P command.
// -P specifies a K-V property key-value pair for the current Project, i.e., -PK=V
gradle -PbuildApp=others buildTask

Task Rules#

The created tasks are all in TaskContainer. We can retrieve the desired task by its name from the tasks property of Project. 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, it is common to use -> as a separator between parameters and the code block
    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 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.