solonovamax
03/08/2025, 10:31 PMfun 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:
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`:
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`:
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.tapchicoma
03/10/2025, 9:11 AMVampire
03/12/2025, 12:23 AMVampire
03/12/2025, 12:25 AMid("ca.solo-studios.nyx") version "0.3.0-20250308.223428-37"
Vampire
03/12/2025, 12:39 AMkotlin("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, ...Vampire
03/12/2025, 12:52 AMKotlinJvmProjectExtension
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.tapchicoma
03/12/2025, 8:34 AMCaused 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 GradleVampire
03/12/2025, 10:57 AMsolonovamax
03/13/2025, 5:56 AMsolonovamax
03/13/2025, 5:57 AMafterEvaluate
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.solonovamax
03/13/2025, 6:01 AMKotlinJvmProjectExtension
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:
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)Vampire
03/13/2025, 4:08 PMno, you cannotOh, 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 pluginThat'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 theWhy 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.function, though.configureJvmCompilerOptions
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 aExactly, 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., and it can be, before attempting the cast.HasConfigurableKotlinCompilerOptions
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.
Vampire
03/13/2025, 4:09 PMconfigureJvmCompilerOptions
not an extension function of HasConfigurableKotlinCompilerOptions
but of KotlinJvmProjectExtension
and that line should become compatible with KGP <2.1.0solonovamax
03/13/2025, 6:56 PMkotlin("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.solonovamax
03/13/2025, 7:26 PMproject.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, 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.Vampire
03/13/2025, 10:11 PMif 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
.Vampire
03/13/2025, 10:27 PMif 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 withOk, 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
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?Vampire
03/13/2025, 10:27 PMsolonovamax
03/16/2025, 6:39 PMDo 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