How do I add new type-safe accessors to a Gradle Project?

Viewed 205

We have a plugin which defines additional properties and adds them as extension properties, like:

project.extra["copyright"] = "Copyright ..."

Then in the build scripts, I can access this like:

project.extra["copyright"]

I'd like to just write:

project.copyright

Some Gradle plugins seem to do something like this. I can access project.sourceSets or project.kotlin even though those certainly aren't in the Project interface.

Using an IDE, I can drill into those convenience methods, which then lands me in some autogenerated code, so I know it's being autogenerated somewhere, but I haven't been able to find any clues to how to get this to happen for our own plugin. The Gradle docs mention type-safe accessors which is ultimately what allowed me to phrase the question, but the docs don't say how to add new ones.

How do we get this treatment for our own plugin?

1 Answers

Creating DSL-like APIS is documented here: https://docs.gradle.org/current/userguide/implementing_gradle_plugins.html#modeling_dsl_like_apis.

It's pretty easy to do. Here's a quick guide, and some tips.

  1. Define an extension.

    import org.gradle.api.provider.Property
    
    interface MyExtension {
      val copyright: Property<String>
    }
    

    It looks pretty boring! What's important is that it's either be an abstract class, or an interface - this is so Gradle can create a new instance (see 'Managed types'), and this is where the Gradle magic begins.

    Aside: I've used Property<String> instead of String, although both will work. I recommend using types compatible with Lazy Configuration.

  2. Register the extension.

    import org.gradle.api.*
    
    abstract class MyPlugin : Plugin<Project> {
    
      override fun apply(project: Project) {
    
        val myExtension: MyExtension = 
            project.extensions.create("myPlugin", MyExtension::class.java)
      }
    }
    

    This is the magic part. Simply by registering the extension against the project, Gradle will make the extension available and automatically generate Kotlin-DSL convenience methods.

  3. Apply the plugin

    // build.gradle.kts
    
    plugins {
      id("my-plugin")
    }
    
    myPlugin {
      copyright.set("blah blah 2022")
    }
    

    Just like that, Gradle will generate Kotlin DSL accessors. Here's one of them:

    // Accessorsajp3oxzka99ro52ctxwv0petb.kt
    
    /**
     * Configures the [myPlugin][MyExtension] extension.
     */
    fun org.gradle.api.Project.`myPlugin`(configure: Action<MyExtension>): Unit =
        (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("myPlugin", configure)
    
  4. Use the extension values

    Going back to the Plugin definition, lets say you want to register your own task:

    // a demo task
    abstract class MyTask : DefaultTask() {
      @get:Input
      abstract val copyright: Property<String>
    
      @TaskAction
      fun run() {
        println("Copyright is ${copyright.get()}")
      }
    }
    

    (Note that this task, like the extension, is a 'managed type').

    Now the custom task can be registered, and a default value for copyright set.

    abstract class MyPlugin : Plugin<Project> {
    
      override fun apply(project: Project) {
    
        val myExtension: MyExtension = project.extensions.create("myPlugin", MyExtension::class.java)
    
        project.tasks.register("myCustomTask", MyTask::class.java) {
          copyright.set(myExtension.copyright)
        }
      }
    }
    

    It's good that both MyExtension and MyTask use Property<String> - the actual value will be evaluated lazily, and only if required.

    Now if I run ./gradlew :myCustomTask, I see:

    > Task :myCustomTask
    Copyright is blah blah 2022
    

Further reading

Related