Can gradle extensions manage the lazy evaluation of a property?

advertisements

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.


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.