ListProperty and afterEvaluate in custom Gradle plugin

I tried to write a precompiled script plugin to learn how to use ListProperty.

buildSrc/src/main/groovy/GreetingPlugin.gradle:

abstract class GreetingPluginExtension {
    abstract ListProperty<String> getUsers()
    abstract Property<String> getOwner()
}

def extension = project.extensions.create('greeting', GreetingPluginExtension)

abstract class MyTask extends DefaultTask {
    @Input
    abstract Property<String> getUsername()

    @TaskAction
    def task_action() {
        println "Hello "+username.get()+" !"
    }
}

tasks.register("AllMyCustomTasks", DefaultTask) {

}

tasks.register("MyCustomTask_Owner", MyTask) {
    username = extension.owner
}

AllMyCustomTasks.dependsOn "MyCustomTask_Owner"

extension.users.get().each { user ->
    tasks.register("MyCustomTask_${user}", MyTask) {
        username = user
    }

    AllMyCustomTasks.dependsOn "MyCustomTask_${user}"
}

The consumer of the plugin, app/build.gradle, looks like:

plugins {
    id 'com.android.application'
    id 'GreetingPlugin'
}
...
greeting {
    users = ["John", "Peter", "Paul"]
    owner = "Larry"
}

preBuild.dependsOn AllMyCustomTasks
...

After a gradle sync, I expected to see 4 custom tasks: app:MyCustomTask_Owner, app:MyCustomTask_John, app:MyCustomTask_Paul, app:MyCustomTask_Peter. And when running the build, I expected to see:

...
> Task :app:MyCustomTask_John
Hello John !

> Task :app:MyCustomTask_Owner
Hello Larry !

> Task :app:MyCustomTask_Paul
Hello Paul !

> Task :app:MyCustomTask_Peter
Hello Peter !

> Task :app:AllMyCustomTasks
> Task :app:preBuild
...

However, only the app:MyCustomTask_Owner task is created.
It is only after I modify buildSrc/src/main/groovy/GreetingPlugin.gradle by placing

extension.users.get().each { user ->
    tasks.register("MyCustomTask_${user}", MyTask) {
        username = user
    }

    AllMyCustomTasks.dependsOn "MyCustomTask_${user}

inside project.afterEvaluate {}, that all 4 custom tasks are correctly created and the build time output matches what I expected.

So my question is: why is it necessary to wait until the end of configure phase to register 3 of the 4 custom tasks that involve reading the ListProperty of the extension? Why is it that the MyCustomTask_Owner task, which only involves a Property of the extension, can be defined outside of project.afterEvaluate {}?

You create the extension.
Then you immediately query the list value users and create tasks for the contents.
Then after that the build script of the user runs and actually sets the property.

By using afterEvaluate you let the user first configure the extension and after that create the tasks.

But afterEvaluate is evil.
In almost all situations it is just a work-around that introduces timing problems and race conditions and just treats symptoms.

In this case, I’d not use a list property, but instead have a method in the extension that is called by the consumer project and registers the wanted tasks.

Hi @Vampire ,

Thank you for your reply.
Following your advice, I modified the precompiled script plugin to:

abstract class GreetingPluginExtension {
    abstract Property<String> getOwner()
    List<String> users = []

    void add_users(String... u) {
        users.addAll(u);
        println("inside add_users: " + users)
    }
}

def extension = project.extensions.create('greeting', GreetingPluginExtension)

abstract class MyTask extends DefaultTask {
    @Input
    abstract Property<String> getUsername()

    @TaskAction
    def task_action() {
        println "Hello "+username.get()+" !"
    }
}

tasks.register("MyCustomTask_Owners", MyTask) {
    username = extension.owner
}

println("at file scope: " + extension.users)

extension.users.each { user ->
    tasks.register("MyCustomTask_${user}", MyTask) {
        username = user
    }
}

On the consumer side, I configure the plugin extension using:

greeting {
    owner = "Larry"
    add_users "John", "Peter", "Paul"
}

However, I find that Gradle still refuses to create MyCustomTask_John, MyCustomTask_Peter, and MyCustomTask_Paul unless I put:

extension.users.each { user ->
    tasks.register("MyCustomTask_${user}", MyTask) {
        username = user
    }
}

inside an afterEvaluate block. What is interesting is that if you consider the two println lines inside the plugin, the output they produce when I perform a Gradle sync is:

> Configure project :app
at file scope: []
inside add_users: [John, Peter, Paul]

It seems as if the call to the add_users method inside the consumer actually takes place after task creation takes place, unless I resort to afterEvaluate.

No, you did not what I advised.
You should register the tasks in the extension method. :wink:

Btw. this is not Python, you shouldn’t use snake-case :smiley:

1 Like