Hi, I am trying to make a gradle plugin which conf...
# gradle
s
Hi, I am trying to make a gradle plugin which configures the kotlin compiler options for a project. in my plugin's code, I have something which looks roughly like this:
Copy code
fun configureProject() {
    project.configure<KotlinProjectExtension> {
        if (this is HasConfigurableKotlinCompilerOptions<*>)
            configureCommonCompilerOptions()

        when (this) {
            is KotlinJvmProjectExtension    -> {
                configureJvmCompilerOptions()
            }

            is KotlinMultiplatformExtension -> {
                targets.withType<KotlinJvmTarget>().configureEach {
                    configureJvmCompilerOptions()
                }

                targets.withType<KotlinJsIrTarget>().configureEach {
                    configureJsCompilerOptions()
                }
            }
        }
    }
}

private fun HasConfigurableKotlinCompilerOptions<*>.configureCommonCompilerOptions() {
    val nyx = this@NyxKotlinExtension
    compilerOptions {
        // ...
    }
}
however, the problem I'm facing is that when I add this plugin to another project and this code gets executed, then the following exception will be thrown:
Copy code
Caused by: java.lang.ClassCastException: class org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension_Decorated cannot be cast to class org.jetbrains.kotlin.gradle.dsl.HasConfigurableKotlinCompilerOptions (org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension_Decorated and org.jetbrains.kotlin.gradle.dsl.HasConfigurableKotlinCompilerOptions are in unnamed module of loader org.gradle.internal.classloader.VisitableURLClassLoader$InstrumentingVisitableURLClassLoader @47f6e669)
this makes absolutely no sense as to why it's occurring, as
KotlinJvmProjectExtension
definitely inherits from
HasConfigurableKotlinCompilerOptions
. if you would like to see exactly what I'm doing, then that can be found here: https://github.com/solo-studios/nyx/blob/ae325df6221244572757ff661767621c8de01262/src/main/kotlin/ca/solostudios/nyx/plugin/compile/NyxKotlinExtension.kt#L370-L439 you can test this behaviour by doing the following in any gradle project: first, add the following to your `settings.gradle.kts`:
Copy code
pluginManagement {
    repositories {
        maven("<https://maven.solo-studios.ca/snapshots/>")
        mavenCentral()
        gradlePluginPortal()
    }
    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "ca.solo-studios.nyx") {
                useModule("ca.solo-studios:nyx:0.3.0-20250308.223428-37")
            }
        }
    }
}
then, add the following to your `build.gradle.kts`:
Copy code
plugins {
    id("ca.solo-studios.nyx") version "0.3.0-SNAPSHOT"
}
re-import the project, and it will fail. the source code for the entire project can be found on github. edit: I have published a workaround, so you need to do funny resolution stuff to get the broken version.
t
Basically you've been hit by Gradle classpath isolation problem
v
@tapchicoma can you elaborate on what exactly you mean that he has hit? I tried the "try this in any Gradle project" instructions and the project synced without any problem.
Also I don't know why that "funny resolution stuff" should be necessary, you can anytime use a concrete snapshot version like
id("ca.solo-studios.nyx") version "0.3.0-20250308.223428-37"
Even mitigating the false statement that it can be applied to any Gradle project and applying
kotlin("jvm")
so that the code in question is hit (verified by breakpoint) does not make anything fail. Besides that the plugin is packed with discouraged bad practices like many
afterEvaluate
usage,
project.plugins
usages, ...
Even not being able to reproduce the problem, I'd say it simply is a compatibility bug of the plugin. Only since Gradle 2.1.0
KotlinJvmProjectExtension
extends
KotlinJvmExtension
which implements
HasConfigurableKotlinCompilerOptions
. But the
configureJvmCompilerOptions
function which is called in the
is KotlinJvmProjectExtension
branch is an extension function on
HasConfigurableKotlinCompilerOptions
, which makes it unusable with Kotlin < 2.1.0 and there will lead to exactly that class cast exception as the invariant does not hold.
t
@Vampire I am referring to this exception pasted in the original message:
Copy code
Caused by: java.lang.ClassCastException: class org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension_Decorated cannot be cast to class org.jetbrains.kotlin.gradle.dsl.HasConfigurableKotlinCompilerOptions (org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension_Decorated and org.jetbrains.kotlin.gradle.dsl.HasConfigurableKotlinCompilerOptions are in unnamed module of loader org.gradle.internal.classloader.VisitableURLClassLoader$InstrumentingVisitableURLClassLoader @47f6e669)
This can happen when plugin tries to cast to a type a class that is in the separate classload which could happen in Gradle
v
Yes and no. If you look closely at the error message, you see that both classes are in the same class loader, or the message would be slightly different, showing the class loader of each class separately. In this case, as I said, the classes are coming from the same class loader, but are not related, which should mean that the cast was compiled against Kotlin 2.1+ where it is valid, but executed against a previous Kotlin version where the classes were unrelated.
👌 1
s
"Also I don't know why that "funny resolution stuff" should be necessary, you can anytime use a concrete snapshot version like `id("ca.solo-studios.nyx") version "0.3.0-20250308.223428-37"`" no, you cannot. if you look at the maven metadata that is published with any snapshot version, you will see that it will always depend on the latest snapshot version: https://maven.solo-studios.ca/snapshots/ca/solo-studios/nyx/ca.solo-studios.nyx.gradle.plugin/0.3.0-SNAPSHOT/ca.solo-studios.nyx.gradle.plugin-0.3.0-20250308.233824-39.pom
"Besides that the plugin is packed with discouraged bad practices like many
afterEvaluate
usage,
project.plugins
usages, ..." yes, I am aware it is extremely bad practice and a massive hack (I do not like it either), however there isn't really any other way to do some of the things I want to do with my plugin, sadly.
"Even not being able to reproduce the problem, I'd say it simply is a compatibility bug of the plugin. Only since Gradle 2.1.0
KotlinJvmProjectExtension
extends
KotlinJvmExtension
which implements
HasConfigurableKotlinCompilerOptions
. But the
configureJvmCompilerOptions
function which is called in the
is KotlinJvmProjectExtension
branch is an extension function on
HasConfigurableKotlinCompilerOptions
, which makes it unusable with Kotlin < 2.1.0 and there will lead to exactly that class cast exception as the invariant does not hold." the issue is not with the
configureJvmCompilerOptions
function, though. I'm pretty sure that it occurs specifically here:
Copy code
if (this is HasConfigurableKotlinCompilerOptions<*>)
    configureCommonCompilerOptions()
