[Gradle-7] Gradle Build Core Task Guide

1 Introduction

This article is the seventh article of the Gradle series , which brings you Taskthe knowledge points related to the core of Gradle construction.

2. What is the Task in Gradle

Taskis a task and is Gradlethe smallest of the 构建单元.
The core of Gradle's construction is a directed acyclic graph composed of Task:
image.png
Task is the core object of Gradle's construction. Task can receive input, perform certain operations, and generate output according to the executed operations.

Task manages a ActionList, you can insert an Action (doFirst) in front of the List, or insert (doLast) from the back of the List, Action is the smallest 执行单元.

3. How to create a task

3.1 Where is the Task written?

First think about where is the Task written?

We introduced in the fourth life cycle of the Gradle series that there are three stages. The first stage initialization will determine which projects participate in the compilation. The second stage is to parse the configuration and generate the Task Registry (DAG). The third stage The stage is to execute the Task in sequence.

In reverse, the execution of a Task requires a Task registry, and the source of the Task needs to determine which projects participate in the compilation, that is to say, the Task registry is determined by the projects participating in the compilation, that is, it can be understood that the Task is determined by the object, Projectso Task is created in Project, a build.gradle file corresponds to a Project object, so we can create Task directly in build.gradle file.

As long as the running context is in the Project .

3.2. Create Task

To create a Task, you need to use the register method of TaskContainer.

Several ways to register:

  1. register(String name, Action<? super Task> configurationAction)
  2. register(String name, Class type, Action<? super T> configurationAction)
  3. register(String name, Class type)
  4. register(String name, Class type, Object… constructorArgs)
  5. register(String name)

1 and 2 are more commonly used.

  • configurationAction refers to Action, that is, the operation of the Task, which will be executed at compile time;
  • The type type refers to the Task type, which can be a custom type, or you can specify the built-in Copy, Delete, Zip, Jar and other types;

We can create a task directly in the build.gradle file:

tasks.register("yechaoa") {
    println("Task Name = " + it.name)
}

The above call is the method 1 of register. If the last parameter is a closure, it can be written outside the parameter.

We created the above task through TaskContainer (tasks). The method of creating Task is also provided in the Project object. There is a little difference in the way of writing:

task("yechaoa") {
    println "aaa"
}

Task can also be created through Plugin, and the rewritten apply method will have a Project object.

4. How to execute Task

4.1, execute a single Task

Order:

./gradlew taskname

Example:

tasks.register("yechaoa") {
    println("Task Name = " + it.name)
}

implement:

./gradlew yechaoa

output:

Task Name = yechaoa

4.2, Execute multiple Tasks

./gradlew taskname taskname taskname

Tasks are separated by spaces.

4.3, Task with the same name

If there are two Tasks with the same name, the compilation will fail, that is, InvalidUserDataException

* What went wrong:
A problem occurred evaluating project ':app'.
> Cannot add task 'yechaoa' as a task with that name already exists.

4.4, Task execution results

We often see a label behind the Task at compile time, which indicates the execution result of the Task.

> Task :app:createDebugVariantModel UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig UP-TO-DATE
.....

4.4.1、EXCUTED

Indicates Task execution, common.

4.4.2、UP-TO-DATE

> Task :app:preBuild UP-TO-DATE

It indicates that the output of the Task has not changed.

There are several situations:

  1. Neither input nor output has changed;
  2. The output has not changed;
  3. Task has no operation and has dependencies, but the content of dependencies is the latest, or skipped, or reused;
  4. Task has no operations and no dependencies;

4.4.3、FOME-CACHE

Literally, it means that the last execution result can be reused from the cache.

4.4.4、SKIPPED

Literally, it means to skip.

For example is excluded:

$ gradle dist --exclude-task yechaoa

4.4.5、NO-SOURCE

Task does not need to be executed. There are inputs and outputs, but no sources.

5, Task Action

5.1、Action

The Action of Task is the operation required at compile time, it is not one, it is a group, that is, there can be multiple.

Multiple Tasks are generally used when we customize.

5.1.1, Custom Task

class YechaoaTask extends DefaultTask {

    @Internal
    def taskName = "default"

    @TaskAction
    def MyAction1() {
        println("$taskName -- MyAction1")
    }

