Using the `kotlin-dsl` Gradle Plugin forces Kotlin 1.4 Compatibility 😱

I previously wrote an article “Sharing Gradle Configuration in Multi-Module Android Projects” about re-using Gradle configuration using the “apply” feature to reduce boilerplate and provide consistency. It’s super helpful and handy, but there is now a better way to do the same kind of thing using Gradle Convention Plugins.

Gradle Convention Plugins

There is a great post by Tony Robalik that goes into the benefits of Gradle Convention Plugins. I say it is “better” because it can be pre-compiled, written in Kotlin and tested in Kotlin. These convention plugins are most easily added in buildSrc, so I figured I’d start there when adding it to my existing project.

My journey writing a Gradle Convention Plugin and how I ran into Kotlin 1.4

So, in a large Android project I added the “kotlin-dsl” plugin to the buildSrc module and things blew up. I’m using the latest version of Gradle 7.4.2, yet it is telling me:

Language version 1.4 is deprecated and its support will be removed in a future version of Kotlin

I had specified Kotlin 1.6.10 everywhere! What was I doing wrong?

Nothing. It’s intentional. Gradle even calls out why on their site:

Gradle plugins written in Kotlin target Kotlin 1.4 for compatibility with Gradle and Kotlin DSL build scripts, even though the embedded Kotlin runtime is Kotlin 1.5.

https://docs.gradle.org/current/userguide/compatibility.html#kotlin

Even though it is intentional, it wasn’t immediately clear to me, and to others, but I understand the thinking behind it now, and in this post show you how you can get around it if you need to.

Martin Bonnin has a great post talking about how you could get around this by doing some crazy things like creating shadow jars, but his answer of “Should I use this in production?” was “it depends”, and where possible, I try to not use complex workarounds (even though this post is a less complex workaround 😂).

Gradle 7.4.2 still targets Kotlin 1.4 with the kotlin-dsl plugin, even though 1.5.31 is embedded now in Gradle 7.4.2. Gradle plugin compatibility is very important when distributing plugins publicly, but if you are just going to use them in your team or organization, you may not want to support old version of Kotlin going back to 1.4.

In order to use the version of Kotlin available in the version of Gradle you have, you need to specify the version yourself to override the default.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    `kotlin-dsl`
}

afterEvaluate {
    tasks.withType<KotlinCompile>().configureEach {
        kotlinOptions {
            apiVersion = "1.5"
            languageVersion = "1.5"
        }
    }
}

This will allow you to use Kotlin 1.5 syntax now when using the “kotlin-dsl” plugin!

One thing that is weird about this is the need for afterEvaulate. I’m not 100% sure why it was needed, but it’s how they specify it in Gradle source code, and it doesn’t work without adding afterEvaluate.

But… what about Kotlin 1.6?

If you really want to use Kotlin 1.6 though, fear not! Kotlin 1.6.10 is going to be available with Gradle 7.5 when it comes out. When it does, you should be able to use this configuration to use 1.6 compatibility:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    `kotlin-dsl`
}

afterEvaluate {
    tasks.withType<KotlinCompile>().configureEach {
        kotlinOptions {
            apiVersion = "1.6"
            languageVersion = "1.6"
        }
    }
}

Kotlin/Gradle Version Table when using kotlin-dsl

Gradle VersionEmbedded Kotlin VersionDefault Api Version
7.5 (Unreleased as of April 14, 2022)1.6.101.4
7.3+1.5.311.4
7.0+1.4.301.4

What do you recommend?

If you are only going to use this plugin internally, and everyone is using the same version of Kotlin everywhere, this seems pretty safe. If you are looking to open source something for public use, you may need to call out a minimum Gradle version required use your Plugin.

Should I put my Gradle Convention Plugin in buildSrc?

This is the first place you should try it out. You will have the same issue with any code you put in buildSrc though, where if any code changes, all code and tests in buildSrc have to be re-run which increases build times.

If you plan to use this a lot going forward, use an includeBuild to add in your Convention Plugins going forward so that you only re-compile when that code changes, and so you could publish a binary to avoid any compilation at all.

Reviewers

Thanks Martin Bonnin and Tony Robalik for reviewing the article. Also special thanks to Martin for helping me dig into the Gradle source to figure this out!