Search code examples
gradlegradle-plugingradle-kotlin-dsl

How can I make typesafe accessors available for new project properties contributed by a Gradle plugin?


If I have a plugin which defines a new project property:

// Common plugin

var copyrightDate: String? by project.extra

And then try to access this property in a build script:

plugins {
    `lifecycle-base`
    id("acme.common")
}

copyrightDate = "2022"

I get the predictable result of:

e: path\to\build.gradle.kts:7:1: Unresolved reference: copyrightDate

A workaround as noted in the answer here is to explicitly declare the property again each time you are about to use it, like:

var copyrightDate: String? by project.extra

Since we have a number of projects using the same plugin, I'd rather have this happen automatically, like how applying the java plugin makes a java {} function available without having to declare it. This mechanism appears to be used by a number of plugins, including those not shipped with Gradle itself. But I'm yet to figure out how they're making it work.

How is this meant to be done?

  • Test project contains a unit test written for Gradle TestKit which tries to run a similar build script.

Solution

  • Register an extension

    The official way to do this is to register an extension.

    First create an extension object. So long as it only has simple properties, it can be a Gradle managed type.

    // src/main/kotlin/my/custom/plugin/MyCustomPluginSettings.kt
    package my.custom.plugin
    
    import org.gradle.api.provider.Property
    
    interface MyCustomPluginSettings {
      val copyrightDate: Property<String>
    }
    

    (I've used a Property<> rather than a String (the benefits are listed here) but you could also use a plain String if you wanted.)

    Next, in your plugin, create the extension, and set a default value for copyrightDate.

    // src/main/kotlin/my/custom/plugin/MyCustomPlugin.kt
    package my.custom.plugin
    
    import org.gradle.api.*
    import org.gradle.kotlin.dsl.*
    
    abstract class MyCustomPlugin : Plugin<Project> {
      override fun apply(target: Project) {
        val myCustomPluginSettings = target.extensions.create<MyCustomPluginSettings>("myCustomPlugin")
        myCustomPluginSettings.copyrightDate.convention("2022")
      }
    }
    

    Note that I'm using the Gradle Kotlin DSL. Make sure to apply the kotlin-dsl plugin in your plugin's build.gradle.kts!

    You can also apply your MyCustomPluginSettings in a buildSrc plugin in the same way - just use the contents of the apply(...) {} function in the .kts file.

    Now when you apply your plugin, Gradle will automatically generate a Kotlin DSL accessor from the name you gave your extension.

    // build.gradle.kts
    
    plugins {
      id("my.custom.plugin")
    }
    
    println(myCustomPlugin.copyrightDate.get())
    

    Non-extension method

    If you define the property in a .kt file, then so long as that file is included with the plugin.

    (If you're writing buildSrc plugins, then the .kt file can be anywhere in ./buildSrc/src/main/kotlin/...)

    // src/main/kotlin/my/custom/plugin/constants.kt
    
    package my.custom.plugin
    
    import org.gradle.api.Project
    
    var Project.copyrightDate: String?
      get() = extra["copyrightDate"] as String?
      set(value) {
        extra["copyrightDate"] = value
      }
    

    Now in a build.gradle.kts uses can import this.

    // build.gradle.kts
    
    import my.custom.plugin.copyrightDate
    
    plugins {
      id("my.custom.plugin")
    }
    
    println(copyrightDate)
    

    Avoiding the import

    You can avoid the import by either putting constants.kt in the source root, without a package, or in one of the default import packages, like org.gradle.kotlin.dsl.

    // src/main/kotlin/my/custom/plugin/constants.kt
    
    package org.gradle.kotlin.dsl
    
    import org.gradle.api.Project
    
    var Project.copyrightDate: String?
      get() = extra["copyrightDate"] as String?
      set(value) {
        extra["copyrightDate"] = value
      }
    

    Because of the risk of clashing, I only recommend this for buildSrc plugins. But this might be handy if you want to include a helper extension for something more complicated, like a helper function for defining dependencies with a default version (like how there's a dependencies { kotlin("reflect") } helper function).