How to configure a settings plugin

Actually, you can even do without the Action variant.
And with the latest master it just works with this from Groovy DSL:

import static dev.aga.gradle.versioncatalogs.Generator.INSTANCE as Generator

plugins {
    id("dev.aga.gradle.version-catalog-generator")
}

dependencyResolutionManagement {
    versionCatalogs {
        Generator.generate(it, 'testLibs') {
            it.from {
                it.toml {
                    it.libraryAlias = 'xyz'
                }
            }
        }
    }
}

But there are other quirks in your plugin, for example:

  • if you specify an entry without version nor version.ref, you get a not too helpful exception, maybe the message should be pimped a bit, for example including the alias name and file name or similar
  • if the specified POM does not have dependencyManagement, you get a NullPointerException instead of a message like “xyz is not a BOM you dork”
  • you use File constructor with a relative path at https://github.com/austinarbor/version-catalog-generator/blob/main/plugin/src/main/kotlin/dev/aga/gradle/versioncatalogs/GeneratorConfig.kt#L99; this is practically always a very bad idea in any Java project. Because that means the relative path is resolved relative to the current working directory, which is often not what you would expect. And for example for Gradle builds it sometimes is the project directory, but often it is the daemon log directory, or the ide installation directory, or whatever. Any Gradle build that depends on the current working directory like that is broken and flaky and can fail any time randomly.

Thanks for your feedback! I agree with everything you are saying. I am planning on improving the error messaging soon but wanted to focus on getting the base functionality working first, which is why I still have it versioned in alpha. What is the better method for handling files? I saw Working With Files but there are many assumptions about being in a project plugin or being able to use lazily configured properties which I cannot take advantage of in this case

What is the better method for handling files?

Do not use relative paths without context.
For example make the file configuration explicit without default.
Or don’t use Kotlin extension functions, but use Gradle extension that you then can create using ObjectFactory#newInstance and inject various services, or give arguments to.

Or with those extension functions, require that the user supplies the reference to the settings, like:

diff --git a/plugin/src/main/kotlin/dev/aga/gradle/versioncatalogs/Generator.kt b/plugin/src/main/kotlin/dev/aga/gradle/versioncatalogs/Generator.kt
index 00a4fd2..7585152 100644
--- a/plugin/src/main/kotlin/dev/aga/gradle/versioncatalogs/Generator.kt
+++ b/plugin/src/main/kotlin/dev/aga/gradle/versioncatalogs/Generator.kt
@@ -5,6 +5,7 @@ import dev.aga.gradle.versioncatalogs.service.GradleDependencyResolver
 import java.util.function.Supplier
 import org.apache.maven.model.Dependency
 import org.apache.maven.model.Model
+import org.gradle.api.initialization.Settings
 import org.gradle.api.initialization.dsl.VersionCatalogBuilder
 import org.gradle.api.initialization.resolve.MutableVersionCatalogContainer
 import org.gradle.api.internal.artifacts.DependencyResolutionServices
