Hi all! I am looking for some information on how t...
# gradle
o
Hi all! I am looking for some information on how to properly set Kotlin compiler options in Gradle as of today. I have several ways that seem to do the same thing, but googling for the right spot of documentation is actually very tricky... First:
Copy code
kotlin {
  compilerOptions {
    // ...
  }
}
vs
Copy code
tasks.withType<KotlinCompile> {
  compilerOptions {
    // ...
  }
}
Do these two kinds of approaches do the exact same thing or not? Which one is newer, which one preferred? (and why?) Then I got a real problem understanding the difference or meaning of setting the Java toolchain using
Copy code
kotlin {
  jvmToolchain(17)
}
and setting the jvm target set via
Copy code
kotlin {
  compilerOptions {
    jvmTarget.set(JvmTarget.JVM_17)
  }
}
I wanted to use a newer toolchain (I thought this meant which Java tools to use building) and set the jvmToolchain to 21... This leads to this error:
Inconsistent JVM-target compatibility detected for tasks 'compileJava' (21) and 'compileKotlin' (17).
In order to get rid of this, I now also had to add this setting:
Copy code
java.sourceCompatibility = JavaVersion_17
where I honestly don't even understand why this is needed - I have no Java source files at all and the doc says that this defines which Java byte code is generated from Java source code. All in all: is there a good source to read about all of these things in one place that I can read and then really do the correct thing? πŸ˜‰ Thanks!
βž• 4
tldr, it's not as easy as it could but you should be using Kotlin
compilerOptions.jvmTarget
. I'm also not 100% clear why Kotlin-only project would need to care about Java but it's been like this for a while now.
t
I'm also not 100% clear why Kotlin-only project would need to care about Java but it's been like this for a while now.
Because in Kotlin/Jvm project Gradle Java plugin controls publishing attributes even if there is no Java sources.
In this case Gradle Java plugin is ecosystem plugin which controls the project. In KMP case - it is Kotlin plugin which controls the project.
m
Because in Kotlin/Jvm project Gradle Java plugin controls publishing attributes even if there is no Java sources.
Ahhh publishing... It could probably be the other way around though? Have Kotlin
jvmTarget
set
java.sourceCompatibility
etc...?
t
it is hard as Gradle Java itself also configure it within Gradle. So it will a really fragile approach
m
Yup, mutable state....
Ideally (but we're probably really far from it), I'd love the Kotlin plugin to wrap the Java one completely, i.e. there would be no valid use case for configuring
java {}
if you have
org.jetbrains.kotlin.jvm
in your build. If something really needs to be set, I'd have to go through
kotlin {}
first so that KGP owns what's happening.
But yea, it's not really how Gradle is designed right now, you can have distinct plugins competing on some values...
t
we had a discussion with Gradle developers on this topic and there is a possible solution but it will require a lot of changes in the Gradle model and decoupling it from Java plugins.
πŸ‘€ 1
πŸ‘ 1
m
One day πŸ™‚ 🀞
o
So what I did for a long time was this:
Copy code
java.sourceCompatibility = JavaVersion.VERSION_17
tasks.withType<KotlinCompile> {
    compilerOptions {
        jvmTarget.set(JvmTarget.fromTarget(java.sourceCompatibility.toString()))
    }
}
But when I read what
java.sourceCompatibility
sets according to the docs:
Copy code
Sets the source compatibility used for compiling Java sources
I decided to remove that in my Kotlin only projects... That's when the problems started and I tried to understand what really happens under the hood. So, can somebody elaborate on the difference between
Copy code
kotlin {
  ...
}
and
Copy code
tasks.withType<KotlinCompile> {
  ...
}
Does one do more or less than the other? And what exactly is controlled by the toolchain setting? As I build in a GitHub action, I also install the JDK and specifically select e.g. JDK 17. So what would the toolchain setting even do then?
v
You do not only build in GitHub, you or any other person also builds locally. πŸ˜‰ Never use
tasks.withType<...> { ... }
anywhere! You completely break task-configuration avoidance for all tasks of that type. If so, then use
tasks.withType<...>().configureEach { ... }
which is task-configuration avoidance-safe. The difference between your approach 1 and the fixed 2 is, that 1 configures an extension and thus all tasks that happen to be wired to that extension, while 2 configures all tasks of the given type no matter where they are registered.
o
Wow, I have some projects with
configureEach
and some without! As the lambda you attach after
tasks.withType<...>
has the same receiver (KotlinCompile), I was under the impression they do the exact same thing! How on earth would I find this out - and why don't I have any problems?
v
The problem is, that you waste time as on each build all those tasks need to be configured even if you do not run them later on
There is not functional problem
And how you know it is by following performance-optimization-recommendations πŸ™‚
o
Even the documentation on
configureEach
does not give ANY hint that this is the "best" way to configure task. It says:
Copy code
Configures each element in this collection using the given action, as each element is required. Actions are run in the order added.
But it does not say: USE me. If you don't, something bad happens. Why does
withType
have an overload method to accept the same kind of code that
configureEach
has? This is bad by design...
@Vampire ah, ok. But in my case, it's a Kotlin project so I guess all Kotlin compile tasks are actually needed! πŸ˜‰
v
Even the documentation on
configureEach
does not give ANY hint that this is the "best" way to configure task.
configureEach
does not know what a "task" is and also it is not said that it is always the best thing to do, depends on the situation like any API, but usually you want
configureEach
to configure a collection of domain objects.
Why does
withType
have an overload method to accept the same kind of code that
configureEach
has? This is bad by design...
Backwards compatibility
o
...Gradle is such a complicated beast. When you focus on your product development it's a horror to also be a Gradle expert... 😞
v
But in my case, it's a Kotlin project so I guess all Kotlin compile tasks are actually needed
Very unlikely
o
if
withType
has this overload for backward compatibility, why don't they deprecate this overload and explain to use
configureEach
?
v
If you for example do
./gradlew help
why should you configure kotlin compile tasks? If you do
./gradlew run
why should you compile Kotlin test classes? If you do
./gradlew whateverThingThatDoesNotNeedEachAndEveryKotlinFileCompiled
why should you configure all Kotlin Compile tasks?
πŸ˜‰ 1
o
Yes, I know what you mean. But never noticed a slow response on any of those sometimes used tasks...
But your explanation was cool, thank you very much! The only problem that remains is: how can I ever be sure to make things the recommended way? Why isn't there a good linter / help in the IDE to keep Gradle build scripts up to date?
v
why don't they deprecate this overload
Ask them. But probably why deprecate a valid method, there are use-cases for it. But actually it should be the same as
tasks.withType<...>().all { ... }
I think
how can I ever be sure to make things the recommended way?
You cannot πŸ˜„ Especially as Gradle is actively developed and evolving, so things change. What was recommended and idiomatic yesterday might be bad-practice tomorrow.
So you would need to read release notes and blog posts for example to stay up to date or follow conversations in communities.
o
I guess the main reason is that tools are not on the same level as e.g. Kotlin warnings in IntelliJ. A novice Kotlin developer can lean tons of things simply by keeping the number of warnings at exactly 0 and for every finding, read about it and apply the QuickFix. I am impressed about what can be achieved by that. This level of tooling is simply missing in Gradle, it always feels weird, sometimes you even can't get proper documentation for plugins, sometimes only weird Groovy stuff... not easy!
The mixture of declarative vs imperative things and the necessity to understand the model underneath the surface is tough...
@Vampire so, now that I understood
withType
and `configureEach`: what does using plain
kotlin { ... }
do? Does it also configure ALL tasks of KotlinCompile? In the "correct" lazy way? And what does setting the toolchain do, if I put it there vs. inside
configureEach
?
So, I learned about
configureEach
on
tasks.withType<...>
. Does this also apply to
tasks.register<...>
? I have this without
configure
, but plain
Copy code
tasks.register<Copy> {
  from(....)
}
Is that bad as well? Same problem?
v
what does using plain
kotlin { ... }
do?
Does it also configure ALL tasks of KotlinCompile?
As I said, it configures a project extension that the Kotlin plugin added. It configures all tasks that are wired to this extension. Whether this means all tasks of type
KotlinCompile
depends on your build scripts and the plugins you apply.
tasks.register<Copy> {
from(....)
}
No, that is fine.
tasks.register
is the lazy way to create a task and so
tasks.register<Copy> { ... }
and
tasks.register<Copy>().configure { ... }
are the same.
Unfortunately before the lazy stuff like
tasks.register
were introduced there were already the non-lazy ways. And for those there were convenience methods like
tasks.withType(...) { ... }
that then of course used the only available non-lazy way. Then the lazy stuff was added and you cannot simply change the meaning of the method to behave the lazy way as that would be potentially breaking.
m
My hot take is KGP should probably hide
KotlinCompile
as an implementation detail altogether. Historically tasks have been part of the (public) Gradle model for most of the plugins but I see them more and more as implementation details.
πŸ‘ 1
I personally prefer
Copy code
kotlin {
  compilerOptions {
    // ...
  }
}
(There's actually a 3rd way going through the compilations that is probably my preferred one but for simple projects, configuring everything in the extension should be fine)