I'm writing a custom gradle plugin to handle some vaguely complicated work and I have run into a frustrating problem while using properties to configure some of the tasks that the plugin applies.
apply plugin: myPlugin
//Provide properties for the applied plugin
myPluginProps {
message = "Hello"
}
//Define a task that uses my custom task directly
task thisTaskWorksFine(type: MyTask) {
input = myPluginProps.message
}
//Define a plugin that will apply a task of my custom type
class MyPlugin implements Plugin<Project> {
void apply(Project project) {
project.extensions.create('myPluginProps', MyPluginExtension)
project.task(type: MyTask, 'thisTaskWorksIncorrectly') {
input = project.myPluginProps.message
}
}
}
//The extension used by my custom plugin to get input
class MyPluginExtension {
def String message
}
//The task used by both the standard build section and the plugin
class MyTask extends DefaultTask {
def String input
@TaskAction
def action() {
println "You gave me this: ${input}"
}
}
The results from using this file are as follows:
$ gradle thisTaskWorksFine thisTaskWorksIncorrectly
:thisTaskWorksFine
You gave me this: Hello
:thisTaskWorksIncorrectly
You gave me this: null
BUILD SUCCESSFUL
I consider this to be very unexpected. To my mind, applying a task from the plugin and writing one directly should result in the same output when given the same input. In this case, both tasks are given myPluginProps.message
as input, but the task applied by the plugin is greedy and evaluates to null early on. (During the apply phase?)
The only solution I have found is to use closures in the plugin task's configuration block like so:
//Define a plugin that will apply a task of my custom type
class MyPlugin implements Plugin<Project> {
void apply(Project project) {
project.extensions.create('myPluginProps', MyPluginExtension)
project.task(type: MyTask, 'thisTaskWorksIncorrectly') {
input = { project.myPluginProps.message }
}
}
}
That solves the greedy evaluation problem pretty nicely except that now the custom task has to be modified to expect and deal with a closure. It's not terrifically hard to do, but I don't think it should be the task's responsibility to deal with the closure, since the plugin is "to blame".
Am I using extensions incorrectly here? Or are they just not adequate? The official stance appears to be that we should use extensions but I've yet to find any examples where extensions could do what I need. I can move forward with my use of closures and writing a bunch of boilerplate getters that do closure eval and setters that can handle closures and normal types, but it seems very against the philosophy of groovy and therefore gradle. I would be very happy if there is a way that I can use extensions and get the lazy evaluation automatically.
EDIT
The below answer is now outdated. Since I provided it a better mechanism than convention mappings has been introduced for doing this called lazy properties.
The usual solution for this problem is to use convention mapping:
class MyPlugin implements Plugin<Project> {
void apply(Project project) {
project.extensions.create('myPluginProps', MyPluginExtension)
project.task(type: MyTask, 'thisTaskWorksIncorrectly') {
conventionMapping.input = { project.myPluginProps.message }
}
}
}
and then in the task:
class MyTask extends DefaultTask {
def String input
@TaskAction
def action() {
println "You gave me this: ${getInput()}"
}
}
Please note that I explicitly used getter for input
- the convention mapping won't kick in if you reference the field directly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With