Gradle custom extension with lazy configuration doesn't work

Hello!

I am working on implementing a custom plugin in a standalone project that will be used by other projects. In this plugin, I want to configure many other plugins.

Here’s the Plugin code:

class CodeStylePlugin : Plugin<Project> {

    override fun apply(project: Project) {
        project.extensions.create(
            "myplugin",
            CodeStyleExtension::class.java
        )
        val config = project.extensions.getByType(CodeStyleExtension::class.java)
        if (config.addCheckStyle.get()) {
            // configure checkstyle
        }
    }
}

Here’s the extension code:

open class CodeStyleExtension @Inject constructor(objectFactory: ObjectFactory) {
    var addCheckStyle: Property<Boolean> = objectFactory.property(Boolean::class.java).convention(false)
}

In some other project, I am calling this plugin and configuring the extension in build.gradle.kts:

apply(plugin = "code-style")
configure<CodeStyleExtension> {
    addCheckStyle.set(true)
}

The config.addCheckStyle.get() call in the plugin script is always false. I am assuming that the apply() call in the build.gradle.kts calls the plugin’s apply() function and the get() immediately returns false. I think configure<CodeStyleExtension> is configured later.
I tried to wrap the val config = and subsequent lines within a afterEvaluate code block. However, the build fails with this error: Extension of type 'CodeStyleExtension' does not exist.

I appreciate any suggestions to fix this issue.

Using afterEvaluate is almost always the wrong thing to do. In 98.7 % of the cases it is just symptom treatment like calling SwingUtilities.invokeLater or Platform.runLater to “fix” a GUI problem. It usually just shifts the problem to a later time where it is much harder to reproduce, much harder to debug, and much harder to fix. The main thing afterEvaluate does is introducing timing problems, ordering problems, and race conditions. That’s why the Property / Provider APIs were introduced, so that properties can lazily be wired together without the need to do delayed evaluation to give someone the chance to configure the value before you read it. What happens for example if someone sets the value in an afterEvaluate that is evaluated after your afterEvaluate? …

Your conclusions were right though.
There are a couple of ways to do it properly.
You could for example make the consumer not set this property but instead just apply the pure checkstyle plugin, then in your plugin you can have project.pluginManager.withPlugin("checkstyle") { /* configure checkstyle */.
Or you can have multiple convention plugins and one of them if applied, applies and configures the checkstyle plugin without condition, as the condition is that the consumer applies it.
Or you can replace the addCheckStyle property by an addCheckStyle function that is then called by the consumer and do the checkstyle configuration within that function.


Btw. your extension can be much simpler unless you need to support ancient Gradle versions:

abstract class CodeStyleExtension {
    abstract val addCheckStyle: Property<Boolean>
    init {
        addCheckStyle.convention(false)
    }
}

Or if you follow good practices and configure defaults and wirings in the plugin, you can even have

interface CodeStyleExtension {
    val addCheckStyle: Property<Boolean>
}

and then

project.extensions.create(
    "myplugin",
    CodeStyleExtension::class.java
).apply {
    addCheckStyle.convention(false)
}

to create it.

Also, extensions.create already returns the extension instance to you, so you don’t have to request the extension by type, unless you oversimplified the example.

And, Propertys should always be val, not var.
Their value might change, but the instance should never change.
And using val also enables the assignment plugin in latest Gradle versions where you can use addCheckStyle = true also in Kotlin DSL build scripts.

1 Like

Thank you! I will not be using afterEvaluate.

I gave an oversimplified example.

  • CodeStylePlugin applies and configures a bunch of plugins like Checkstyle, Jacoco, checker framework and a few more plugins.
  • It adds a bunch of dependencies like lombok etc.
  • It creates a few tasks.

Calling addCheckStyle, addJacoco, addLombok etc. in all our consumer packages is a bit verbose since most of the consumer packages will be using all of them. I want to add them by default and let the consumer disable it if they don’t want it.

Is there a way to create the extension and evaluate/load the extension config before calling CodeStylePlugin.apply()? Maybe move the plugin creation to another plugin? Will the following work (consumer’s build.gradle.kts):

apply(plugin = "code-style-extension-create") // This will call project.extensions.create()
configure<CodeStyleExtension> {
    addCheckStyle.set(true)
}
apply(plugin = "code-style") // Not sure if CodeStyleExtension will be configured before this plugin's apply() is called. 

If this doesn’t work, I will go ahead with your solution of converting the properties to functions that can be called by the consumers.

It would probably work, but is extremely bad practice.
It imposes Plugin application ordering constraints which should be avoided.
And it requires to use the legacy plugin application method which should be avoided.
Actually, the code-style-extension-create could be applied properly using the plugins { ... } block and thus you would get the type-safe accessor for the extension, so that you can properly configure it DSL-style and not by type.
But imho you should really not do it.

Another option:

  • create CheckstylePlugin
  • create JacocoPlugin
  • create CheckerFrameworkPlugin
  • create DoItAllPlugin that applies all the others

Now the typical consumer can apply the DoItAllPlugin and just gets all.
And picky consumers can just apply the individual plugins they want to have.

1 Like