    @TaskAction
    def MyAction2() {
        println("$taskName -- MyAction2")
    }
}
  • Customize a class that inherits from DefaultTask;
  • The Action method needs to be @TaskActionannotated;
  • Externally exposed parameters need to use @Internalannotations;

Use a custom Task:

tasks.register("yechaoa", YechaoaTask) {
    taskName = "我是传入的Task Name "
}

The type is passed into the custom Task class.

Results of the:

> Task :app:yechaoa
我是传入的Task Name  -- MyAction1
我是传入的Task Name  -- MyAction2

If the constructor of the Action method passes parameters, the parameters can be written after the type type:

tasks.register('yechaoa', YechaoaTask, 'xxx')

5.2、doFirst

It is a kind of Action and is executed at the head of Task Action. There can be more than one.

5.3、doLast

It is a kind of Action and is executed at the end of Task Action. There can be more than one.

Sample code:

tasks.register("yechaoa") {
    it.doFirst {
        println("${it.name} = doFirst 111")
    }
    it.doFirst {
        println("${it.name} = doFirst 222")
    }

    println("Task Name = " + it.name)

    it.doLast {
        println("${it.name} = doLast 111")
    }
    it.doLast {
        println("${it.name} = doLast 222")
    }
}

Results of the:

Task Name = yechaoa

> Task :app:yechaoa
yechaoa = doFirst 222
yechaoa = doFirst 111
yechaoa = doLast 111
yechaoa = doLast 222

The output of Task Name is in the configuration phase of the Gradle life cycle, because it is under the closure, not in any Action, and there is no execution opportunity. When the configuration phase resolves to this Task, println will be executed.

Other outputs are under Task :app:yechaoa, because there is a clear timing for Action execution.

5.4, ​​Action execution sequence

The execution order of Action is also a misunderstanding of most people. Through the above log, let's sort it out:

  • doFirst: reverse order
  • Action: normal order
  • doLast: positive sequence

6. Task attribute

The attributes of Task are as follows:

    String TASK_NAME = "name";

    String TASK_DESCRIPTION = "description";

    String TASK_GROUP = "group";

    String TASK_TYPE = "type";

    String TASK_DEPENDS_ON = "dependsOn";

    String TASK_OVERWRITE = "overwrite";

    String TASK_ACTION = "action";

Attribute configuration is easier to understand, as the name suggests.

If your IDEA has Gradle visual management, such as Android Studio, then you can find our custom Task in the Gradle panel menu on the right, and double-click to execute it.
Screenshot 2023-06-18 00.26.56.png

7. Task dependency

Gradle already has a set of task building process by default. If you want to add a custom task to this process or do some aspect programming before and after a built-in task, you need to understand the dependencies of the task.

7.1、dependsOn

tasks.register("yechaoa111") {
    it.configure {
        dependsOn(provider {
            tasks.findAll {
                it.name.contains("yechaoa222")
            }
        })
    }

    it.doLast {
        println("${it.name}")
    }
}

tasks.register("yechaoa222") {
    it.doLast {
        println("${it.name}")
    }
}

implement:

./gradlew yechaoa111

output:

> Task :app:yechaoa222
yechaoa222

> Task :app:yechaoa111
yechaoa111

The Task yechaoa111 that defines dependsOn is executed after the target Task yechaoa222.

In fact, compared to the above, the more common way of writing dependsOn is this:

def yechaoa111 = tasks.register("yechaoa111") {
    it.doLast {
        println("${it.name}")
    }
}

def yechaoa222 = tasks.register("yechaoa222") {
    it.doLast {
        println("${it.name}")
    }
}

yechaoa111.configure {
    dependsOn yechaoa222
}

The task that dependsOn depends on can be a name or a path

It can also be a type type:

dependsOn tasks.withType(Copy)

If it is a task of another project (Project), it can also:

dependsOn "project-lib:yechaoa"

7.2、finalizedBy

Add the specified finalizer task to Task. That is to specify the next task to be executed, and dependsOn specifies the previous one.

task taskY {
    finalizedBy "taskX"
}

Here it means that taskX is executed after taskY is executed.

If finalizedBy is replaced with dependsOn, it means that taskX must be executed before taskY is executed.

7.3、mustRunAfter

def yechaoa111 = tasks.register("yechaoa111") {
    it.doLast {
        println("${it.name}")
    }
}

