Gradle shouldRunAfter not available for a task

2019-06-12 17:30发布

问题:

I created this repo to reproduce exactly what I'm seeing. I have a project that I'm building with Gradle. I would like to configure my Gradle build so that running:

./gradlew build

Has the exact same effect as running:

./gradlew clean build scalafmt shadowJar fizzbuzz

Meaning Gradle calls tasks in the following order:

  1. clean
  2. build (compile & run unit tests)
  3. scalafmt (run a tool that formats my code to a styleguide)
  4. shadowJar (creates a self-contained executable "fat" jar)
  5. fizzbuzz (prints out "Fizzbuzz!" to the console)

According to the Gradle docs on ordering tasks, it seems that I can use shouldRunAfter to specify ordering on all tasks...

If you clone my repo above and then run ./gradlew build you'll get the following output:

./gradlew build
Fizzbuzz!

FAILURE: Build failed with an exception.

* Where:
Build file '/Users/myUser/thelab/idea-scala-hate-each-other/build.gradle' line: 65

* What went wrong:
A problem occurred evaluating root project 'idea-scala-hate-each-other'.
> Could not find method shouldRunAfter() for arguments [task ':build'] on cz.alenkacz.gradle.scalafmt.PluginExtension_Decorated@6b24ddd7.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 10.983 secs

So even though I specify fizzbuzz to run last...its running first! And there are (obviously) errors with the rest of my config. And I'm not sure how to "hook" clean so that even when I run ./gradlew build it first runs clean.

I'd be OK with a solution that requires me to write my own "wrapper task" to achieve the order I want, and then invoke it, say, via ./gradlew buildMyApp, etc. Just not sure how to accomplish what I want.


Update

I made some changes to build.gradle and am now seeing this:

./gradlew fullBuild
:compileJava UP-TO-DATE
:compileScala
:processResources UP-TO-DATE
:classes
:jar
:startScripts
:distTar
:distZip
:assemble
:compileTestJava UP-TO-DATE
:compileTestScala UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build
:clean
:fizzbuzz
Fizzbuzz!
:scalafmt
:shadowJar
:fullBuild

So when I run ./gradlew fullBuild task execution seems to be:

  1. build (which calls compileJava through check)
  2. clean
  3. fizzbuzz
  4. scalafmt
  5. shadowJar

So the ordering is still wrong even given my most recent changes...

回答1:

As Oliver already stated, you need to put the console output to a doFirst or doLast closure, otherwise it will be executed when the task is defined (during configuration phase).

The exception is caused by the fact that both extension properties and tasks are added to the scope of the Project object, but if both an extension property and a task with the same name (scalafmt in this case) exist, the extension property will be accessed. As the error message tells you, you are trying to access the shouldRunAfter method on an object of the type PluginExtension, where it does not exist. You need to make sure to access the task:

tasks['scalafmt'].shouldRunAfter build

Update

Actually, I thought that you only need the two specific problems to be solved, but already have a solution for your basic Gradle structure.

First of all, both shouldRunAfter and mustRunAfter do not actually cause a task to be executed, they only define the order if both tasks are executed (caused by command line or a task dependency). This is the reason why the tasks clean, scalafmt, shadowJar and even fizzbuzz are not executed if you call gradle build. So, to solve your first problem, you could let the build task, which you call explicitly, depend on them:

build.dependsOn 'clean', 'scalafmt', 'shadowJar', 'fizzbuzz'

But a task dependency will always run before the parent task, so all tasks will be executed before the build task. This should not be a problem, since the build task does nothing more than collecting task dependencies for all required build steps. You would also need to define the order not only between of your task dependencies, e.g. clean and the parent task build, but mainly between the existing task dependencies, e.g. compileJava. Otherwise clean could run after compileJava, which would delete the compiled files.

Another option would be to define a new task, which then depends on all the tasks you want to execute:

task fullBuild {
    dependsOn 'clean', 'build', 'scalafmt', 'shadowJar', 'fizzbuzz'
}

This would still require to define the order between your tasks and the existing task dependencies, e.g.

compileJava.mustRunAfter 'clean'
[...]

Please note, that now you would have to call gradle fullBuild from command line. If you really need to only call gradle build via command line and still execute some tasks after the actual build task, you could use a little trick in your settings.gradle file:

startParameter.with {
    if (taskNames == ['build']) {
        taskNames = ['clean', 'build', 'scalafmt', 'shadowJar', 'fizzbuzz']
    }
}

This piece of code checks the task name input you entered via command line and replaces it, if it only contains the build task. This way you would not have to struggle with the task order, since the command line tasks are executed consecutively.

However, this is not a clean solution. A good solution would include the definition of task dependencies for all tasks that really depend on each other and the invocation of multiple tasks via command line (which you want to avoid). Especially the hard connection between the clean and the build task bypasses a lot of useful features from the Gradle platform, e.g. incremental builds.

Regarding your second point in the update, it is wrong that the fizzbuzz task is running first. It is not running at all. The command line output is printed when the task is configured. Please move the println call to a doFirst / doLast closure:

task fizzbuzz {
    doFirst {
        println "Fizzbuzz!"
    }
}


标签: gradle