Kotlin binary plugin extension won't read values from dsl

Hi there,

I’ve created a binary plugin written entirely in kotlin and the experience has been great, other than one detail: I can’t seem to get anything other than the conventional value out of my extension object no matter what approach I try.

In unit tests I can directly modify the value of the extension without any issue, it just refuses to take anything from the build file DSL. I’ve tried cribbing from many extant examples on github.

Clearly I’m doing something wrong, but the documentation isn’t any help in this regard, can someone point me in the right direction?

Example plugin code:

const val EXTENSION_NAME = "myPlugin"

open class MyExtension @Inject internal constructor(objectFactory: ObjectFactory) {
    val myProp: Property<String> = objectFactory.property(String::class.java).convention("")
}

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project): Unit =
        project.run {
        val extension = project.extensions.create(
            EXTENSION_NAME,
            MyExtension::class.java,
            project.objects,
        )
        logger.debug("myProp: {}", extension.myProp.get())
    }
}

Example build file:

plugins {
    id ("myorg.my-plugin") version "0.1.0"
}

myPlugin {
    myProp.set("WORKING")
}

Example output

2025-10-31T15:11:41.014+0000 [DEBUG] [org.gradle.api.Project] myProp:

Thanks.

Well, you register the extension and directly read its value, how should it magically know what the build script will set in the future? :slight_smile:

Generally, it is a bad idea to call get() or similar methods on a Provider (Property is a subclass of it) at configuration time, because you thereby defeat one of the major advantages of Property and friends. By doing so, introduce the same ordering problems and race conditions you traditionally get from using afterEvaluate (which you should avoid at almost any cost).

Optimally you should ever only get the value of a Provider at task execution phase, as then you know configuration is finished. If you get it at configuration time, you never know whether the value might be changed further on later, unless you use finalizeValue or finalizeValueOnRead which would then cause a failure on trying to further modify the value.

Typically your plugin creates extensions, sets conventions on it, and wires the properties of the extensions to properties of tasks the plugin registers. Only task then evaluates the propery at its execution phase. This way users can safely configure your stuff without problems.

Btw.

interface MyExtension {
    val myProp: Property<String>
}

and then set the convention from the plugin. :slight_smile:

1 Like

Thank you very much for the concise and detailed explaination. I have managed to rework my plugin as you described and it works as expected. :slight_smile:

1 Like