A task action is the code that implements what a task is doing, as demonstrated in the previous section.
For example, the
javaCompile
task action calls the Java compiler to transform source code into byte code.
It is possible to dynamically modify task actions for tasks that are already registered.
This is helpful for testing, patching, or modifying core build logic.
Let’s look at an example of a simple Gradle build with one
app
subproject that makes up a Java application ? containing one Java class and using Gradle’s
application
plugin.
The project has common build logic in the
buildSrc
folder where
my-convention-plugin
resides:
app/build.gradle.kts
plugins {
id("my-convention-plugin")
}
version = "1.0"
application {
mainClass = "org.example.app.App"
}
app/build.gradle
plugins {
id 'my-convention-plugin'
}
version = '1.0'
application {
mainClass = 'org.example.app.App'
}
We define a task called
printVersion
in the build file of the
app
:
buildSrc/src/main/kotlin/PrintVersion.kt
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
abstract class PrintVersion : DefaultTask() {
// Configuration code
@get:Input
abstract val version: Property<String>
// Execution code
@TaskAction
fun print() {
println("Version: ${version.get()}")
}
}
buildSrc/src/main/groovy/PrintVersion.groovy
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
abstract class PrintVersion extends DefaultTask {
// Configuration code
@Input
abstract Property<String> getVersion()
// Execution code
@TaskAction
void printVersion() {
println("Version: ${getVersion().get()}")
}
}
This task does one simple thing: it prints out the version of the project to the command line.
The class extends
DefaultTask
and it has one
@Input
, which is of type
Property<String>
.
It has one method that is annotated with
@TaskAction
, which prints out the version.
Note that the task implementation clearly distinguishes between "Configuration code" and "Execution code".
The configuration code is executed during Gradle’s configuration phase.
It builds up a model of the project in memory so that Gradle knows what it needs to do for a certain build invocation.
Everything around the task actions, like the input or output properties, is part of this configuration code.
The code inside the task action method is the execution code that does the actual work.
It accesses the inputs and outputs to do some work if the task is part of the task graph and if it can’t be skipped because it’s UP-TO-DATE or it’s taken FROM-CACHE.
Once a task implementation is complete, it can be used in a build setup.
In our convention plugin,
my-convention-plugin
, we can register a new task that uses the new task implementation:
app/build.gradle.kts
tasks.register<PrintVersion>("printVersion") {
// Configuration code
version = project.version as String
}
app/build.gradle
tasks.register(PrintVersion, "printVersion") {
// Configuration code
version = project.version.toString()
}
Inside the configuration block for the task, we can write configuration phase code which modifies the values of input and output properties of the task.
The task action is not referred to here in any way.
It is possible to write simple tasks like this one in a more compact way and directly in the build script without creating a separate class for the task.
Let’s register another task and call it
printVersionDynamic
.
This time, we do not define a type for the task, which means the task will be of the general type
DefaultTask
.
This general type does not define any task actions, meaning it does not have methods annotated with
@TaskAction
.
This type is useful for defining 'lifecycle tasks':
app/build.gradle.kts
tasks.register("printVersionDynamic") {
}
app/build.gradle
tasks.register("printVersionDynamic") {
}
However, the default task type can also be used to define tasks with custom actions dynamically, without additional classes.
This is done by using the
doFirst{}
or
doLast{}
construct.
Similar to defining a method and annotating this
@TaskAction
, this adds an action to a task.
The methods are called
doFirst{}
and
doLast{}
because the task can have multiple actions.
If the task already has an action defined, you can use this distinction to decide if your additional action should run before or after the existing actions:
app/build.gradle.kts
tasks.register("printVersionDynamic") {
doFirst {
// Task action = Execution code
// Run before exiting actions
}
doLast {
// Task action = Execution code
// Run after existing actions
}
}
app/build.gradle
tasks.register("printVersionDynamic") {
doFirst {
// Task action = Execution code
// Run before exiting actions
}
doLast {
// Task action = Execution code
// Run after existing actions
}
}
If you only have one action, which is the case here because we start with an empty task, we typically use the
doLast{}
method.
In the task, we first declare the version we want to print as an input dynamically.
Instead of declaring a property and annotating it with
@Input
, we use the general inputs properties that all tasks have.
Then, we add the action code, a
println()
statement, inside the
doLast{}
method:
app/build.gradle.kts
tasks.register("printVersionDynamic") {
inputs.property("version", project.version.toString())
doLast {
println("Version: ${inputs.properties["version"]}")
}
}
app/build.gradle
tasks.register("printVersionDynamic") {
inputs.property("version", project.version)
doLast {
println("Version: ${inputs.properties["version"]}")
}
}
We saw two alternative approaches to implementing a custom task in Gradle.
The dynamic setup makes it more compact.
However, it’s easy to mix configuration and execution time states when writing dynamic tasks.
You can also see that 'inputs' are untyped in dynamic tasks, which can lead to issues.
When you implement your custom task as a class, you can clearly define the inputs as properties with a dedicated type.
Dynamic modification of task actions can provide value for tasks that are already registered, but which you need to modify for some reason.
Let’s take the
compileJava
task as an example.
Once the task is registered, you can’t remove it.
You could, instead, clear its actions:
app/build.gradle.kts
tasks.compileJava {
// Clear existing actions
actions.clear()
// Add a new action
doLast {
println("Custom action: Compiling Java classes...")
}
}
app/build.gradle
tasks.compileJava {
// Clear existing actions
actions.clear()
// Add a new action
doLast {
println("Custom action: Compiling Java classes...")
}
}
It’s also difficult, and in certain cases impossible, to remove certain task dependencies that have been set up already by the plugins you are using.
You could, instead, modify its behavior:
app/build.gradle.kts
tasks.compileJava {
// Modify the task behavior
doLast {
val outputDir = File("$buildDir/compiledClasses")
outputDir.mkdirs()
val compiledFiles = sourceSets["main"].output.files
compiledFiles.forEach { compiledFile ->
val destinationFile = File(outputDir, compiledFile.name)
compiledFile.copyTo(destinationFile, true)
}
println("Java compilation completed. Compiled classes copied to: ${outputDir.absolutePath}")
}
}
app/build.gradle
tasks.compileJava {
// Modify the task behavior
doLast {
def outputDir = file("$buildDir/compiledClasses")
outputDir.mkdirs()
def compiledFiles = sourceSets["main"].output.files
compiledFiles.each { compiledFile ->
def destinationFile = new File(outputDir, compiledFile.name)
compiledFile.copyTo(destinationFile)
}
println("Java compilation completed. Compiled classes copied to: ${outputDir.absolutePath}")
}
}