Tasks can only be configured in "afterEvaluation" in a custom plugin?

plugins

(lu.shunshun) #1

I’m writing a custom plugin for packaging different ‘applications’ for our project. The DSL ends up like below (simplified version of the the real DSL):

myApplications {
    trials {
        artifactName = 'trials-app'
    }
}

This is basically saying “creating a trials application with the name ‘trials-app’”. A ‘application’ is basically a ‘fat’ jar created from archives of sub projects.

The application domain object is very simple like below:
class MyApplication {

    String name
    String artifactName

    MyApplication(String name) {
        this.name = name
    }
}

The plugin generates a corresponding ‘Jar’ task, a configuration object and declares a artifact:

class MyApplicationPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        NamedDomainObjectContainer<MyApplication> myApplicationsContainer = project.container(MyApplication)
        project.extensions.add("myApplications", myApplicationsContainer)

        myApplicationsContainer.all(new Action<MyApplication>() {

            @Override
            void execute(MyApplication application) {
                // create a matching configuration
                String configurationName = application.name + "Application"
                Configuration configuration = project.configurations.create(configurationName)

                // create the Jar task
                String taskName = "jar" + configurationName.capitalize()
                Jar applicationTask = project.tasks.create(taskName, Jar)
                applicationTask.dependsOn configuration

                // declare a artifact created from the task
                project.artifacts.add(configurationName, applicationTask)

                // configure the task
                project.afterEvaluate(new Action<Project>() {
                    @Override
                    void execute(Project p) {
                        applicationTask.baseName = application.artifactName
                        applicationTask.from configuration.collect {
                            it.isDirectory() ? it : p.zipTree(it)
                        }
                    }
                })
            }
        })
    }
}

For the DSL at the beginning, this plugin will generate a jarTrialsApplication task, a configuration called trialsApplication and also adds a artifact.

Notice, the task is configured only after the project is evaluated. I got this from this Gradle documentation. I also tried to configure the project right after it’s created but all the property on my domain object are null:

Jar applicationTask = project.tasks.create(taskName, Jar)
applicationTask.configure {
       applicationTask.baseName = application.artifactName // this is always null
       .......
}

I think this must have something to do with the build lifecycle but I just can’t see why I can’t configure the task during configuration phase (I suppose the DSL was evaluated at configuration phase) . This is essentially my first question. Any pointers?

The second questions is related to tasks that depends on the generated tasks. I’ve got a ‘dist’ task in my main build script, which is not part of the plugin, like below together with the DSL:

 myApplications {
    trials {
        artifactName = 'trials-app'
    }
 }

dependencies {
    trialsApplication project(':A')
    trialsApplication project(':B')
}

task dist(type: Copy) {
    dependsOn jarTrialsApplication

    from jarTrialsApplication.archivePath
    into "${project.buildDir}/dist/trials-app.jar"

    doFirst {
        mkdir "${project.buildDir}/dist"
    }
}

This task basically copies the ‘application’ to another location. The problem with this task is that jarTrialsApplication.archivePath is always set to{my project name}.jar rather than the expected trials-app.jar.

Again, I guess this has is to do with the fact that jarTrialsApplication task is only going to be configured after the whole project is evaluated, which is too late for ‘dist’ task.

I know I can pass a closure to the from method, but this is like I have to ‘expose’ the internal behavior of the plugin to the users of the plugin which is not ideal from end users point of view. And it might not be possible to pass a closure at all in some scenarios, e.g. the task doesn’t not accept closure.

So the second question is that am I doing something anti-pattern/practice here when writing the custom plugin? Especially when you need to reference the generate tasks later on during configuration?

Thanks in advance!


(Chris Doré) #2

The action provided to myApplicationsContainer.all is called after the MyApplication instance is created, but before it is configured by the closure specified in the build file. A println in the action’s execute method and the closure that configures the application object should confirm that behaviour.


(Chris Doré) #3

Instead of using the archivePath property of jarTrialsApplication directly, just use jarTrialsApplication. This will have two effects, first, the outputs of that task will be used for the copy, lazily, and second, the task dependency you declared will no longer be necessary.


(Chris Doré) #4

If you know you will be running your plugin in new Gradle versions (4+), then you should look at using lazy properties.
https://docs.gradle.org/4.4/userguide/lazy_configuration.html#lazy_properties

The guides section of the Gradle docs contains some information for plugin development that you may also find useful.


(lu.shunshun) #5

Thanks for answers!

Unfortunately the project stuck with Gradle v2.9 at moment as it’s based on Java 5 and we haven’t upgraded the JVM to Java 7 (which is required in order to run Gradle 3) yet. But it’s interesting to know the new lazy properties!


(lu.shunshun) #6

The reason I used archivePath in the example is because in the real project we have a downstream job which only takes the path of the artifact produced by upstream job.

But if archivePath is only evaluated after the downstream job, I guess my only option is to change the downstream job to take a task object instead of a path.

I’ll give it try after the holiday!

Thanks!


(lu.shunshun) #7

I managed to change the downsteam job to take a Jar task instead of a string so this solved the second issue.


(lu.shunshun) #8

Hi @Chris_Dore, just a follow up question regarding this.

I just think this makes things less clean and potentially problematic.

