My task… what’s wrong with your Gradle task?
During my over 5-year-long activity on Stack Overflow, I noticed that despite Gradle being a mature build tool that is settled on the market, some basic questions related to this technology are still recurring. Also, most of them are related to task
, which is a basic unit of work in Gradle. Below are the most common issues connected with using task
:
- … executes automatically
- … does not run
- … runs in an invalid order
- … cannot be found
- … is always up-to-date
- … is never up-to-date
I’ll explain them one after another. Let’s go!
… executes automatically
Let’s start with the following piece of a Gradle script:
println 'a'task t1 {
doFirst {
println 'b'
}
}println 'c'task t2 << {
println 'd'
}task t5 {
println 'e'
}
If this file is saved to build.gradle
and run with gradle
do you know which lines will be printed to standard output? Yup, a
, c
and e
. Do you know why? Every Gradle run consists of 3 phases:
- initialisation — in this phase Gradle resolves which projects (yes, plural in case of a multi-module project) will be included in the build and creates an instance of a
Project
for each of the included projects. - configuration — in this phase previously created
Project
objects are configured, and for every single object related build script is executed. Tasks, configurations and multiple other objects are created and configured accordingly at this phase. - execution — in this phase the tasks created in the previous phase and resolved based on the arguments passed via command line interface are executed.
In our simple snippet, there’s no execution phase at all, no additional arguments were passed. Since everything happens in configuration phase — b
and d
are skipped. What’s important, although e
was printed to the standard output, it doesn’t mean that task t5
has run — it has been configured.
… does not run
Let’s consider the following build.gradle
:
task itMustRun
which is run with gradle -i itMustRun
. Surprisingly Gradle reports that the task was skipped. Why? Because the task doesn’t have any actions configured. Every single task has a list of actions that are executed while the task is run. doFirst
and doLast
methods are used to add an action at the beginning or to the end of the list respectively. It’s possible to add multiple actions to a task, however I rarely saw it in action. If you use Groovy to implement Gradle scripts you may also spot <<
which is nothing else than an alias for doLast
method. It’s been already deprecated and scheduled to be removed, however it still remains quite popular. So, itMustRun
should be configured as follows:
task itMustRun {
doFirst {
println "It's running from start..."
}
doLast {
println "...to an end."
}
}
… runs in an invalid order
Consider the following snippet (tasks — except check
which is a lifecycle task — have no actions for the sake of brevity):
task unit(type: Test)
task functional(type: Test)task check // lifecycle task
task report
and let’s assume the rules that define the order in which tasks should be run are:
- when
check
is run, bothunit
andfunctional
should be executed unit
andfunctional
must be able to run separately- if
unit
andfunctional
are run togetherunit
should always go first report
should be always run aftercheck
Starting with the first rule we should add check dependsOn unit, functional
to the script. Why not mustRunAfter
or shouldRunAfter
? Because the latter defines the order in which tasks will be run if they are added to the graph, whereas the former forces the task to be run even though it wasn’t passed via command line — it defines a dependency.
The second and the third rule can be fulfilled with functional mustRunAfter unit
. Here mustRunAfter
is used, because we want the tasks both to be able to run separately (no dependencies) and to run in a particular order when run together (ordering). shouldRunAfter
would also do the job, the only difference is that shouldRunAfter
is less strict. The ordering will not be enforced if the rules in the script introduce a cycle between tasks or in parallel execution when all conditions have been satisfied except shouldRunAfter
.
To run report
after check
it’s enough to add: check finalizedBy report
. finalizedBy
also defines a dependency but after the task was run — not before it.
Here’s the script in it’s final shape:
task unit(type: Test)
task functional(type: Test)task check // lifecycle task
task reportcheck.dependsOn unit, functionalfunctional.mustRunAfter unit //shouldRunAftercheck.finalizedBy report
… cannot be found
After running gradle tasks
you receive the following output:
...
G tasks
-------
before
after
...
and before
is a task provided with some additional plugin at the beginning of the script. Let’s define a dependency between before
and after
:
task after {
group = 'g'
dependsOn before
doLast {
println 'after'
}
}
and run gradle after
which results in:
$ gradle after...
> Could not get unknown property 'before' for task ':after' of type org.gradle.api.DefaultTask.
...
How is this even possible? Output of gradle tasks
clearly shows that both tasks are defined, but an attempt to define a dependency results in an unknown property exception. It turns out that some tasks are added in the following way:
afterEvaluate {
task before {
group = 'g'
doLast {
println 'before'
}
}
}
Hence the task will be accessible no sooner than the whole project is evaluated. What can be done here, is to change dependsOn before
to dependsOn 'before'
—it replaces an object with a plain string that will be resolved correctly. Another way is to use whenTaskAdded
hook on tasks
object:
tasks.whenTaskAdded { t ->
println "Got task: $t"
//... further configuration
}
…is always up-to-date or … is never up-to-date
The last two points will be analyzed together since they both touch the same issue. Assume that we have the following task:
task createFile {
doLast {
file('whatever').createNewFile()
}
}
This task’s action will always be executed — every time the task is run — even though file whatever
already exists, it will be recreated. If you run the task with the -i
switch you’ll know why — the task doesn’t have any outputs. Let’s add an output then.
task createFile {
outputs.file('whatever')
doLast {
file('whatever').createNewFile()
}
}
Now when you run the task exactly in the same way (make sure that whatever
file doesn’t exist), it will run. At first time it will be run, but with a different message, because no history is available. If you run it for the second time it will not be run — because it’s up to date. This is what build tool’s job is about: to avoid work that has been already done. From now on, unless whatever
file is deleted, createFile
will be always up to date. This is the reason why your task is always up to date or the opposite, it’s never update: the outputs are misconfigured. But there’s also the other side of the coin and it’s called, well you guessed: inputs. Inputs are used to notify Gradle that indeed something has changed and the task should be rerun. Let’s add an input
then:
task createFile {
inputs.file('data')
outputs.file('whatever')
doLast {
file('whatever').createNewFile()
}
}
If the task is run again, Gradle will run. Once again, it’s up to date. On data
file change, task will be re-run. It’s an oversimplification but more or less inputs and outputs are the basis of Gradle cache mechanism. Inputs & outputs is a big topic and deserves another article that describes the mechanism much better.
I hope that this short post shed some light on how Gradle tasks work. I encourage you to contact me in case of any questions and experiment with the mechanism mentioned here — Gradle has a terrific documentation.
P.S. I know that over two years ago Kotlin met Gradle ;) However Groovy is still very popular and widely adopted in Gradle scripts — that’s why Groovy is used in this post for the examples.
Looking for Scala and Java Experts?
We will make technology work for your business. See the projects we have successfully delivered.