Gradle forcefully brings you the incorrect version of Kotlin dependencies

Viewed 1668

There are several questions here on Stackoverflow regarding the Kotlin compiler warning when different versions of Kotlin jars are mixed in the classpath.

This question is a different one and concerns the situation when you are developing Gradle plugins using Kotlin. When you have at least one of the following:

  • kotlin-dsl plugin is applied;
  • java-gradle-plugin plugin is applied;
  • gradleApi() dependency is added;
  • gradleKotlinDsl() dependency is added;

And you have Kotlin plugin like kotlin("jvm") version "1.4.10", e.g.:

plugins {
    java
    kotlin("jvm") version "1.4.10"
    `kotlin-dsl`
    `java-gradle-plugin`
}

You will get famous:

w: Runtime JAR files in the classpath should have the same version. These files were found in the classpath:

bla-bla-bla/gradle-6.7/lib/kotlin-stdlib-1.3.72.jar (version 1.3)
bla-bla-bla/modules-2/files-2.1/.../kotlin-stdlib-jdk8-1.4.10.jar (version 1.4)
and so on for every Kotlin jar

w: Consider providing an explicit dependency on kotlin-reflect 1.4 to prevent strange errors
w: Some runtime JAR files in the classpath have an incompatible version. Consider removing them from the classpath

Here Kotlin 1.3.72 is the embedded version for Gradle 6.7, and Kotlin 1.4.10 is the one you added manually. The problem is with gradleApi() or gradleKotlinDsl() because they add Kotlin jars directly as local filesystem files, thus bypassing Gradle dependency versions resolution.

2 Answers

The only way I found to fix this problem is to remove incorrect dependencies before incorrect configurations got resolved. Here is a part of my build.gradle.kts:

import org.gradle.api.internal.artifacts.dependencies.DefaultSelfResolvingDependency
import org.gradle.api.internal.artifacts.dsl.dependencies.DependencyFactory.ClassPathNotation
import org.gradle.internal.component.local.model.OpaqueComponentIdentifier

plugins {
    java
    kotlin("jvm") version "1.4.10"
    `kotlin-dsl`
    `java-gradle-plugin`
}

project.afterEvaluate {
    // Stupid `kotlin-dsl` and `java-gradle-plugin` plugins adds gradle embedded Kotlin dependencies
    //  directly as files providing you with incorrect classpath with mixed versions of Kotlin jars.
    // This code removes such dependencies.

    // First create a copy of embedded Kotlin dependencies - we only need to remove Kotlin jars from them.
    configurations.create("gradlefix").also { cfg ->
        dependencies {
            add(cfg.name, gradleApi())
            add(cfg.name, gradleKotlinDsl())
        }
    }

    // Here are all non-Kotlin jars from gradleKotlinDsl and gradleApi.
    val neededEmbeddedDependencies = configurations["gradlefix"].files
        .filterIsInstance<File>()
        .filterNot {
            it.name.startsWith("kotlin-")
                    && it.name.endsWith(".jar")
        }

    // Now remove embedded Kotlin from all configuration, but keep needed embedded jars.
    // It is expected that configurations are not yet resolved.
    configurations
        .filterNot { it.name == "gradlefix" }
        .forEach { cfg ->
            cfg.resolutionStrategy.eachDependency {
                // Needed if you use chain of `includeBuild`s
                if (requested.group == "org.jetbrains.kotlin") {
                    useVersion("1.4.10")
                }
            }

            val removed = cfg.dependencies.removeIf {
                val notation = ((it as? DefaultSelfResolvingDependency)
                    ?.targetComponentId as? OpaqueComponentIdentifier)
                    ?.classPathNotation

                notation == ClassPathNotation.GRADLE_API
                        || notation == ClassPathNotation.GRADLE_KOTLIN_DSL
            }

            if (removed) {
                dependencies {
                    add(cfg.name, project.files(*neededEmbeddedDependencies.toTypedArray()))
                }
            }
        }
}

dependencies {
    implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    // implementation(gradleApi()) // already added by java-gradle-plugin plugin
    // implementation(gradleKotlinDsl()) // already added by kotlin-dsl plugin
}

I came to a similar solution, but very different still:

I found that I can undo adding default named Gradle dependencies (gradleApi() and gradleKotlinDsl() and gradleTestKit()) and provided my own version of those which don't contain the embedded Kotlin dependencies.

You can find it in real context here

fun Project.replaceGradlePluginAutoDependenciesWithoutKotlin() {
    plugins.withId("org.gradle.java-gradle-plugin") {
        @Suppress("DEPRECATION")
        dependencies {
            // Undo org.gradle.plugin.devel.plugins.JavaGradlePluginPlugin.applyDependencies
            if (configurations[JavaPlugin.COMPILE_CONFIGURATION_NAME].dependencies.remove(gradleApi())) {
                add(JavaPlugin.COMPILE_CONFIGURATION_NAME, gradleApiWithoutKotlin())
            }

            // Undo org.gradle.plugin.devel.plugins.JavaGradlePluginPlugin.TestKitAndPluginClasspathDependenciesAction
            afterEvaluate {
                gradlePlugin.testSourceSets.forEach {
                    if (configurations[it.compileConfigurationName].dependencies.remove(gradleTestKit())) {
                        add(it.compileConfigurationName, gradleTestKitWithoutKotlin())
                    }
                }
            }
        }
    }

    plugins.withId("org.gradle.kotlin.kotlin-dsl.base") { // applied from org.gradle.kotlin.kotlin-dsl
        dependencies {
            // based on org.gradle.kotlin.dsl.plugins.embedded.kotlinArtifactConfigurationNames (in EmbeddedKotlinPlugin.kt)
            val kotlinArtifactConfigurationNames = listOf(
                JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME,
                JavaPlugin.TEST_IMPLEMENTATION_CONFIGURATION_NAME
            )
            kotlinArtifactConfigurationNames.forEach {
                // Undo org.gradle.kotlin.dsl.plugins.base.KotlinDslBasePlugin.addGradleKotlinDslDependencyTo
                if (configurations[it].dependencies.remove(gradleKotlinDsl())) {
                    add(it, gradleKotlinDslWithoutKotlin())
                }
            }
        }
    }
}

fun DependencyHandler.gradleApiWithoutKotlin(): Dependency =
    withoutKotlin(ClassPathNotation.GRADLE_API)

fun DependencyHandler.gradleKotlinDslWithoutKotlin(): Dependency =
    withoutKotlin(ClassPathNotation.GRADLE_KOTLIN_DSL)

fun DependencyHandler.gradleTestKitWithoutKotlin(): Dependency =
    withoutKotlin(ClassPathNotation.GRADLE_TEST_KIT)

private fun DependencyHandler.withoutKotlin(notation: ClassPathNotation): Dependency {
    // Originally created in org.gradle.api.internal.notations.DependencyClassPathNotationConverter.create
    val gradleApi = create(notation) as FileCollectionDependency
    val filteredSource = gradleApi.files.filter { !it.name.startsWith("kotlin-") }
    val displayName = OpaqueComponentIdentifier("${notation.displayName} (without Kotlin)")
    return DefaultSelfResolvingDependency(displayName, filteredSource as FileCollectionInternal)
}
Related