How to configure a settings plugin

TL;DR:

A project plugin can be configured by the config block introduced by creating an extension for it (project.extensions.create(...)). How can this be done in a “Settings” plugin?

Long story:

I have a Settings plugin, that should apply a different “Project” plugin, if configured to do so. To do that I have the following code:

MySettingsPluginExtension.kt

open class MySettingsPluginExtension
@Inject constructor(
	private val settings: Settings,
	objects: ObjectFactory
) {

	val applyProjectPlugin: Property<Boolean> =
		objects.property<Boolean>()
			.convention(
				settings.providers.provider {
					return@provider settings.extensions.extraProperties.let {
						if (it.has("mySettings.applyProjectPlugin")) it.get("mySettings.applyProjectPlugin").toString().toBoolean() else true
					}
				}
			)
}

MySettingsPlugin.kt

open class MySettingsPlugin : Plugin<Settings> {

	override fun apply(settings: Settings) {
		val mySettingsPluginExtension = settings.extensions.create("mySettings", MySettingsPluginExtension::class.java, settings)

		// automatically apply the project plugin in the root project, if configured
		if (mySettingsPluginExtension.applyProjectPlugin.get()) {
			settings.gradle.rootProject {
				it.plugins.apply(MyProjectPlugin::class.java)
			}
		}
	}
}

My first assumption was, that I just get a block mySettings { ... } in my settings.gradle.kt where I could do applyProjectPlugin.set(false) to disable it. That does not exist though.

It works to set mySettings.applyProjectPlugin=false in my gradle.properties file.

I found two ways of having some way to configure my plugin, but both don’t seem to work:

I added the following block to my Settings Plugin:

fun Settings.mySettings(action: Action<MySettingsPluginExtension>): Unit =
	(this as ExtensionAware).extensions.configure("mySettings", action)

Now I can import this method in my settings.gradle.kt:

import com.example.gradle.plugins.settings.mySettings
...
mySettings {
    applyProjectPlugin.set(false)
}

But changing values there does not seem to have any effect.

The same goes for the following block in the project I applied the Settings plugin to:

configure<MySettingsPluginExtension> {
    applyProjectPlugin.set(false)
}

After both of them I would expect, that the project plugin is not applied anymore. But it still is.

So my question: How do I configure my Settings plugin from my actual project.

Thanks in advance!

As you found out already, there are unfortunately no type-safe accessors generated for Kotlin DSL settings scripts. And you already found 2 of the 2.5 ways to configure your extension, either by providing the type-safe accessor yourself and importing it, or by configuring the extension by type. A variation would be to get the extension by type and name like extensions.getByName<MySettingsPluginExtension>("mySettings").apply { ... }.

The .5 would be to have the accessor in the package org.gradle.kotlin.dsl, then it would be available without import, but imho that is not really a clean way, just mentioning it for completeness.

That your configuration does not have an effect is exactly the same problem you would have in a project plugin with the code you used. You create the extension and then immediately check its values before the settings script had a chance to configure it. So you first evaluate the value and react to it and after that configure the value.

So either defer the evaluation of the extension e.g. like with

settings.gradle.rootProject {
    if (mySettingsPluginExtension.applyProjectPlugin.get()) {
        it.apply(MyProjectPlugin::class.java)
    }
}

or have a method in the extension that does the action instead of having a property, something like

fun applyProjectPlugin() = settings.gradle.rootProject {
    it.apply(MyProjectPlugin::class.java)
}

in the extension that you then call from your settings script.

Sidenote: if you look at the JavaDoc of Project.getPlugins() you learn that you should not use .plugins, that’s why it is missing from my snippets. :wink:

Thanks! I assumed it had something to do with “the plugin is applied, but the config is not evaluated yet”. But I could not find any examples on how to do it properly!

Doing this does not work, unfortunately:

settings.gradle.rootProject {
	if (cosmoSettingsPluginExtension.applyProjectPlugin.get()) {
		it.apply(MyProjectPlugin::class.java)
	}
}