def yechaoa222 = tasks.register("yechaoa222") {
    it.doLast {
        println("${it.name}")
    }
}

yechaoa111.configure {
    mustRunAfter yechaoa222
}

implement:

./gradlew yechaoa111

output:

> Task :app:yechaoa111
yechaoa111

It can be seen that yechaoa222 has not been executed, because we executed yechaoa111 separately, and we must execute it together to see the order of dependencies.

Let's execute it together again:

./gradlew yechaoa111 yechaoa222

output:

> Task :app:yechaoa222
yechaoa222

> Task :app:yechaoa111
yechaoa111

It can be seen that mustRunAfter takes effect, and yechaoa111 is executed after yechaoa222.

7.4、shouldRunAfter

yechaoa111.configure {
    shouldRunAfter yechaoa222
}

shouldRunAfter is written in the same way as mustRunAfter.

mustRunAfter is "must run", shouldRunAfter is "should run".
Such as taskB.mustRunAfter(taskA), when taskA and taskB run at the same time, taskB must always run after taskA.
The shouldRunAfter rule is similar, but not the same, because it is ignored in both cases. First, if using that rule would introduce a sort cycle; second, when using parallel execution, all dependencies of a task are satisfied except for the "should run" task, then regardless of whether its "should run" dependencies have run, will run this task.

8. Skip Task

Gradle provides multiple ways to skip Task.

  • conditional skip
  • exception skip
  • disable skip
  • timeout skip

8.1, conditional skip

Gradle provides onlyIf(Closure onlyIfClosure)a method to execute the Task only when the result of the closure returns True.

tasks.register("skipTask") { taskObj ->
    taskObj.configure {
        onlyIf {
            def provider = providers.gradleProperty("yechaoa")
            provider.present
        }
    }

    taskObj.doLast {
        println("${it.name} is Executed")
    }
}

implement:

./gradlew skipTask -Pyechaoa

output:

> Task :app:skipTask
skipTask is Executed

The skipTask will only be executed if parameters are added to the script command -Pyechaoa.

By analogy, as long as the result of the onlyIf closure is True, the conditions are self-determined.

8.2, Exception skipping

If onlyIf does not meet the requirements, it can also be used StopExecutionExceptionto skip.

StopExecutionException is an exception. When an exception is thrown, the current Action and subsequent Actions will be skipped, that is, the current Task will be skipped to execute the next Task.

tasks.register("skipTask") { taskObj ->

    taskObj.doFirst {
        println("${it.name} is Executed doFirst")
    }

    taskObj.doLast {
        def provider = providers.gradleProperty("yechaoa")
        if (provider.present) {
            throw new StopExecutionException()
        }

        println("${it.name} is Executed doLast")
    }
}

output:

> Task :app:skipTask
skipTask is Executed doFirst

If parameters are added to the script command -Pyechaoa, an exception will be thrown and skipped.

But the previous Action in the Task will be executed, such as doFirst in the example.

8.3, Disable Skip

Each Task has a enabledswitch, true is enabled, false is disabled, and no operation will be executed after being disabled.

tasks.register("skipTask") { taskObj ->
    taskObj.configure {
        enabled = true
    }

    taskObj.doLast {
        println("${it.name} is Executed")
    }
}

8.4, timeout skip

Task provides timeoutproperties for limiting execution time.

If the Task's running time exceeds the specified time, the thread executing the Task will be interrupted.

By default, tasks never time out.

Example:

tasks.register("skipTask") { taskObj ->
    taskObj.configure {
        timeout = Duration.ofSeconds(10)
    }

    taskObj.doLast {
        Thread.sleep(11 * 1000)
        println("${it.name} is Executed")
    }
}

output:

> Task :app:skipTask FAILED
Requesting stop of task ':app:skipTask' as it has exceeded its configured timeout of 10s.
---Gradle:buildFinished 构建结束了

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:skipTask'.
> Timeout has been exceeded

The execution is abnormal, and it prompts timeout of 10s, because I sleep for 11s in doLast.

9. Task incremental construction

增量构建When the input and output of the Task do not change, the execution of the action is skipped. When the input or output of the Task changes, only the changed input or output is processed in the action, so that a Task that has not changed is avoided. Build repeatedly, and only process the changed part when the Task changes, which can improve the construction efficiency of Gradle and shorten the construction time.

