Search code examples
gradlegradle-plugingradle-kotlin-dsl

Add a custom function to a Gradle Project in a convention plugin


The Gradle api documentation of Project states that one can add a dynamic method to a project by

A property of the project whose value is a closure. The closure is treated as a method and called with the provided parameters. The property is located as described above.

I guess "described above" means that one can use dynamic project properties. These are described in the api documentation as

A project has 5 property 'scopes', which it searches for properties. You can access these properties by name in your build file, or by calling the project's property(String) method. The scopes are:

...

  • The extra properties of the project. Each project maintains a map of extra properties, which can contain any arbitrary name -> value pair. Once defined, the properties of this scope are readable and writable. See extra properties for more details.

So I thought that I can write a convention plugin, e.g. "sayHello.gradle.kts"

// sayhello.gradle.kts
val sayHello : (String) -> Unit = { who ->
    println("Hello $who")
}

ext.set("sayHello", sayHello)

and then use the function (or lambda) I added to the project in another build.gradle.kts

// Some projects build.gradle.kts
plugins {
   id("sayHello")
}

sayHello("René")

But I'm getting

Script compilation errors:

  Line 7: sayHello("")
          ^ Expression 'sayHello' cannot be invoked as a function. The function 'invoke()' is not found

  Line 7: sayHello("")
          ^ Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
              public val TaskContainer.sayHello: TaskProvider<DefaultTask> defined in org.gradle.kotlin.dsl

I thought that I'm wrong with the Kotlin DSL and read the Extra Properties section, but this only shows how values are registered and not closures.

Nevertheless I tried to register a closure similar to the values of the example. So I did:

val sayHelloFun : (String) -> Unit = { who ->
    println("Hello $who")
}

val sayHello by extra(sayHelloFun)

I got the same error message.

Does anyone know how to add a custom function to a Project in a convention plugin using Kotlin DSL?


Solution

  • Dynamic property access in not available in Kotlin DSL

    Unfortunately it's not possible to add functions to the Project object in that way from a precompiled script plugin in Kotlin DSL. The section of the documentation that talks about the "five project scopes" anticipates the user is writing a Groovy build script and is not strictly accurate for Kotlin: such access relies on dynamic features of the Groovy language.

    To prove this, look at the documentation on using extra properties. If you compare the Kotlin and Groovy examples, you will see that all the Kotlin extra properties must by accessed using the string name for the property as key, for example by writing extra["myProperty"], whereas Groovy can access them directly.

    Alternatives

    Since Kotlin is statically typed, we instead need a real method the Kotlin compiler can see. There are two alternatives I suggest to do this: Gradle extensions and Kotlin extension functions.

    Gradle extensions

    When you create an extension, Gradle creates an accessor for that extension. You can use this to create a function in projects applying your plugin, except that you need to call it on the extension rather than Project directly:

    // In plugin script
    extensions.create<Greeter>("greeter")
    
    abstract class Greeter {
        fun sayHello() {
            println("G'day mate")
        }
    }
    
    // In project build.gradle.kts
    
    greeter.sayHello()
    

    Kotlin extension functions

    This approach is just exploiting a regular feature of the Kotlin language. Such functions must be defined in code that is on the buildscript classpath, so this would not work in a precompiled script plugin (as per your question).

    However, such code could be written, for instance, in a regular Kotlin file in the buildSrc folder1. Then you could simply do:

    // In buildSrc/src/main/kotlin/Greeter.kt
    
    import org.gradle.api.Project
    
    fun Project.sayHello() {
        println("Howdy, partner")
    }
        
    fun waveGoodbye() { // or a regular function if you don't need the Project object
        println("Ciao, for now")
    }
    
    // In project build.gradle.kts
    
    sayHello()
    waveGoodbye()
    

    1Such code could also be written inside the compilation of a class-defined plugin.