# Exposing an "API" via kotlin-dsl script plugin

**URL:** <https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755>\
**Category:** Help/Discuss\
**Created:** [June 2, 2023, 10:47pm UTC](https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755 "2023-06-02T22:47:09Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![bric3](https://sea1.discourse-cdn.com/gradle/user_avatar/discuss.gradle.org/bric3/32/9458_2.png) [@bric3](https://discuss.gradle.org/u/bric3)\
**Post date:** [June 2, 2023, 10:47pm UTC](https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755/1 "2023-06-02T22:47:09Z")

</div>

Hi,

I’m trying to write kotlin-dsl script/convention plugin that allow the consumer to set a property / calla function.

The convention plugins are set via the usual `includedBuild` pointing to a separate gradle project that has the `kotlin-dsl` plugin. Then in the actual convention plugin, e.g. for java, I would like to write something like this (using kotlin extentions methods)

```gradle
plugins {
    id("sandbox.java-conventions")
}

java {
    configureJavaToolChain(
      20,
      listOf("jdk.incubator.concurrent")
    )
}

```

Or if using property extensions

```plaintext
java {
    javaToolchain Version = 20,
    addedModules = listOf("jdk.incubator.concurrent")
}

```

Here’s how I started to write this, notice the `JavaCompile` and `KotlinCompile` tasks are configured in the body by calling `tasks.withType`.  
While this code compiles the method `configureJavaToolChain` does not seem to be reachable from the consumer. Also ideally the function approach doesn’t seem to scale much if I want to add other parameters, properties looks more appealing but also have the same visibility issues.

So I’m unsure how to proceed from this point.

```plaintext
plugins {
    id("java-library")
}

fun JavaPluginExtension.configureJavaToolChain(
    javaVersion: Int,
    // other parameters
    addedModules: List<String> = listOf(),
) {
    toolchain {
        languageVersion = JavaLanguageVersion.of(javaVersion)
    }

    tasks.withType<JavaCompile> {
        // sets release or source & target
        options.compilerArgs.addAll(
            // transform to proper args the added modules
        )
    }

    tasks.withType<JavaExec>().configureEach {
        // Need to set the toolchain https://github.com/gradle/gradle/issues/16791
        javaLauncher.set(javaToolchains.launcherFor(java.toolchain))
        jvmArgs(
            // transform to proper args the added modules
        )
    }
}

```

I’m currently experimenting on a public personal repo, if needed that may five more context.

> <https://github.com/bric3/sandbox/pull/113/files>

Thanks in advance for any help

---

<div class="post-metadata">

**Author:** ![Vampire](https://sea1.discourse-cdn.com/gradle/user_avatar/discuss.gradle.org/vampire/32/9082_2.png) [@Vampire](https://discuss.gradle.org/u/Vampire)\
**Post date:** [June 2, 2023, 11:46pm UTC](https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755/2 "2023-06-02T23:46:17Z")

</div>

Imagine the content of a Kotlin script as the body of a class, which it effectively is.  
So your extension function is only scoped to the body of that class.  
Move it out to a regular `.kt` file.

---

<div class="post-metadata">

**Author:** ![bric3](https://sea1.discourse-cdn.com/gradle/user_avatar/discuss.gradle.org/bric3/32/9458_2.png) [@bric3](https://discuss.gradle.org/u/bric3)\
**Post date:** [June 3, 2023, 12:02pm UTC](https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755/3 "2023-06-03T12:02:31Z")

</div>

I understand the concept, but since methods are public by default I assumed the methods or properties were accessible.

Also another question functions like `JavaPluginExtension.configureJavaToolChain` in the above example are actually accessing multiple things, e.g. this method “scope” is `JavaPluginExtension` so it’s only applicable if the `java` plugin has been applied and this method is also accessing `Project` items like `tasks`. Such method cannot be moved in a common `.kt` because the global scope won’t be `Project`. How should I approach the problem ?

I was thinking about creating extension properties on `JavaPluginExtension` in a regular kt file, and access them from the script with more or less the content of the method above. How does that sound ?

---

<div class="post-metadata">

**Author:** ![bric3](https://sea1.discourse-cdn.com/gradle/user_avatar/discuss.gradle.org/bric3/32/9458_2.png) [@bric3](https://discuss.gradle.org/u/bric3)\
**Post date:** [June 4, 2023, 12:15pm UTC](https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755/4 "2023-06-04T12:15:45Z")

</div>

Another exemple in a `common.kt`, this property wouldn’t compile, because `objects` is not accessible from `JavaPluginExtensions` (only form `Project`).

```plaintext
var JavaPluginExtension.myJvmVendor: Property<JvmVendorSpec>
    get() = objects.property(JvmVendorSpec::class)

```

---

<div class="post-metadata">

**Author:** ![Vampire](https://sea1.discourse-cdn.com/gradle/user_avatar/discuss.gradle.org/vampire/32/9082_2.png) [@Vampire](https://discuss.gradle.org/u/Vampire)\
**Post date:** [June 4, 2023, 7:02pm UTC](https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755/5 "2023-06-04T19:02:30Z")

</div>

> I understand the concept, but since methods are public by default I assumed the methods or properties were accessible.

That’s not so much a Gradle question, but more a question for some Kotlin community.  
But for example consider, that the extension method within the “class body” also has that class’ instance as a receiver and thus could access its fields and methods, even private ones.  
If you could use the extension method from outside the class, how would the receiver instance be defined?

> Also another question functions like `JavaPluginExtension.configureJavaToolChain` in the above example are actually accessing multiple things, e.g. this method “scope” is `JavaPluginExtension` so it’s only applicable if the `java` plugin has been applied

Which is not a problem, because if the `java` plugin is not applied, you do not get hold of an instance of that extension and thus cannot call the extension function.

> and this method is also accessing `Project` items like `tasks`. Such method cannot be moved in a common `.kt` because the global scope won’t be `Project`. How should I approach the problem ?

Well, that could be a problem. I’m not sure how to mitigate that out of the box, because I usually don’t provide Kotlin DSL specific things like extension functions, but do it in a DSL agnostic way. So for example, I’d register an extension on the project or on the java plugin extension or wherever it should be, that contains the method to be called and that gets the project on instantiation, …

> I was thinking about creating extension properties on `JavaPluginExtension` in a regular kt file, and access them from the script with more or less the content of the method above. How does that sound ?

I’m not fully sure what you mean, but if it works for you, why not? 🙂

> Another exemple in a `common.kt`, this property wouldn’t compile, because `objects` is not accessible from `JavaPluginExtensions` (only form `Project`).

Yeah, same as above. Do it the Gradle way, not the Kotlin way and it will probably work much better.  
So e.g. add an extension called `myJvmVendor` with the `Property<JvmVendorSpec>` public type and the `objects.property` call result as instance, or similar.

---

<div class="post-metadata">

**Author:** ![bric3](https://sea1.discourse-cdn.com/gradle/user_avatar/discuss.gradle.org/bric3/32/9458_2.png) [@bric3](https://discuss.gradle.org/u/bric3)\
**Post date:** [June 4, 2023, 10:39pm UTC](https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755/6 "2023-06-04T22:39:25Z")

</div>

Thank you ! Indeed using an extension is clearly a better approach I think.

**EDIT: ⚠ DISCLAIMER this code has some problems as discussed below**

For future readers I can now write some thing like this now :

```plaintext
plugins {
    id("sandbox.java-conventions")
}

javaConvention {
    languageVersion = 20
    useRelease.set(true)
    addedModules.addAll("jdk.incubator.foreign")
}

```

This is baked by the interface declared in a `.kt` file

```plaintext
interface JavaConventionExtension {
    val languageVersion: Property<Int>
    val useRelease: Property<Boolean>
    val addedModules: ListProperty<String>
}

```

The convention plugin registers the extension and default values

```plaintext
val javaConventions: JavaConventionExtension =
    project.extensions.create(
        "javaConvention",
        JavaConventionExtension::class.java
    )
javaConventions.languageVersion.convention(11)
javaConventions.useRelease.convention(true)

```

And configures java extension and tasks

```gradle
java {
    toolchain {
        languageVersion.set(javaConventions.languageVersion.map(JavaLanguageVersion::of))
    }
}

tasks {
    withType<JavaCompile> {
        if (javaConventions.useRelease.get()) {
            options.release.set(javaConventions.languageVersion)
        } else {
            sourceCompatibility = javaConventions.languageVersion.get().toString()
            targetCompatibility = javaConventions.languageVersion.get().toString()
        }
        options.compilerArgumentProviders.add(
            javaConventions.addedModules
                .map {
                    CommandLineArgumentProvider {
                        it.map { "--add-modules=$it" }
                    }
                }
                .get()
        )
    }

    // ...
}

```

---

<div class="post-metadata">

**Author:** ![Vampire](https://sea1.discourse-cdn.com/gradle/user_avatar/discuss.gradle.org/vampire/32/9082_2.png) [@Vampire](https://discuss.gradle.org/u/Vampire)\
**Post date:** [June 4, 2023, 10:54pm UTC](https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755/7 "2023-06-04T22:54:10Z")

</div>

Did you test that?  
I don’t think it will work.

You use `tasks.withType<...> { ... }` which per-se is bad, as it eagerly configures all tasks of that type, instead you should almost always do `tasks.withType<...>().configureEach { ... }`.

But either way you evaluate your logic as soon as those tasks are configured.  
And while doing so, you eagerly evaluate your extension properties, making their laziness void.

When using `configureEach`, you will maybe not recognize the problem, as it will only happen if something then eagerly configures those tasks before the extension is configured.

But without the `configureEach` it should already break, as you break the task-configuration avoidance yourself and thus eagerly configure the tasks and thus eagerly evaluate your extension.

You instead need to wire providers together if possible and if not possible find other ways.  
You could for example instead of having a `val useRelease: Property<Boolean>`, have a `fun useRelease()` and in that do the according logic to react to the consumer calling that function. Or `fun useRelease(doIt: Boolean, languageVersion: Int)` or something like that.

---

<div class="post-metadata">

**Author:** ![bric3](https://sea1.discourse-cdn.com/gradle/user_avatar/discuss.gradle.org/bric3/32/9458_2.png) [@bric3](https://discuss.gradle.org/u/bric3)\
**Post date:** [June 4, 2023, 11:30pm UTC](https://discuss.gradle.org/t/exposing-an-api-via-kotlin-dsl-script-plugin/45755/8 "2023-06-04T23:30:10Z")

</div>

Ha yes, I noticed the issue, when moving the configuration from modules to convention I forgot to report the `.configureEach`, added it after writing the answer ! But you uncovered a few things I wasn’t entirely aware. Typically I experienced the problem you described in a submodule that was configuring the task before my extension.

In the convention I’m trying to set up `useRelease` is just about using the `--release` on `javac`, so a `fun useRelease()` should really only act on the `JavaCompile` regardless of the version. That said I think I get the point of having an actual function implementation that configuring the elements mentioned above.  
I need to wrap around my head around this.