An important part of any build tool is the ability to avoid doing work that's already been done. After compiling source files, they do not need to be recompiled unless something changes that affects the output, such as modifying the source file or deleting an output file. Compiling can take a lot of time, so skipping steps when not needed can save a lot of time.

Gradle provides this out-of-the-box incremental build functionality, when you compile, Task is marked as in the console output UP-TO-DATE, which means the incremental build is working.

Let's take a look at how incremental builds work and how to ensure that Tasks support incremental runs.

9.1. Input and output

In general, tasks require some input and generate some output. We can regard the Java compilation process as an example of Task, the Java source file is used as the input of the Task, and the generated class file, that is, the result of compilation, is the output of the Task.
image.png
An important characteristic of an input is that it affects one or more outputs, as shown above, different bytecodes are generated depending on the content of the source file and the minimum version of the Java runtime you want to run the code on.

When writing a Task, you need to tell Gradle which Task properties are input and which are output. If a Task property affects an output, be sure to register it as an input, otherwise the Task will be considered up-to-date when it is not. Conversely, if the property does not affect the output, do not register the property as an input, otherwise the Task may be executed when it is not needed. Also beware of non-deterministic tasks that may generate different outputs for the exact same input, these tasks should not be configured for incremental builds, as the UP-TO-DATE check will not work.

The above two paragraphs of theory are taken from the official website. For novices, it may be a bit difficult to understand. The following will take you to practice it.

9.2. Two forms of incremental construction

  • The first one, Task can be reused completely, and there is no change in input and output, that is, UP-TO-DATE;
  • The second type has some changes, and only needs to operate on the changed part;

9.3, case practice

Scenario: Write a task that copies files and supports incremental builds.

class CopyTask extends DefaultTask {

    // 指定输入
    @InputFiles
    FileCollection from

    // 指定输出
    @OutputDirectory
    Directory to

    // task action 执行
    @TaskAction
    def execute() {
        File file = from.getSingleFile()
        if (file.isDirectory()) {
            from.getAsFileTree().each {
                copyFileToDir(it, to)
            }
        } else {
            copyFileToDir(from, to)
        }
    }

    /**
     * 复制文件到文件夹
     * @param src 要复制的文件
     * @param dir 接收的文件夹
     * @return
     */
    private static def copyFileToDir(File src, Directory dir) {
        File dest = new File("${dir.getAsFile().path}/${src.name}")

        if (!dest.exists()) {
            dest.createNewFile()
        }

        dest.withOutputStream {
            it.write(new FileInputStream(src).getBytes())
        }
    }

}

When writing a Task, we need to use annotations to declare the input and output. @InputXXXmeans input, @OutputXXXmeans output.

  • The above code fromis our input, that is, the file to be copied;
  • tois our output, the folder to receive;
  • Then execute()the method is the Action executed by the Task.

Let's see how to use

tasks.register("CopyTask", CopyTask) {
    from = files("from")
    to = layout.projectDirectory.dir("to")
}

Before execution, create data, add a from folder in the app directory, and add a txt1.txt file under it

├── app
│   ├── from
│   │   └── txt1.txt

implement:

./gradlew CopyTask

The txt1.txt file has been copied to the to folder.
WX20230619-011700.png
The directory structure at this time:

├── app
│   ├── from
│   │   └── txt1.txt
│   └── to
│       └── txt1.txt

9.3.1、UP-TO-DATE

The log just executed:

➜  GradleX git:(master) ✗ ./gradlew CopyTask

...

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

To verify the incremental build, execute again:

➜  GradleX git:(master) ✗ ./gradlew CopyTask

...

BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date

This time we can find that the execution result of the Task has executedchanged from up-to-dateto , indicating that our incremental build has taken effect.

Although the incremental build has taken effect at this time, the degree of completion is not enough, and finer-grained processing is needed, so let's see.

9.3.2. Incremental build

Above we talked about the two forms of incremental construction, and up-to-date has been implemented, and now we need to simulate the input/output 部分变化scenario.

Scenario: Based on the above scenario, add a txt2.txt file under the from folder, and support incremental builds.

When adding a txt2.txt file and executing the above command again, you will find that the txt1.txt file has been copied again.