It fails with

None of the following functions can be called with the arguments supplied.
apply((Closure<Any!>…Closure<*>?)) defined in org.gradle.api.Project
apply((Mutable)Map<String!, *>!) defined in org.gradle.api.Project
apply(Action<in ObjectConfigurationAction!>!) defined in org.gradle.api.Project

I use this settings plugin also to setup my repositories. And I add a parameter to the extension for the repositoriesMode the same way I do it for applying the project plugin. I then do the following in my apply method:

	override fun apply(settings: Settings) {
		val mySettingsPluginExtension = settings.extensions.create("mySettings", MySettingsPluginExtension::class.java, settings)

		settings.dependencyResolutionManagement.apply {
			repositories....
			repositoriesMode.set(mySettingsExtension.repositoriesMode)
		}
	}

Can you tell me why this is working then? Is it because I do not actually evaluate the value of mySettingsExtension.repositoriesMode, but pass on the Property object?

Thanks!

None of the following functions can be called with the arguments supplied.

If it.plugins.apply ... worked, it.apply ... should have too.
I wrote that from the top of my head, adapting your example.
Actually in Kotlin DSL it should be this, not it, so

settings.gradle.rootProject {
	if (cosmoSettingsPluginExtension.applyProjectPlugin.get()) {
		apply(CosmoProjectPlugin::class.java)
	}
}

Can you tell me why this is working then? Is it because I do not actually evaluate the value of mySettingsExtension.repositoriesMode , but pass on the Property object?

Exactly.
That’s exactly why the lazy Property / Provider API was invented.
To be able to wire properties together and evaluate them as late as possible, optimally when configuration was finished completely.
With your boolean property you call .get() and thus immediately evaluate its value.

The it in the context is the project object.

Doing a plain apply(MyProjectPlugin::class.java) fails as well. It’s trying to recursively calling the apply method of this Plugin.

Ah, sorry.
Of course it.apply<MyProjectPlugin>() in Kotlin DSL then. :slight_smile:

Another solution seems to be:

settings.gradle.rootProject { project ->
	if (mySettingsPluginExtension.applyProjectPlugin.get()) {
		project.pluginManager.apply(MyProjectPlugin::class.java)
	}
}

In fact the only solution. it.apply<MyProjectPlugin>() fails with the same error as above :slight_smile:

I found the docu here: PluginAware - Gradle DSL Version 8.1.1

Thanks for your help!

Which “same error” are you referring to?

It results in the same error as this.

The documentation for plugins says:

The container of plugins that have been applied to this object.

While not deprecated, it is preferred to use the methods of this interface or the PluginAware.getPluginManager() than use the plugin container.

Use one of the ‘apply’ methods on this interface or on the PluginAware.getPluginManager() to apply plugins instead of applying via the plugin container.

https://docs.gradle.org/current/dsl/org.gradle.api.plugins.PluginAware.html#org.gradle.api.plugins.PluginAware:plugins

What seems to work is

settings.gradle.rootProject { project ->
	project.apply(mapOf("plugin" to MyProjectPlugin::class))
}

For it.apply<MyProjectPlugin>() you need to import org.gradle.kotlin.dsl.apply.

@Vampire is there any doLast type equivalent for Settings plugins? As I mentioned in the other thread I am trying to create a plugin that generates version catalogs. If I use any of the
settings.gradle.beforeProject, settings.gradle.rootProject, settings.gradle.beforeSettings, settings.gradle.afterSettings, etc hooks, my generated catalog is not registered in the dsl.

If I move it outside of those blocks and use a hardcoded string name for the library, it works correctly. If I use a Property and it’s outside of the above blocks, I get an error because the property hasn’t been set yet.

It doesn’t seem like a hook or event exists where I need it to in order to use extension properties for the plugin configuration

