Adding task Options depending on configured property

We would like to be able to selectively add some Options to a task, depending on whether a property is configured. Currently, this isn’t possible, because Options can only be attached to a field or a setter method and cannot be added dynamically. What we currently do is to have to task classes:

class Test extends DefaultTask {
    @Option(option = 'gui', description = 'Starts simulation with GUI.')
    @Input
    Property<Boolean> getGui() {
        return gui
    }
   // ...
}

and

class TestWithUvm extends Test {
    @Option(option = 'uvm-verbosity', description = 'Configure verbosity of UVM messages.')
    @Input
    Property<String> getUvmVerbosity() {
        return uvmVerbosity
    }
   // ...
}

We have split our code into two plugins, one that registers a task with the the first type and one that uses the second type:

class SVUnitPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.tasks.register('test', Test)
    }
}

and

class SVUnitWithUvmPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.tasks.register('test', TestWithUvm)
        // ...
    }
}

Having two plugins makes it rather difficult to write other plugins that are supposed to use these plugins, because it causes us to create two variants of those plugins. For example, if we would want to build a SystemVerilogLibrary plugin, which would either use UVM or not, we would need to create two different flavors of this one as well, SystemVerilogLibrary and SystemVerilogLibraryWithUvm. Now assume we’d like to build on top of the SystemVerilogLibrary plugins… This leads to way too many plugins.

We would have liked to just have one plugin, which allows us to configure which actual task is used using some kind of useUvm property. We would have been fine with only having a single task type and passing it a usesUvm constructor argument, but we can’t use it to selectively add the options. We looked into configuring the type of the task using a property, but this isn’t supported, because register() takes a Class<T>.

Does anyone have any idea on how we could solve this?

because register() takes a Class<T>.

How does that hinder you to define the task type using a property?

Besides that, this is what you want to thumbs-up and watch: Runtime addition of task command line options · Issue #11117 · gradle/gradle · GitHub

Doesn’t this cause some kind of race with when the user sets that property?

Example:

// in build.gradle
plugins {
  id 'org.example.svunit' version '0.1.0'
}

svunit {
  useUvm = true  // useUvm is a Property<Bool>
}

I can’t just do project.tasks.register('test', project.useUvm.get() ? TestWithUvm : Test) in the apply() call, because at this point, the user didn’t get a chance to set a value to useUvm.

I could probably do the register() call in afterEvaluate(), but that’s generally not recommended. Also, I’m not even sure whether it’s allowed to register tasks so late.

Is there a way to react to the value of the useUvm property being set and calling the register() based on that?

Ah, you mean a property of an extension, I somehow thought project property.
Then you are right of course.
The typical pattern would for example be, to not have a property in the extension, but have a function in the extension.
You would then probably force the user to call useUvm(true) or useUvm(false) or he does not get a test task at all if that is ok with you.

I like the idea of providing a useUvm() method.

Even though it kind of breaks lazy configuration, maybe it would also be possible to add a task for the false case, then if useUvm() is called, it gets replaced with a new instance of the TestWithUvm type.

Task replacing is very risky and discouraged.
Another option that comes to mind is, that you register both tasks with different names.
You can also make a task called test that depends on both of these tasks.
And then you add an onlyIf { ... } condition to the two actual tasks that checks the property from the extension.
Then you still get type-safe accessors for Kotlin DSL, can use the property for configuration and it will just work.