Is project afterEvaluate the proper way for gradle plugin to dynamically create default tasks?

My team has written dozens of gradle plugins and have tried out different ways to do so. Recently we have been discussing what the best way is to create default tasks if a user does not manually create one them selves.

The pattern I have come to use is as follows:

// pseduo code
apply(project) {
    // some code ...
    project.afterEvaluate { setupDefaults(it) }
}

setupDefaults(project) {
    // Pattern for a single task. Create a default MyTask if the user doesn't declare one
    if (project.tasks.withType(MyTask).isEmpty()) {
        // create MyTask with reasonable defaults
    }

    // Pattern for creating a default task for each parent task
    if (project.tasks.withType(MyTask).isEmpty()) {
        // withType passing in a closure will evaluate as objects are added to the collection
        project.tasks.withType(MyParentTask) {
            // create MyTask depending on MyParentTask with reasonable defaults
        }
    }

    // Other patterns exist like generating tasks based on config found in an extension created by the plugin
}

Note that our situation is unique:

  • We have a need to support java 6, 7, and 8 build environments.
  • We support Gradle 2.8 - 4.10.3 (2.8-2.14.1 for java 6).
  • Users always run with the latest version of a plugin

In order to work within these parameters we very careful as to which gradle APIs we use so we don’t have to maintain separate forks of a plugin for different gradle versions. Occasionally we replicate behavior instead of using inheritance to prevent runtime problems when running with different versions. I have tried using a rule based annotation mechanism to dynamically generate tasks but found that it was constantly changing and people found it to be harder to follow due to a lack of technical documentation on it. It also appears to be deprecated now.

The mechanism above works but has a drawback when there are many plugins in play all using afterEvaulate. Any default setup needs to be carefully crafted to work regardless of the order that plugins are applied (one way to do this is using the withType(type, closure) method on project asks).
Some coworkers of mine have expressed concern about issues that have arose because of using project afterEvaluate in plugins. I believe this mechanism is the best solution that I am aware of and order dependency issues would appear no matter what mechanism was used.

We are looking for input from gradle users or developers involved in plugin development. Is this the proper approach to take to generate default tasks dynamically or is there a better way? Is this an appropriate use for afterEvaluate in a plugin or are we abusing it?

I try to avoid project.afterEvaluate as it gets messy when multiple plugins use it. It’s also hard for one plugin to intercept/tweak afterEvaluate logic from another plugin.

The TaskContainer extends DomainObjectCollection which has “live” methods such as all() and matching() etc. You could use these methods to disable a default task as soon as an override is added to the model.

Eg:

task defaultMyTask(type: MyTask) {
  // default stuff
} 
assemble.dependsOn defaultMyTask

tasks.all { task ->
   if (task.name != 'defaultMyTask' && task instanceof MyTask) {
      defaultMyTask.enabled = false
   } 
} 
2 Likes

I agree with @Lance: afterEvaluate should be avoided if possible; especially with multiple different plugins using it.

I don’t understand the use-case in creating a default-task if the user does not create one: Either way, some task is going to be created. The only thing that can’t be changed via configuration is the task’s name, right? Or do you dynamically create a different type of default task?

As far as I can tell, the Gradle core plugins always create the “default” task, and allow users to override the task’s configuration, if they wish.

Another option I have seen is to have a base plugin, that only provides the Task types and project extensions. An additional plugin is responsible for creating the default tasks (this applies the base plugin first, of course).

2 Likes