> "Do not make it a property in the extension, ...
# plugin-development
l
"Do not make it a property in the extension, but a function in the extension, then do the configuration in that function and the consumer can call that function to enable that configuration"
@Vampire
I am trying to distill the standard pattern for configuring the JavaPluginExtension (which, in turn would yield configuration to various Java-related tasks) using lazy configuration from my own conventions plugin. Start with parts of an extension, which has a
JavaVersion
Property with the default value (convention)
JavaVersion.VERSION_17
and a convenience function which can be used from an individual gradle project or another plugin:
Copy code
open class EtgJavaStandardsBuildExtension(project: Project) : AbstractEtgBuildExtension(project) {

  /**
   * The Java version to use within this project. Will be used for both source and target compatibility.
   */
  val withJavaVersion: Property<JavaVersion> = optionalProperty(JavaVersion.VERSION_17)

  /**
   * Convenience method to set the [JavaVersion] to use within this project.
   *
   * @param javaVersion The Java version to use, both for source and target compatibility.
   */
  fun useJavaVersion(javaVersion: JavaVersion) = withJavaVersion.set(javaVersion)
...
}
I would now - in the conventions plugin which publishes the extension above - want to call the JavaPluginExtension's configuration methods and set its JavaVersion value with the value from my conventions plugin above. I need to do this lazily and it must permit configuration from the individual project. 1. The JavaLibraryPlugin - and therefore the JavaPlugin - is applied before the addAndConfigureExtensions method is invoked, so the
JavaPluginExtension
exists. 2. Currently, my implementation uses the
afterEvaluate
block, but from what I understand above this is a bad practise. 3. What is the best practise pattern to set the properties lazily? Ideally, no configuration should be done in individual projects, but the defaults/conventions should be used as much as possible.
Copy code
open class EtgJavaStandardsPlugin : AbstractEtgPlugin() {

  override fun applyPrerequisitePluginsOfType(): List<Class<out Plugin<Project>>>? =
    listOf(EtgLifecyclePlugin::class.java, JavaLibraryPlugin::class.java)