This is because our input has changed, and the Action of CopyTask will be fully constructed, and the effect we want is to only copy the txt2.txt file. Only the newly added or modified files are copied, and the unchanged files are not copied.

To achieve this effect, the Action method must be supported 增量构建.

We need to add a InputChangesparameter to the Action method. The Action method with the InputChanges type parameter indicates that this is an incremental task operation method. This parameter tells Gradle that the Action method only needs to process the changed input. In addition, the Task also needs to use @Incrementalor @SkipWhenEmptyto Specify at least one delta file input attribute.

class CopyTask extends DefaultTask {

    // 指定增量输入属性
    @Incremental
    // 指定输入
    @InputFiles
    FileCollection from

    // 指定输出
    @OutputDirectory
    Directory to

    // task action 执行
    @TaskAction
    void execute(InputChanges inputChanges) {

        boolean incremental = inputChanges.incremental
        println("isIncremental = $incremental")

        inputChanges.getFileChanges(from).each {
            if (it.fileType != FileType.DIRECTORY) {
                ChangeType changeType = it.changeType
                String fileName = it.file.name
                println("ChangeType = $changeType , ChangeFile = $fileName")

                if (changeType != ChangeType.REMOVED) {
                    copyFileToDir(it.file, to)
                }
            }
        }
        
    }

    /**
     * 复制文件到文件夹
     * @param src 要复制的文件
     * @param dir 接收的文件夹
     * @return
     */
    static def copyFileToDir(File file, Directory dir) {
        File dest = new File("${dir.getAsFile().path}/${file.name}")

        if (!dest.exists()) {
            dest.createNewFile()
        }

        dest.withOutputStream {
            it.write(new FileInputStream(file).getBytes())
        }
    }

}

The changes here are divided into two steps:

  1. Add @Incrementalannotations to the from attribute to indicate incremental input attributes;
  2. InputChangesThe action method execute() has been rewritten, and parameters have been added to support incremental copying of files. Then ChangeType, only newly added or modified files will be copied according to the verification of the files.

Several types of ChangeType:

public enum ChangeType {
    ADDED,
    MODIFIED,
    REMOVED
}
  • ADDED: Indicates that the file is newly added;
  • MODIFIED: Indicates that the file is modified;
  • REMOVED: Indicates that the file is deleted;

Let's execute it first:

./gradlew CopyTask

output:

> Task :app:CopyTask
isIncremental = false
ChangeType = ADDED , ChangeFile = txt1.txt

Execute for the first time, and there is no incremental build, execute it again to see.

BUILD SUCCESSFUL in 2s
1 actionable task: 1 up-to-date

The second direct up-to-date.

9.3.3、ADDED

At this time, we have not verified the incremental build, we add a txt2.txt file to the from folder, and then execute it

> Task :app:CopyTask
isIncremental = true
ChangeType = ADDED , ChangeFile = txt2.txt

It can be seen from the log that the incremental build of the action has taken effect, and it indicates that the txt2.txt is newly added, and the txt1.txt file is not copied again.

The directory structure at this time:

├── app
│   ├── build.gradle
│   ├── from
│   │   └── txt1.txt
│       └── txt2.txt
│   └── to
│       ├── txt1.txt
│       └── txt2.txt

9.3.4、MODIFIED

We can further verify, add a line "yechaoa" in the txt1.txt file to simulate the modification, and execute it again

> Task :app:CopyTask
isIncremental = true
ChangeType = MODIFIED , ChangeFile = txt1.txt

It is still an incremental build, and indicates that the txt1.txt file is modified, and the txt2.txt file is not copied again.

9.3.5、REMOVED

After verifying the deletion, we delete the txt2.txt file in the from folder and execute it to see

> Task :app:CopyTask
isIncremental = true
ChangeType = REMOVED , ChangeFile = txt2.txt

It is still an incremental build, and indicates that the txt2.txt file has been deleted.

The directory structure at this time:

├── app
│   ├── from
│   │   └── txt1.txt
│   └── to
│       ├── txt1.txt
│       └── txt2.txt

It can be found that although we have deleted the txt2.txt file under the from folder, and the Action of the Task does support incremental builds, the txt2.txt file under the to folder is still there. If the contents of the to folder change If it affects your build results, you still need to deal with it to keep it in sync.

