How do you delay configuration of a task by a custom plugin using the extension method?


I am trying to use the Exec task within my custom plugin along with the extension configuration mechanism and am running into the problem of the Exec task being configured before the extension evaluates, resulting in the task using the default values instead of the user supplied ones. I don’t think this is really a problem with Exec itself, it just happens to be what I am trying to use.

Here is a snipet of an example build.gradle, with a task to print the commandLine from the Exec task:

apply plugin: 'launch4j'
  launch4j {
    launch4jCmd = "NewValueFromExtension"
    mainClassName = "com.example.myapp.Start"
    icon = 'icons/myApp.ico'
    task printCmd() << {
   println project.getTasksByName('createExe', false).iterator().next().commandLine

Simply flipping the order of the apply plugin and the launch4j extension doesn’t work as the plugin has to be applied before the extension exists.

My plugin creates the task like this:

def task = project.tasks.add(TASK_RUN_NAME, Exec) = LAUNCH4J_GROUP
        task.commandLine "$project.launch4j.launch4jCmd", "${project.buildDir}/${project.launch4j.outputDir}/${project.launch4j.xmlFileName}"
        task.workingDir "${project.buildDir}/${project.launch4j.outputDir}"
        return task

I have also tried this

def task = project.tasks.add(TASK_RUN_NAME, Exec)
        task.configure {
            description = "Runs launch4j to generate an .exe file"
            group = LAUNCH4J_GROUP
            commandLine "$project.launch4j.launch4jCmd", "${project.buildDir}/${project.launch4j.outputDir}/${project.launch4j.xmlFileName}"
             workingDir "${project.buildDir}/${project.launch4j.outputDir}"

and this

def task = project.task(TASK_RUN_NAME, Type:Exec) << {
            description = "Runs launch4j to generate an .exe file"
            group = LAUNCH4J_GROUP
            commandLine "$project.launch4j.launch4jCmd", "${project.buildDir}/${project.launch4j.outputDir}/${project.launch4j.xmlFileName}"
             workingDir "${project.buildDir}/${project.launch4j.outputDir}"

but in all cases the configuration of the task happens before the extension is evaluated, and so the commandLine does not reflect the users configuration values from the build file.

Digging into the Exec source code, I found it accepts an Object for setting the commandLine, so I thought I might be able to pass a closure in to delay the evaluation, but Exec only does a toString() on the object (" line 43). And so no evaluation of the closure, and hence no delayed configuration.

I assume I must be missing something obvious about how to wait until after the extension is evaluated to grab the contents to configure the Exec task, but am stumped.

BTW, the (slightly broken) plugin is here:

Thanks Philip

1 Like

There are multiple ways to deal with this, depending on the circumstances. Some task properties do accept a closure, but most don’t. A simple general solution (with some gotchas) is to perform the configuration in a ‘doFirst {}’ task action. A more capable general solution is convention mapping. This is not yet considered a public feature but is used extensively in Gradle’s own plugins. If you feel like using an unofficial and undocumented feature, have a look at, say, Gradle’s code quality plugin.

I am trying to implement things via a doFirst {}, although I think I am running into one of the gotchas. Namely, I don’t think I can configure things that effect up-to-date checks this way since by definition the doFirst is only run if the task is run, which only happens if it is not up to date.

Another gotcha I am guessing is that if someone using my plugin also does a doFirst on the task, then their doFirst happens before my doFirst, meaning that they are dealing with an unconfigured task. I am guessing there isn’t a way to do a doFirstAndIReallyMeanFirst(Closure)

I will explore the convention mapping approach, but it feels to me like there should be an explicit place for a configuration closure in a task so it is easy for a task writer to configure the task lazily using dynamic values and also easy for gradle to make sure the task is completely configured before doing up to date checks. Seems like the implementation would be no trouble as you already have doFirst, so having a Task.configure(Closure) would simply be the same as a doFirst that happened before the up to date check and before any other doFirst.

Hopefully the convention mapping will allow me to do this. I’ll explore that next.


Using lazy GStrings might be easier:

def task = project.tasks.add(TASK_RUN_NAME, Exec) = LAUNCH4J_GROUP
        task.commandLine "${->project.launch4j.launch4jCmd}", "${->project.buildDir}/${->project.launch4j.outputDir}/${->project.launch4j.xmlFileName}"
        task.workingDir "${->project.buildDir}/${->project.launch4j.outputDir}"
        return task

GStrings are not lazy by default, but that syntax makes them so.

Lazy GStrings seem to work great, thanks.

Also saw this message on the list

about having the task depend on a “configuration task” whose purpose is only to configure the task at runtime. This sounds like it might be a good general purpose solution when strings are not sufficient.

I think between the lazy project.file(), lazy GStrings and maybe configuration tasks, I should be able to get everything configured at the correct time and working.

Thanks for the help, Philip

Out of curiosity, would the changes done in a ‘doFirst{}’ action be included in the up-to-date checking mechanism?

Additionally, for a Test task, could a ‘doFirst{}’ action add to ‘systemProperties’ and ‘classpath’? Or is ‘doFirst{}’ too late in the game?

No, anything done in doFirst() happens after input/output analysis and is therefore not considered.

I know this thread is quite old, but just had to say thanks for the lazy GString approach. I was pretty much banging my head against the wall wondering why when I was trying to create my JAR Manifest my Project Description was null. Perhaps a sidenote on the Manifest docs?