  override fun addAndConfigureExtensions(project: Project) {

    val config = createBuildExtension<EtgJavaStandardsBuildExtension>(project, "etgJavaStandards")

    // #1) Configure the JavaPluginExtension
    //
    project.afterEvaluate {
      it.extensions.configure(JavaPluginExtension::class.java) { jpe ->
        with(jpe) {

          // Set the Java Version
          val javaVersion = config.withJavaVersion.get()
          sourceCompatibility = javaVersion
          targetCompatibility = javaVersion

          // Add sources JAR to all publications
          withSourcesJar()

          // Add the JavaDoc JAR if asked to
          if (config.addJavaDocToComponent.get()) {
            withJavadocJar()
          }
        }
      }
t
As you quoted, change your extension to make configuration imperative:
Copy code
private val internalJavaVersion: Property<JavaVersion>

val javaVersion: Provider<JavaVersion> = internalJavaVersion // expose it, read-only, for others to possibly use

fun withJavaVersion(javaVersion: JavaVersion) {
  internalJavaVersion.set(javaVersion)
  javaExtension.sourceCompatibility = javaVersion
  javaExtension.targetCompatibility = javaVersion
}

fun withJavadoc() {
  javaExtension.withJavadocJar()
}
(and configure the default Java version and
withSourcesJar()
right from your plugin's
configure()
)
l
So .. other plugin extensions must be injected into my build extension?
Implies separating publicly available extension type from the implementation where those internal types are available?
v
Actually, I would say do not set
sourceCompatibility
and
targetCompatibility
, but instead use JVM toolchains. Those are properly leveraging `Property`s, so you can just wire your extension property to that property without the need for a function. If you insist on setting
sourceCompatibility
and
targetCompatibility
, which are not `Property`s up to now, then yes, use a function instead. You can inject the other plugins' extensions into your extension to configure them. Or you can just inject the
Project
and get the extensions from that.
Project
injection must even not be done explicitly as long as you let Gradle create your extension instance, it can automatically inject the
Project
instance.
You don't need to separate API from implementation, you can either have a protected field for injecting the project for example, or constructor arguments for injecting whatever you want. The consumer should not see that usually.
👍 1
But you could of course separate api from implementation if you prefer.
👍 1
l
One of my points here is that using imperative configuration will miss the case when convention values are defined in a Property and the imperative function is not called.
Or are those lazy functions called immediately if a conventions value is supplied? That does not seem to make sense, because it is - in effect - a non-lazy evaluation of a supposed-to-be-lazily evaluated Property. Right?
v
How it is done concretely, heavily depends on the concrete situation. You can usually not mix having a
Property
with convention value with imperative configuration. The problem is with mixing lazy-logic with eager-logic. If you have
Property
that you want to configure and
Property
in your extension (like if you would use JVM toolchains instead of
*Compatibility
, you can wire those `Property`s together and as long as those are evaluated as late as possible, all is fine. If you need to configure eager things, for example plain properties like
*Compatibility
you have to define some point in time where you configure them. You can of course configure them from a
Property
in your extension in an
afterEvaluate { ... }
block so that the consumer of your extension had a chance to configure that
Property
. But as you noted, using
afterEvaluate { ... }
is bad practice in almost all cases and the main thing you add with using it is ordering problem, timing problems, and race conditions. What happens for example if the consumer of your extension also uses
afterEvaluate { ... }
which is registered after you
afterEvaluate { ... }
and is thus also executed after yours and changes the
Property
in your extension in there. You would miss that configuration and not use what the consumer configured. These problems are why
afterEvaluate
is bad practice and why the
Property
and so on were invented. To bridge between
Property
and eager config like
*Compatibility
you can of course use
afterEvaluate
, but as said will earn the same problems. If you in this case would just set
*Compatibility
to your convention values in your plugin apply and then have a function in your extension that reconfigures it, you would probably achieve what you want. But as I said, how to properly do it might be different from case to case. And here I would just use JVM toolchains instead which imho are always preferable, as the decouple the Java verison used to execute Gradle from the Java version used for compilation and so on.
Actually, if you really don't like having your consumers call functions, you can also add a level of nesting in your extension instead. If you extension configuraion looks like
Copy code
myExtension {
    config {
        foo = true
    }
}
you could have the "property-style" setting while configuring the eager values safely
l
That is simple enough with some Kotlin extension functions, so that already exists.
Functions with 1 argument == setters in a kotlin environment anyhow.
v
config
would be a function in the extension that gets
Action<MyExtensionConfig>
as parameter. Then when
config
is called, you call the parameter and after that you can extract the information to set the other eager values.
If such actions would include non-reversible things like registering tasks, you would additionally finalize their values, so that their configuration can only be set once and then not changed again
l
That was my idea at first.
v
That is simple enough with some Kotlin extension functions, so that already exists.
Kotlin extension functions are always bad, unless you have a narrow set of consumers from which you know they always use Kotlin DSL. Otherwise you leave Groovy DSL users behind.
l
However, the JavaPluginExtension does not expose plugins, so cannot be finalized
v
your properties
l
True, that.
v
But in your case the finalizing wouldn't be necessary anyway
setting
*Compatibility
to one value and then to another is no problem
l
And simple enough to do. However, I would - in the same manner - finalize the properties on the JavaPluginExtensions after being set in my conventions plugin. That is not quite possible as is
v
Some people want to register one task or another depending on a
Property<Boolean>
. There for example you would need to finalize the property or might get into trouble.
However, I would - in the same manner - finalize the properties on the JavaPluginExtensions after being set in my conventions plugin
Why should you do that?
There might be valid cases for consumers to change the values again after your convention applied
l
To ensure that only one (convention) Plugin would control the Java version being used within the project.
v
Well, your decision, but I really wouldn't do it even if you could.
l
Why?
I don't see the case for having several plugins modifying a property and letting the last one applied win
v
It's the user's responsibility to not apply conflicting convention plugins. If he applies a convention plugin "use java 8" and a convention plugin "use java 11", that's just a configuration error
l
.. in which case I would want the build to explode with an exception, right?
Not use the last configuration applied.
v
In that specific case, maybe. But there are situation you cannot foresee. Maybe a user want to have the other things that convention plugin is doing but needs a different Java version and thus applies it or whatever.
l
Sounds rather unpredictable, but OK.
v
I just don't like too much taking flexibility away 🙂
Anyway, as you said, you cannot do it with
*Compatibility
anyway.
Maybe you can with Gradle 9 where hopefully `Property`s are everywhere.
Actually, if you follow my suggestion above, you could do it.
l
I really want no flexibility in a large build, to be honest. 🙂 Having things like a Toolchain plugin choosing anuything at all dynamically at runtime is not really a viable option for a really reproducible build. However, I quite see the appeal with smaller or more greenfield builds.
v
JVM toolchains are
Provider
enabled and are preferable anyway, so there you could finalize the values you set
l
Thanks for the input and your suggestions, though. 🏅
v
Of course you would need to finalize it on all tasks that use a toolchain too to really lock in the user, or he could reconfigure it on a per-task level 🙂
👍 1
l
It is ... to be honest ... somewhat difficult to distill these insights from the Gradle API.
v
Yes
🙂