so, just prior I had checked if it can be cast to a
HasConfigurableKotlinCompilerOptions
, and it can be, before attempting the cast. also, iirc, this issue was occurring in projects where the kotlin version was >= 2.1.0 (though I'm not 100% sure on this and would have to double check it)
v
no, you cannot
Oh, of course sorry. I have the flu so am not fully fit mentally. But that explains why I was not able to reproduce it. After putting the resolution config in place again, I was able to reproduce and it confirms exactly what I said. With
kotlin("jvm") version "2.0.0"
the error comes, with
kotlin("jvm") version "2.1.0"
it works.
there isn't really any other way to do some of the things I want to do with my plugin
That's quite unlikely actually. 🙂 Assuming you talk about the
afterEvaluate
, because whatever you can do with
project.plugins
you should be able to do with
project
directly or with
project.pluginManager
too and in the recommended way, just like the JavaDoc of
project.plugins
recommends. 🙂 I've seen almost no case that was not solvable better. For some other solutions the consumer DSL changes, but that is usually still preferable. Preferable, because the main earning you get from using
afterEvaluate
is timing problems, ordering problems, and race conditions. There are very little cases I've seen where it really needs to be used. One case was when a plugin evilly uses
afterEvaluate
and you need to use it yourself to re-configure something the plugin configured in
afterEvaluate
for example. I didn't analyse your usages, I just mentioned it, because often they can also cause strange behaviour, although here it is not the case as we know now. Feel free to describe why you think you need to use
afterEvaluate
then I might be able to suggest alternative patterns. For example if you use it to check whether some plugin is applied and react to that, use
pluginManager.withPlugin
instead. Or if you use it to read a value the consumer has set to configure some task property, better use lazy properties. Or if you use it to read a value the consumer has set that you need at configuration time to change some non-lazy configuration or create some domain objects, better not have properties the consumer sets, but have a function in your extension that the consumer sets with the needed values and do the configuration change in the implementation of that function, eventually preventing multi-call of the function if the first call cannot be "undone" in the second call but would need to be. ...
the issue is not with the
configureJvmCompilerOptions
function, though.
Why do you think so? Did you debug? The logic says differently, the stacktrace says differently, my debugger says differently now that I was able to reproduce the issue.
I'm pretty sure that it occurs specifically here:
```if (this is HasConfigurableKotlinCompilerOptions<*>)
configureCommonCompilerOptions()```
so, just prior I had checked if it can be cast to a
HasConfigurableKotlinCompilerOptions
, and it can be, before attempting the cast.
Exactly, you check that it can be casted, so it is impossible that the cast cannot succeed. If that would be possible, that would be Kotlin or JVM bug, but it is not.
also, iirc, this issue was occurring in projects where the kotlin version was >= 2.1.0 (though I'm not 100% sure on this and would have to double check it)
Then please double-check. As I said, not that I put the resolution configuration in place again, I can reproduce with <2.1.0 and not with >=2.1.0. And like I described it is also how it makes sense and also what the debugger not confirms.
Or in other words, make
configureJvmCompilerOptions
not an extension function of
HasConfigurableKotlinCompilerOptions
but of
KotlinJvmProjectExtension
and that line should become compatible with KGP <2.1.0
s
> Oh, of course sorry. > I have the flu so am not fully fit mentally. > But that explains why I was not able to reproduce it. > After putting the resolution config in place again, I was able to reproduce and it confirms exactly what I said. > With
kotlin("jvm") version "2.0.0"
the error comes, with
kotlin("jvm") version "2.1.0"
it works. no worries yeah, after further attempts to reproduce, it definitely seems to be an issue only when using kotlin <2.1.0. My bad on that, you were absolutely right initially. > Feel free to describe why you think you need to use
afterEvaluate
then I might be able to suggest alternative patterns. I'm using
afterEvaluate
primarily because I want to only override the default, if some property is set. otherwise, I want to leave the default unchanged. If I wanted to change the default, then I could just set the convention for a specific property, however this does not work for me because it overrides the default, which I want to leave intact unless a certain property is set. if it was possible to have a reactive/listenable property, where I could execute some block of code whenever a property is modified, then that would honestly be the ideal solution for me. another reason I have to use it is because of some of the plugins I'm configuring are extremely finicky and do some odd things (specifically: the fabric loom & neogradle gradle plugins). I forget what the exact issues I had were, however iirc they could only be solved with
afterEvaluate
. > Then please double-check. > As I said, not that I put the resolution configuration in place again, I can reproduce with 2.1.0 and not with =2.1.0. > And like I described it is also how it makes sense and also what the debugger not confirms. yeah, I was wrong on that one, my bad. it is definitely an issue with the kgp version I was using.
> because whatever you can do with
project.plugins
you should be able to do with
project
directly or with
project.pluginManager
too and in the recommended way, I didn't realise that
project.plugins
should be avoided, so I'm going around and changing that now. however, from what I can tell, there's no good way to do one very specific thing with the `project.pluginManager`: in a few cases, I want to apply some configuration when a plugin with a specific class is applied, and I don't think there is a good way to do this. there is no reliable plugin id I can use in this case, which is what makes it tricky. some of the plugins that use this class include, but is not limited to, • fabric-loom • quilt-loom • architectury-loom • crystaelix's fork of architectury-loom • quiet-loom • essential's fork of architectury-loom • polyfrost's fork of essential's fork of architectury-loom • & more I do also use it for detecting if any of the kotlin gradle plugin variants (ie. kotlin/jvm, the now deprecated kotlin/js, kotlin/multiplatform, etc.) have been applied, however iirc they also apply a common base plugin id, so I'm probably going to use that instead. I checked, and it doesn't seem like they apply a common plugin, so I guess I can instead just do something similar what is done by the kotlin gradle plugin, but with all of the different kgp plugin ids:
Copy code
fun Project.runAgpCompatibilityCheckIfAgpIsApplied(
    agpVersionProvider: AndroidGradlePluginVersionProvider = AndroidGradlePluginVersionProvider.Default
) {
    val wasChecked = AtomicBoolean(false)
    androidPluginIds.forEach { agpPluginId ->
        plugins.withId(agpPluginId) {
            if (!wasChecked.getAndSet(true)) {
                checkAgpVersion(agpVersionProvider, agpPluginId)
            }
        }
    }
}
however, this wouldn't work with any of the loom forks because I want to keep compatibility with all forks of it, some of which I may not know about. of course if the api differs then compatibility will break, so I won't account for that, but if the api is the same then I want to keep compatibility.
v
if it was possible to have a reactive/listenable property, where I could execute some block of code whenever a property is modified, then that would honestly be the ideal solution for me.
Well, nothing prevents you from implementing your own listenable properties I guess. another reason I have to use it is because of some of the plugins I'm configuring are extremely finicky and do some odd things (specifically: the fabric loom & neogradle gradle plugins). I forget what the exact issues I had were, however iirc they could only be solved with
afterEvaluate
.
if it was possible to have a reactive/listenable property, where I could execute some block of code whenever a property is modified, then that would honestly be the ideal solution for me.
Well, nothing prevents you from implementing your own listenable properties I guess. Built-in this is indeed not yet available. I mean to remember a feature request for them but cannot find it right now. An easy approach would be to - as I said - not have a property, but have a function to set that value. You can still use the argument to set some property if you then need that for further lazy task-dependency preserving wiring, but by having the function called by the end user you can additionally react by changing the mentioned default if appropriate.
another reason I have to use it is because of some of the plugins I'm configuring are extremely finicky and do some odd things (specifically: the fabric loom & neogradle gradle plugins). I forget what the exact issues I had were, however iirc they could only be solved with
afterEvaluate
.
Ok, yeah, that could well be one of the cases I described above. Both of these plugins (from a quick code-search on GH) use
afterEvaluate
and so if you need to do something after they did that, you have no other chance. But at the same time you should make those plugins stop using bad-practice, then you might in the end be able to also do so. 🙂
some of the plugins that use this class include, but is not limited to,
Do you mean that all these plugins use the same FQCN plugin class but different IDs? Well, that is a very strange situation then actually and you might indeed need to use the discouraged
plugins
for that. At least I don't have a better way in mind either if that is the situation you have to handle. For the KGP plugins, there should only be those three, shouldn't it? K/JVM, K/JS, and KMP. I would simply check for those three explicitly if there is no common base-plugin. You need different code for each anyway, don't you?
All in all, sounds not being big fun to maintain that plugin 😄
s
Do you mean that all these plugins use the same FQCN plugin class but different IDs?
yep it's odd, but it's what I'm working with
For the KGP plugins, there should only be those three, shouldn't it? K/JVM, K/JS, and KMP.
I would simply check for those three explicitly if there is no common base-plugin.
yeah, that's what I'm moving to.
You need different code for each anyway, don't you?
they do share some code between them, as they share the same common
KotlinBaseExtension
, which all the plugin-specific extensions inherit from