Keep default value of task property

In my plugin I’ve created an extension and want to set properties of a task only when the extension property has been set.

The task BootJar for instance has a sensible default value for mainClass.

Is there a way to not set the tasks mainClass property unless the extension property has been set?

// ...
interface AppExtension {
    val mainClass: Property<String>
    // a lot of other properties removed
val app = extensions.create<AppExtension>("app")

tasks.named<BootJar>("bootJar") {
    // === v === mainClass.set should only be called if app.mainClass is set

Something like this should probably do what you want:

val bootJar by tasks.getting(BootJar::class)

Good idea, but unfortunately this runs into a StackOverflowError.

My next attempt was to change your code to:

val bootJar by tasks.getting(BootJar::class)
app.mainClass.convention(bootJar.mainClass.get())  // <-- note the .get() here

This however gave me the following error-message:

An exception occurred applying plugin request [id: 'app-conventions']
> Failed to apply plugin 'app-conventions'.
   > Failed to query the value of task ':proj2:backend:bootJar' property 'mainClass'.
      > Querying the mapped value of task ':proj2:backend:bootJarMainClassName' property 'outputFile' before task ':proj2:backend:bootJarMainClassName' has completed is not supported

Any other ideas I could try?

Ah, right.
This cannot work of course.
You cannot get just the provider that is set for the mainClass property to reuse it.
You should probably simply manually set your extension property to the the same convention as the boot jar property is set to.
Otherwise it becomes ugly.
You would then probably have to use an afterEvaluate and hope it got set before, then check whether it was set and conditionally configure the boot jar property.

What do you think about:

interface DmAppExtension {
    var mainClass: Property<String>  // Note that mainClass is a var now
    // other properties

val bootJar by tasks.getting(BootJar::class)

val app = extensions.create<AppExtension>("app")
app.fileName.convention(project.provider { })
app.mainClass = bootJar.mainClass   // !!!  interesting Code here !!!

I guess making the property a var makes it easy to accidentally set it using = instead of set().

The following code prevents users of the extension from replacing the property itself.

open class AppExtension @Inject constructor(
    project: Project
) {
    val name: Property<String> =
        .convention(project.provider { })
    val fileName: Property<String> =
        .convention(project.provider { "${}.jar" })
    var mainClass: Property<String> =
    internal fun setMainClass(newMainClass: Property<String>) { mainClass = newMainClass }

val app = extensions.create<DmAppExtension>("app")
app.mainClass = bootJar.mainClass

Hm, maybe if you make the mainClass property a constructor arg, then you can make it val, can’t you?

And btw. better inject the ObjectFactory and ProviderFactory directly rather than the Project .

Or maybe more idiomatic, keep the convention setting out of the extension and just have all properties abstract except for the mainClass that you set through the constructor, then you don’t need any injections at all.

Like this:

abstract class AppExtension(
    val mainClass: Property<String>
) {
    abstract val name: Property<String>
    abstract val fileName: Property<String>

val bootJar by tasks.getting(BootJar::class)
val app = extensions.create<AppExtension>("app", bootJar.mainClass).apply {
    name.convention(project.provider { })
    fileName.convention(project.provider { "${}.jar" })

The caveat it has is, that if someone modifies the property on the BootJar task it will also modify the one on the extension of course as they are the same now. If that is ok for you, that’s probably the way to go and much better than using afterEvaluate.

1 Like

Thank you!

I’ve really learned a lot from your replies!

1 Like