Making an extension that will run in a closure

Apologies if that title isn’t especially clear. Some tasks (exec, copy) can be run in a doLast closure:

doLast {
  exec { ... }
}

I have an extension that implements DefaultTask so I can say:

task myTask(type: MyTaskType) { ... }

and ./gradlew myTask will run it. And if I instantiate the plugin with:

  void apply(Project project) {
    project.task('mytype', type: MyTaskType)
  }

then I can put

mytype { ... }

at the top level in build.gradle and ./gradlew mytype will run it.

However, if I do this:

task someTask() {
  doLast {
    mytype { ... }
  }
}

and run ./gradlew someTask I can see that the various configuration methods get called on mytype but it doesn’t actually run. Neither the @TaskAction method is called nor is the execute() method called if I claim to implement Action. What obvious thing am I overlooking?

It remains elusive. If I implement Action then

doLast ( // note paren, not curly brace
  mytype {
    ...
  }
)

will run the execute() method on mytype, but it doesn’t evaluate the closure the way

doLast { // note curly brace
  mytype {
    ...
  }
}

does. Which isn’t suprising because it’s not a closure in the former case. Several hours digging around in the Gradle source code has not made me any the wiser about what copy does differently in

doLast { copy { ...  } }

that both evalutes the copy configuration closure and does something with the results.

I think you confuse a few things here.
exec { ... } or copy { ... } are no tasks.
You cannot call tasks, you can only declare dependencies between tasks.
exec { ... } calls ExecOperations (Gradle API 8.0.2), copy { ... } calls FileSystemOperations (Gradle API 8.0.2).
They work intentionally exactly like a task of type Exec or Copy, but they are only related semantically, there is no automagic translation from tasks to methods.

The mytype { ... } you have is just syntactic sugar for tasks.mytype { ... } and just configures your task with the name mytype and has no effect at all on whether the task is executed or not.

Ah. Thank you. I’m easily confused. So what I want to do is figure out how to create an “operation”. I’ll investigate.

That, unfortunately, does not look like an extensibility point. :frowning:

Well, it’s not too trival.
Depends on what you really want and need.
If you for example want something only available in Groovy DSL because it is only for your builds, you can probably just add an extension named foo that is a Closure, then you can call it from Groovy code like that.
If it is for the broader public, you probably need additionally a Kotlin extension function for usage in Kotlin DSL.

Or to have it easier and more consistent and not needing Kotlin-specific code, you would add an extension for example called foo that has a method called bar, then you should be able to call foo.bar... in both DSLs.

Yes. I haven’t looked at the Kotlin DSL at all. I’ll have to give it a think. Perhaps it’s not worth the effort. I was inspired to try it because I found that I had to update a build and that update was to create the same file in two different places. So I ended up with two tasks with the same dependencies that run the same transformation to produce the same file in two different locations. (Because reasons.) I thought, that’s just silly, I should do it like copy and exec and just run the transformation inline, xslt { ... } instead of having two tasks. I thought that would be easy.

The obvious alternative, stepping back from “I should be able to do it like copy” is simply to use copy in a doLast. So maybe I’m persuading myself that it’s not worth it, especially since I’d be even more effort to make it work in Groovy and Kotlin.

Thank you!