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
Projectfor each of the included projects. - configuration â in this phase previously created
Projectobjects 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 itMustRunwhich 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
checkis run, bothunitandfunctionalshould be executed unitandfunctionalmust be able to run separately- if
unitandfunctionalare run togetherunitshould always go first reportshould 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.
Get Maciek OpaÅaâs stories in your inbox
Join Medium for free to get updates from this writer.
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.