9.4. Incremental vs full volume

Not every execution of Task is an incremental build. We can use the isIncremental method of InputChanges to judge whether this build is an incremental build. However, there are the following situations where a full build will be performed:

  • The Task is executed for the first time;
  • The Task has only input but no output;
  • The upToDateWhen condition of the Task returns false;
  • One of the output files of this Task has changed since the last build;
  • Since the last build, an attribute input of the Task has changed, such as some basic types of attributes;
  • Since the last build, a non-incremental file input of the Task has changed. A non-incremental file input refers to a file input that is not annotated with @Incremental or @SkipWhenEmpty.

When the Task is in full construction, that is, when the isIncremental method of InputChanges returns false, all input files can be obtained through the getFileChanges method of InputChanges, and the ChangeType of each file is ADDED. When the Task is in incremental construction, that is, the InputChanges When the isIncremental method returns true, the input file that only changes can be obtained through the getFileChanges method of InputChanges.

9.5. Commonly used annotation types

annotation type meaning
@Input Any Serializable type or dependency resolution result type A simple input value or dependency resolution result
@InputFile File* a single input file (not a directory)
@InputDirectory File* a single input directory (not a file)
@InputFiles Iterable* iterable of input files and directories
@OutputFile File* a single output file (not a directory)
@OutputDirectory File* a single output directory (not a file)
@OutputFiles Map<String, File>*或Iterable Iterable or map of output files. Using a file tree turns off caching for tasks .
@OutputDirectories Map<String, File>*或Iterable Iterable of output directories. Using a file tree turns off caching for tasks .
@Nested any custom type A custom type that may not implement Serializable, but has at least one field or property marked with one of the annotations in this table. It might even be another @Nested.
@Internal any type Indicates that the property is used internally, but is neither an input nor an output.
@SkipWhenEmpty File or Iterable* Used with @InputFiles or @InputDirectory, tells Gradle to skip the task if the corresponding file or directory is empty, and all other input files declared with this annotation. Tasks that are skipped due to all input files declaring this annotation empty will result in apparently "no source". For example, NO-SOURCE will be emitted in the console output.
Hint @Incremental .
@Incremental any type Used with @InputFiles or @InputDirectory, instructs Gradle to track changes to annotated file properties, so changes can be queried via @ InputChanges.getFileChanges() . Incremental tasks required.
@Optional any type Used with any of the attribute type annotations listed in the optional API documentation. This annotation disables validation checks on the corresponding property. See the Validation section for more details .

More can be viewed in the document .

9.6. Incremental construction principle

Before executing the Task for the first time, Gradle takes the fingerprint of the input, which contains the path of the input file and a hash of the content of each file. Then execute the Task. If the Task is successfully completed, Gradle will get the output fingerprint, which contains a set of output files and a hash of the content of each file. Gradle will retain two fingerprints the next time the Task is executed.

Each subsequent time before executing the Task, Gradle will perform new fingerprinting on the input and output. If the new fingerprint is the same as the previous fingerprint, Gradle assumes that the output is the latest and skips the Task. If they are not the same, Gradle will execute the Task. . Gradle will keep two fingerprints the next time it executes the Task.

If the file's statistics (i.e. lastModified and size) have not changed, Gradle will reuse the file fingerprint from the last run, i.e. when the file's statistics have not changed, Gradle will not detect changes.

Gradle also considers the Task's code as part of the task's input, and when the Task, Action, or its dependencies change between executions, Gradle considers the Task to be out of date.

Gradle understands whether file properties (such as those holding the Java classpath) are order-sensitive, and when comparing the fingerprints of such properties, even if the file order changes, it will cause the Task to be out of date.

Note that if a Task specifies an output directory, any files added to that directory since the last execution will be ignored and will not cause the Task to become obsolete, so unrelated Tasks may share an output directory without mutual Interference, if for some reason this is not the behavior you want, consider using TaskOutputs.upToDateWhen(groovy.lang.Closure).
Note also that changes to the availability of unavailable files (for example, modifying the target of a broken symlink to a valid file and vice versa), will be detected and handled by up-to-date checks.
The Task's input is also used to compute the build cache key used to load the Task's output when enabled.

10. Find Task