Suppose I have a improved DSL like below:

myAplications {

       a-basic-app {
            variants {
               debug {
                    artifactName = "basic-debug-app"
                    loggings { // this is optional
                        // some loggings configuration
                    }
                    ....
                }

               trials {
                   artifactName = "basic-trials-app"
                   ....
               }
               // ..... more variants of basic app
            }
        }

       a-enhanced-app {
              // same structure as 'a-useful-app'
        }
    }

Now, in my plugin, say I want to create a configuration or a task using the data of a variant, I have to do it in project.afterEvaluate because, like you commented previously, non of the variant has been created in this case because the new MyApplication instance hasn’t been configured yet.

This is potentially too late because there might be another task in a different project want to depends on the configuration or task but the configuration or task won’t be created during configuration phase.

Is this something common in plugin development? or there’s a better way to archive the same thing?


(uklance) #9

If you want to add tasks etc before afterEvaluate you can do it in the “setter” of the property on your extension object.

Example here


(lu.shunshun) #10

@Lance, thanks for the reply! The MonkeyPatchExtension is a standard extension object which is not what I used in my script.

I’m using NamedDomainObjectContainer so that different “applications” can be created dynamically on the fly. Here’s my extension object:

class MyApplication {
    String name
    NamedDomainObjectContainer<Variant> variants

    PicassoApplication(String name) {
        this.name = name
    }

    void variants(Closure configuration) {
        variants.configure(configuration)
    }
} 

And

class Variant {
    String name
    String artifactName
    Closure loggings

    Variant(String name) {
        this.name = name
    }

    void loggings(Closure logsterRule) {
        this.loggings = logsterRule
    }
}

And in my plugin I have something like this:

void apply(Project project) {
    setupExtension(project)
    createTasks(project)
}

private static void setupExtension(Project project) {
    NamedDomainObjectContainer<MyApplication> applicationConfigurationsContainer = project.container(MyApplication)
    project.extensions.add(EXTENSION_NAME, applicationConfigurationsContainer)

    applicationConfigurationsContainer.all {
        variants = project.container(Variant)
    }
}

The problem I’ve got is that in createTasks, I’m trying to create a task setupLoggingOptimisationTask using the data provided by the variant block (variant.loggings) and I can only do this in afterEvaluate:

private static void createTasks(Project project) {
    def applications = project.extensions.getByName(EXTENSION_NAME)

    applications.all { application ->
        variants.all { variant ->
            // create a matching configuration
            String configurationName = "${application.name}${variant.name.capitalize()}Application"
            Configuration appConfiguration = project.configurations.create(configurationName);

            // create a matching Jar task
            String jarTaskName = "jar${appConfiguration.name.capitalize()}"
            Jar jarTask = project.tasks.create(jarTaskName, Jar)
            jarTask.dependsOn appConfiguration

            project.afterEvaluate {
                setupJarTask(project, jarTask, variant, appConfiguration)

                if(variant.loggings) {
                    String optimisationConfigurationName = "optimise${appConfiguration.name.capitalize()}"
                    //================= Can this be done during configuration phase?
                    setupLoggingOptimisationTask(project, optimisationConfigurationName, jarTask, variant.loggings)
                }
            }

            // declare a matching artifact
            project.artifacts.add(configurationName, jarTask)
        }
    }
}

Like in the comment, is it possible to create setupLoggingOptimisationTask during configuration phase rather than after project evaluated?


(uklance) #11

I think you are calling createTasks too early (you’re creating tasks before you have received any input from the client buildscript). You should create a task each time the user adds a variant by moving task creation inside setter/adder method on the extension object

Since you are using NamedDomainObjectContainer, you could use the whenObjectAdded method to perform actions (eg create a task) as you receive input from the client script.

The java plugin has functionality like this. If you add a custom SourceSet to project.sourceSets then you’ll magically get a JavaCompile task for each


(lu.shunshun) #12

That sounds sensible to me. I’ll have a look at this. Thanks @Lance!


(uklance) #13

I do a similar thing for each java “flavour” (aka variant). Interesting code here


(lu.shunshun) #14

Hi @Lance, unfortunately whenObjectAdded didn’t work out in my case. I’m having the exact same problem as before. Basically, all fields of my domain object are null except the name.

I guess this is (like what @Chris_Dore said earlier) because whenObjectAdded is only called, like its name suggested, when an object is created and added to the container, but also before the object is configured. This is effectively the same as using container.all method. There’s not much my plugin can do if those domain objects are not populated.

Don’t know if this is a overlooked feature of Gradle or not, but shouldn’t the NamedDomainObjectContainer also exposes whenObjectConfigured API? I feel this could be much more useful than whenObjectAdded because there’s nothing available except the name.


(Markus Perndorfer) #15

I don’t remember where exactly I saw it, but NamedDomainObjectContainer.all(...) (inherited from DomainObjectCollection) is used in one of the core plugins to configure tasks after a new element is added


(lu.shunshun) #16

Yes, the configuration action passed to NamedDomainObjectContainer.all will be executed for every domain object created. The problem is timing.

The domain object are not populated (by the actions) at the same time at which the domain object is created. The only way I can access the populated domain object is in project.afterEvaluate().