My task… what’s wrong with your Gradle task?

Maciek Opała
SoftwareMill Tech Blog
6 min readOct 15, 2018

--

credit @srosinger3997

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, both unit and functional should be executed
  • unit and functional must be able to run separately
  • if unit and functional are run together unit should always go first
  • report should be always run after check

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 report
check.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?

Contact us!

We will make technology work for your business. See the projects we have successfully delivered.

--

--