override fun apply(settings: Settings) {
  val extension = settings.extensions.create<VersionCatalogGeneratorExtension>("generateVersionCatalogs")
  settings.dependencyResolutionManagement.versionCatalogs.create(extension.name.get()) {
    // logic here
 }

I guess I can create extension function on MutableVersionCatalogContainer and pass my arguments in there, that seems to be working

Yes, or a function in VersionCatalogGeneratorExtension that does the actual logic and is called by your users with the name as argument instead of setting a property for example.

@Vampire do you know how to call the Kotlin extension function from the Groovy dsl? Is it even possible? I have the extension function

    fun MutableVersionCatalogContainer.generate(
        name: String,
        conf: GeneratorConfig.() -> Unit,
    ): VersionCatalogBuilder {
        val config = GeneratorConfig().apply(conf)
        val resolver = GradleDependencyResolver(objectFactory, dependencyResolutionServices)
        return generate(name, config, resolver)
    }

This works fine in settings.gradle.kts:

import dev.aga.gradle.versioncatalogs.Generator.generate

dependencyResolutionManagement {
 versionCatalogs {
  generate("myLibs") {
     toml {
       libraryAlias = "xyz"
    }
  }
}
}

But when I try and invoke in settings.gradle

dependencyResolutionManagement {
    versionCatalogs { mvcc ->
        dev.aga.gradle.versioncatalogs.Generator.generate(mvcc, "testLibs") {
            toml {
                libraryAlias = "xyz"
            }
        }
    }
}

I get the error

Caused by: groovy.lang.MissingMethodException: No signature of method: static dev.aga.gradle.versioncatalogs.Generator.generate() is applicable for argument types: (org.gradle.internal.management.DefaultVersionCatalogBuilderContainer_Decorated...) values: [[], testLibs, settings_c5uckf7ijczor339o6ni4cmxl$_run_closure1$_closure2$_closure3@377277ba]
Possible solutions: generate(org.gradle.api.initialization.resolve.MutableVersionCatalogContainer, java.lang.String, kotlin.jvm.functions.Function1)

You probably need to change GeneratorConfig.() -> Unit to Action<GeneratorConfig> as then Groovy and Kotlin both can provide that using a closure / lambda. Groovy cannot provide the Kotlin function type via closure.

But you might consider my other suggestion, making it as method in VersionCatalogGeneratorExtension then both DSLs can just call that method conveniently.

I’m not sure the extension approach will work anymore since I need access to the MutableVersionCatalogContainer that Gradle creates/manages. I also tried using Action instead but can’t seem to get the arguments to work

def action = new Action<dev.aga.gradle.versioncatalogs.GeneratorConfig>() {
    @Override
    void execute(dev.aga.gradle.versioncatalogs.GeneratorConfig o) {

    }
}
dependencyResolutionManagement { drs ->
    dev.aga.gradle.versioncatalogs.Generator.generate(drs.getVersionCatalogs(), "testLibs", action as Action<dev.aga.gradle.versioncatalogs.GeneratorConfig>)
}
Caused by: groovy.lang.MissingMethodException: No signature of method: static dev.aga.gradle.versioncatalogs.Generator.generate() is applicable for argument types: (org.gradle.internal.management.DefaultVersionCatalogBuilderContainer_Decorated...) values: [[], testLibs, settings_c5uckf7ijczor339o6ni4cmxl$1@75440741]
Possible solutions: generate(org.gradle.api.initialization.resolve.MutableVersionCatalogContainer, java.lang.String, org.gradle.api.Action)

Is the action not being casted correctly? Or does the decorated container not implement the interface maybe?

Don’t create an action like that, just use a closure like

dependencyResolutionManagement { drs ->
    dev.aga.gradle.versioncatalogs.Generator.generate(drs.getVersionCatalogs(), "testLibs") {
        toml {
            libraryAlias = "xyz"
        }
    }
}

I’ve tried both ways without much luck - do you know of any other plugins that have done something like this?

EDIT: I figured it out, I needed @JvmStatic on the extension function :man_facepalming:t3: I really appreciate all your help and patience