Sometimes we need to find an official Task to hook operations, such as adding an Action; sometimes we can also find a custom Task to make it depend on an official Task.

Finding the Task mainly involves the TaskContainer object. As the name suggests, it is the management class of the Task container, which provides two methods:

  • findByPath(String path), the parameter can be empty
  • getByPath(String path), the parameter can be empty, if the Task cannot be found, an UnknownTaskException will be thrown

At the same time, TaskContainer inherits from TaskCollectionand NamedDomainObjectCollectionadds two methods that can be used:

  • findByName
  • getByName

The parameter definition is the same as the xxxByPath method.

10.1、findByName

Example:

def aaa = tasks.findByName("yechaoa").doFirst {
    println("yechaoa excuted doFirst by findByName")
}

Find a Task named "yechaoa", add a doFirst Action, and print the log in doFirst.

When we execute aaa at this time, it will not trigger the execution of yechaoa Task Action, because there is no dependency, so we have to execute yechaoa Task.
implement:

 ./gradlew yechaoa

output:

> Task :app:yechaoa
yechaoa excuted doFirst by findByName
...

You can see that the log we added has been printed out.

10.2、findByPath

Example:

def bbb = tasks.findByPath("yechaoa").doFirst {
    println("yechaoa excuted doFirst by findByPath")
}

output:

> Task :app:yechaoa
yechaoa excuted doFirst by findByPath
yechaoa excuted doFirst by findByName
...

10.3、named

Look up Task by name, if there is no exception, UnknownTaskException will be thrown

tasks.named("yechaoa") {
    it.doFirst {
        println("yechaoa excuted doFirst by named")
    }
}

output:

> Task :app:yechaoa
yechaoa excuted doFirst by named
yechaoa excuted doFirst by findByPath
yechaoa excuted doFirst by findByName
...

10.4 Others

withType:

tasks.withType(DefaultTask).configureEach(task -> {
    if (task.name.toLowerCase().contains("copytask")) {
        println(task.class.name)
    }
})

each/forEach/configureEach:

tasks.each {
    // do something
}

tasks.forEach(task->{
    // do something
})

tasks.configureEach(task -> {
    // do something
})

11, extra

11.1 The difference between register and create

In addition to the methods in the above example register, there are createmethods to create Task, so what is the difference between them?

  • When created by register, only when this task is needed, it will be created and configured;
  • When created by create, the Task will be created and configured immediately, and added to the TaskContainer;

To put it bluntly, register is a way to create tasks on demand, so that the performance of gradle execution is better (not the performance of your project).

The way create creates a task is no longer officially recommended. Although @Deprecated is not marked yet, it may be abandoned in the future.

However, it should be noted that register is lazy loading, and the task created by nesting cannot be initialized during the configuration phase, so it will not be executed.

In addition to register and create, there is also a replace method, which is used to replace the existing Task with the name.

11.2、Task Tree

We can ./gradlew tasksview all Tasks through , but we cannot see the dependencies of Tasks.

To view the dependencies of a Task, we can use the task-tree plugin

plugins {
    id "com.dorongold.task-tree" version "2.1.1"
}

taskTreeWhen using it, we only need to add it after the command

gradle <task 1>...<task N> taskTree

Example:

./gradlew build taskTree

output:

:app:build
+--- :app:assemble
|    +--- :app:assembleDebug
|    |    +--- :app:mergeDebugNativeDebugMetadata
|    |    |    \--- :app:preDebugBuild
|    |    |         \--- :app:preBuild
|    |    \--- :app:packageDebug
|    |         +--- :app:compileDebugJavaWithJavac
|    |         |    +--- :app:compileDebugAidl
|    |         |    |    \--- :app:preDebugBuild *
|    |         |    +--- :app:compileDebugKotlin
......

12. Finally

So far, the introduction of Gradle Task is over.

From the aspects of what is the Task, where to write it, how to write it, how to run it, and how to write it well, etc., it introduces the execution sequence of Task Action, custom Task, Task dependency, and Task incremental construction from shallow to deep. Generally speaking, there are still a lot of knowledge points involved, and it needs to be understood and applied in practice.

I hope this article is helpful to you~

13、Github

https://github.com/yechaoa/GradleX

14. Related documents

Guess you like

Origin blog.csdn.net/yechaoa/article/details/131368728