Is it possible to register a task in an extension?

I don’t know if I can describe this in sufficient detail without constructing a repo that reproduces it, but I’m gonna try.

The broader context is that I’m trying to write an extension to simplify configuration and execution of test suites. There are different test drivers and different options for different test suites and I want to try to hide as much of that complexity as I can. So I wrote a TestSuitesExtension that looks something like this:

interface TestSuitesExtension {
  abstract val project: Property<Project>
  abstract val platform: Property<String>
  abstract val edition: Property<String>
  // more properties

  fun configureSuite(suite: String, language: String,
                     options: Map<String,String> = emptyMap()) {
    // more stuff
    val taskName = "${computeTheTaskName()}"
   val thisSuite = project.get().tasks.register(taskName) {
      doLast {
        println("Hello, world")
    }
  }
}

Then I wrote a little ts.gradle.kts to import and use it (yes, “ts” is a stupid name, I’m just trying to make something work, I’ll fix that later):

import ...
val extension = project.extensions.create<TestSuitesExtension>("ts")
extension.project.convention(project)
extension.platform.convention("")
extension.edition.convention("")

Both of those files are in buildSrc/src/main/kotlin/...

Then in my actual build.gradle.kts, I use the plugin:

plugins {
  id("ts")
}

configure the plugin:

ts {
  platform = "java"
  edition = "ee"
}

And I can run the task. Yay!

Now, In TestSuitesExtension.kt I change my task so it can do something more useful than print hello world,:

   val thisSuite = project.get().tasks.register<JavaExec>(taskName) {
       classpath = ...
       mainClass = ...
       args(...)
   }

And it won’t compile:

: file:///path/buildSrc/src/main/kotlin/TestSuitesExtension.kt:90:56 Type mismatch: inferred type is () -> Unit but Class<TypeVariable(T)!> was expected

If I move the task registration into the ts.gradle.kts file, it will register, but then if I attempt to do reference layout.buildDirectory or run mkdir(dir), it blows up in a different way:

e: file:///path/buildSrc/src/main/kotlin/ts.gradle.kts:21:1 Interface TestSuitesExtension captures the script class instance. Try to use class instead

I’m hoping this is enough detail to spot the place where I’m being an idiot :slight_smile:

Dunno why it works with hello world, but I think your problem with

“file:///path/buildSrc/src/main/kotlin/TestSuitesExtension.kt:90:56 Type mismatch: inferred type is () → Unit but Class<TypeVariable(T)!> was expected” is exactly what it says. You do not have the right signature. Actually, you miss the import of the extension function org.gradle.kotlin.dsl.register which provides that signature. As you do not imported that extension function (in build scripts and precompiled script plugins they are auto-imported) you use the register method that is in the TaskContainer and that expects as second argument the Class, not the configure lambda.

it blows up in a different way

From the message I’d say you moved the whole TestSuitesExtension into the ts.gradle.kts file and then use things from the scope of that script object, namely layout and mkdir and that seems to not work with an interface, but would need a class instead.

Actually, you should @Inject a ProjectLayout instance and use normal mkdir method instead of the one on Project in plugins. also you can @Inject the Project instance to your extension and don’t need to set it to a Property. Generally at execution time you should avoid as far as possible to use Project, because that is deprecated and incompatible with configuration cache for example. For registering the task, you can also directly inject the TaskContainer instead of Project.

Thank you. Is there some documentation somewhere that I should have read about how to @Inject things like the ProjectLayout and/or the TaskContainer? I can’t work out what that means in practice and I haven’t found any documentation or examples through web searching.

On the question of the second argument to register(), I’m slightly embarrassed to say that I can’t work out what, syntactically, one writes to represent a JavaExec task type as a KClass<TypeVariable(T)>.

Is there some documentation somewhere

Generally it is at Understanding Services and Service Injection, but it does indeed not list everything you can inject. The ProjectLayout for example is mentioned, but neither Project nor TaskContainer. Also not everything is injectable at every place. Ususally, I just try to inject the thing I need directly, so if I want to use project.layout, I try to inject ProjectLayout, if I want to use project.tasks I try to inject TaskContainer. If it works, fine, if not, I use some other means to get hold of it like using the Project method, especially if it is for configuration time, not execution time.

You can either inject on properties like @get:Inject abstract val projectLayout: ProjectLayout, or you can make the thing to inject a constructor parameter and annotate the contructor with @Inject.

I’m slightly embarrassed to say that I can’t work out what, syntactically, one writes to represent a JavaExec task type as a KClass<TypeVariable(T)> .

JavaExec::class, but as you now imported the method, you can use the syntax you showed, both would work with the import. Without the import you would need a Class, the it were JavaExec::class.java.

Thank you! I have it working now.

1 Like