@@ -26,9 +27,10 @@ object Generator {
      */
     fun MutableVersionCatalogContainer.generate(
         name: String,
+        settings: Settings,
         conf: GeneratorConfig.() -> Unit,
     ): VersionCatalogBuilder {
-        val config = GeneratorConfig().apply(conf)
+        val config = GeneratorConfig(settings.rootDir.resolve("gradle/libs.versions.toml")).apply(conf)
         val resolver = GradleDependencyResolver(objectFactory, dependencyResolutionServices)
         return generate(name, config, resolver)
     }
diff --git a/plugin/src/main/kotlin/dev/aga/gradle/versioncatalogs/GeneratorConfig.kt b/plugin/src/main/kotlin/dev/aga/gradle/versioncatalogs/GeneratorConfig.kt
index a71bab5..32a673e 100644
--- a/plugin/src/main/kotlin/dev/aga/gradle/versioncatalogs/GeneratorConfig.kt
+++ b/plugin/src/main/kotlin/dev/aga/gradle/versioncatalogs/GeneratorConfig.kt
@@ -3,7 +3,7 @@ package dev.aga.gradle.versioncatalogs
 import dev.aga.gradle.versioncatalogs.service.FileCatalogParser
 import java.io.File

-class GeneratorConfig {
+class GeneratorConfig(val toml: File) {

     /**
      * Function to generate the name of the library in the generated catalog The default function
@@ -59,7 +59,7 @@ class GeneratorConfig {
      * @param sc the config block
      */
     fun from(sc: SourceConfig.() -> Unit) {
-        val cfg = SourceConfig().apply(sc)
+        val cfg = SourceConfig(toml).apply(sc)
         if (cfg.hasTomlConfig()) {
             val parser = FileCatalogParser(cfg.tomlConfig.file)
             source = { parser.findLibrary(cfg.tomlConfig.libraryAlias) }
@@ -68,12 +68,12 @@ class GeneratorConfig {
         }
     }

-    class SourceConfig {
+    class SourceConfig(val toml: File) {
         internal lateinit var tomlConfig: TomlConfig
         internal lateinit var dependencyNotation: Any

         fun toml(tc: TomlConfig.() -> Unit) {
-            val cfg = TomlConfig().apply(tc)
+            val cfg = TomlConfig(toml).apply(tc)
             require(cfg.isInitialized()) { "Library name must be set" }
             tomlConfig = cfg
         }
@@ -91,12 +91,12 @@ class GeneratorConfig {
         }
     }

-    class TomlConfig {
+    class TomlConfig(toml: File) {
         /** The name of the library in the TOML catalog file */
         lateinit var libraryAlias: String

         /** The catalog file containing the BOM library entry */
-        var file = File("gradle/libs.versions.toml")
+        var file = toml

         internal fun isInitialized(): Boolean {
             return ::libraryAlias.isInitialized

Will the extension function always run in the same execution as the plugin application? If so, I could set the rootDir from the plugin apply function so the settings object doesn’t need to be passed in by users

I don’t really get the question.
Extension functions are run when you call them, they are just functions.
If you want to avoid the need to give something by the user, then - as I said - you should probably not use extensions functions (as long as context receivers are not available at least), but instead use Gradle extensions where you can inject values when you let Gradle create them on plugin application time.

I mean are all phases of the build lifecycle executed in the same java runtime and classloader? For example, if I set a global variable in the plugin’s apply method, will that value always be visible during the plugin’s execution?

I will look into extensions again but I didn’t think they were usable for this application. In the stage where the version catalog generation needs to occur there is no way to lazily evaluate properties. So in the extension approach I think I would need to pass in all of the properties I depend on (the MutableVersionCatalogContainer and Settings if I want to use the rootDir) into the extension eagerly, and I think the end result would look similar to the extension method implementation I have now.

I mean are all phases of the build lifecycle executed in the same java runtime and classloader?

Not necessarily.
There can be multiple runtimes involved, but more for task work that are offloaded from the main process like Java compilation, Kotlin compilation, test execution, …
And there are many class loaders in play, at least one per build script and a few more.

For example, if I set a global variable in the plugin’s apply method, will that value always be visible during the plugin’s execution?

Depends on the exact details you mean. If you apply the plugin individually to separate subprojects, they will be in different class loaders and not see each other. Unless you add it to a common class loader for example by adding it to the classpath of a common parent project.

But if with “global variable” you mean some static field, then you should never do that anyway. Static state is veeery often a bad idea and within Gradle logic it is definitely the wrong thing to do. If you want some value store that is valid for the time of one build execution, then use a shared build service, that is one of its intended use-cases. If you use static state, different build executions, or even builds of different project can influence each other and already did in the past.

I will look into extensions again but I didn’t think they were usable for this application. In the stage where the version catalog generation needs to occur there is no way to lazily evaluate properties.

Using an extension is not equal to do stuff lazily, even if often the case.
You can for example have the method generate(name, conf) in your extension, then the consumer can call that method and the method can do the logic you currently do in the Kotlin extension function. Just that you have the extension instance into which you were able to inject things like the root directory.

So in the extension approach I think I would need to pass in all of the properties I depend on (the MutableVersionCatalogContainer and Settings if I want to use the rootDir) into the extension eagerly,

So?
That’s the point, isn’t it?
That you can do it eagerly in your plugin application method instead of needing the user to supply it.

and I think the end result would look similar to the extension method implementation I have now.

Yes, just that it will work better and also from both DSLs consistently. :slight_smile:

I implemented it as an extension and was able to keep the public api mostly the same. From groovy dsl you can do

generator.generate("myLibs") {
 // config block
}

But from Kotlin DSL I think the better user experience is still importing the extension function

import dev.aga.gradle.versioncatalogs.Generator.generate
generate("myLibs") {
 // config
}

The difference is now the above lives in Settings and it can be used anywhere. Thanks for the help

1 Like