I'm trying to convert a monolith Gradle project in...
# multiplatform
e
I'm trying to convert a monolith Gradle project into separate Gradle modules, with a no-code root. Now, my Gradle-fu is crap (I cry in Maven), so I don't get how to reduce the multiplatform configuration duplication. The root module simply imports the Multiplatform plugin, and then it applies it to every sub-modules.
Copy code
plugins {
  alias(libs.plugins.multiplatform) apply false
}

subprojects {
  apply(plugin = "org.jetbrains.kotlin.multiplatform")
}
Now, in every module I find myself with this code.
Copy code
kotlin {
  explicitApi = ExplicitApiMode.Warning

  jvm {
    jvmToolchain(11)
    withJava()

    testRuns["test"].executionTask.configure {
      useJUnitPlatform()
    }
  }

  js {
    moduleName = project.name
  }
}
If I need to change something I have to do it three+ times, which is tedious. How do I move this configuration at the root? Or at least, do it one time?
a
easy peasy - convention plugins! For some weird reason Gradle lets you use
subprojects {}
and
allprojects {}
, even though in practice they really screws up project configuration. The better way of sharing configuration is to... 1. create
buildSrc/build.gradle.kts
(example) and
buildSrc/settings.gradle.kts
(example) 2. add the Kotlin plugin as a regular dependency in
buildSrc/build.gradle.kts
- e.g.
Copy code
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
}
3. create some convention plugins - I prefer making one convention plugin per Kotlin target, so that I can apply each target specifically per subproject - but you could combine all of these into one single convention plugin • Kotlin/Native conventionKotlin/JS conventionKotlin/JVM convention 4. now apply the convention plugins in the plugins block of each subproject
Copy code
// my-cool-subproject/build.gradle.kts

plugins {
    buildsrc.conventions.lang.`kotlin-multiplatform-jvm`
    buildsrc.conventions.lang.`kotlin-multiplatform-js`
    buildsrc.conventions.lang.`kotlin-multiplatform-native`
}
(the convention plugin has an ID of the package name + the filename before
.gradle.kts
, e.g.
id("buildsrc.conventions.lang.kotlin-multiplatform-jvm"
)
e
Thanks @Adam S! Will give it a go later tonight. I think I'm missing some core Gradle concepts tho. Better if after this I step aside and read a Gradle book
👍 1
a
I can sympathise - Gradle is really difficult to learn! I haven't really found a great resources that helps unpick everything, but I can recommend these blogs/videos: • https://melix.github.io/blog/tags/gradle.htmlhttps://github.com/liutikas/gradle-best-practiceshttps://www.youtube.com/playlist?list=PLWQK2ZdV4Yl2k2OmC_gsjDpdIBTN0qqkEhttps://stackoverflow.com/q/71883613/4161471
e
@Adam S gave a look at the example and I think I got how it all works together. Pretty neat in the end! A question tho: after applying a convention plugin in a module, can I still access the KMP target and change its configuration? Example. The JS convention plugin you linked registers a
js
target
Copy code
kotlin {
    targets {
        js(IR) {
            browser()
            nodejs()
        }
    }
}
Will I be able to access that registered target and customize it further, if required?
👍 1
a
yes
✔️ 1
e
Perfect. On a different note, while I was previously targeting Kotlin 1.9.0, it seems it's forcing me on 1.8.20 right now. I think it's because of the kotlin-dsl plugin, even though I have requested
Copy code
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
Yeah, browsing the kotlin-dsl code I see
Copy code
/**
 * The version of the Kotlin compiler embedded in gradle-kotlin-dsl (currently _1.8.20_).
 */
val embeddedKotlinVersion = "1.8.20"
Not sure how you control the Kotlin version in the snakeyaml library
e
I'd create binary plugins to apply Kotlin conventions, due to the interaction with Gradle's embedded Kotlin version with script plugins
e
Gradle's embedded Kotlin version
Do you mean that there is no way out of the embedded version? Sounds like a pain since for what I've read around
buildSrc
stuff is used a lot
a
On a different note, while I was previously targeting Kotlin 1.9.0, it seems it's forcing me on 1.8.20 right now.
that's very unusual! I've never seen, or heard of, such an issue before. The classpaths should be separate... What are you seeing? What is being forced to use Kotlin 1.8.20? It's okay if the build scripts are using the embedded Kotlin version
e
@Adam S I'm experimenting with binary plugins right now, but with the script ones I was getting an error similar to "can't specify a version here because another one has been specified elsewhere"
Note that I get a similar error with a binary plugin, even tho I was able SOMEHOW (lol) to get 1.9.0 into the classpath Root:
Copy code
plugins {
  id("org.jetbrains.kotlin.multiplatform") version "1.9.0" apply false
  id("zproto.module") apply false
}
If I import the project again:
Copy code
Error resolving plugin [id: 'org.jetbrains.kotlin.multiplatform', version: '1.9.0', apply: false]
> The request for this plugin could not be satisfied because the plugin is already on the classpath with an unknown version, so compatibility cannot be checked.
a
I'm guessing that's because somewhere in your project you still applied a Kotlin Gradle plugin? If you specify the KGP in
buildSrc/build.gradle.kts
then you mustn't specify the version anywhere else.
Copy code
// some-other-subproject/build.gradle.kts
plugins {
  id("org.jetbrains.kotlin.multiplatform") // no version needed
}
yeah, this is another weird Gradle behaviour that's not obvious to figure out. Basically, there's a Java classpath for all Gradle build scripts. When you add a plugin (either in the plugins block, or in
buildSrc/build.gradle.kts
dependencies, or in one of the other 5 different ways that I won't go into!) it gets added to the build script classpath. But if you add a plugin with a different version in two different places, then Gradle does not like it, and you get the error.
👀 1
e
This is my current
buildSrc/build.gradle.kts
file:
Copy code
plugins {
  `embedded-kotlin`
  `java-gradle-plugin`
}

plugins.apply(SamWithReceiverGradleSubplugin::class.java)
extensions.configure(SamWithReceiverExtension::class.java) {
  annotations(HasImplicitReceiver::class.qualifiedName!!)
}

dependencies {
  implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
}

gradlePlugin {
  plugins {
    register("zproto.module") {
      id = "zproto.module"
      implementationClass = "zproto.build.plugins.ZProtoModulePlugin"
    }
  }
}
I can't see any import related to the Kotlin Multiplatform plugin. Is it imported by
kotlin-gradle-plugin
?
a
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
contains all Kotlin Gradle Plugins
e
Ah damn, I need to be more careful then. Didn't know that. Should I import more fine grained plugins instead of this all-in-one solution?
a
all in one :)
oh wait
sorry, I don't think I understand your question, but I would pick which ever option uses
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
e
Oh I was basically questioning if importing
kotlin-gradle-plugin
is indeed correct. Or if I should import multiple other plugins instead of that single one.
Because at this point is seems what drives the Kotlin version is
buildSrc
, which conceptually doesn't sound correct
a
adding a single dependency on the KGP artifact with all the Kotlin plugins is fine - in fact I don't think there's a reasonable way of separately adding a dependency on each Kotlin plugin
Because at this point is seems what drives the Kotlin version is buildSrc , which conceptually doesn't sound correct
Indeed, it is weird, but it's also nice to have a single place where you can define all plugins, and so you can ensure that the versions of all plugins will be consistent in all subprojects. You can help things a bit by specifying the KGP dependency in a version catalog and using in in
buildSrc/build.gradle.kts
. You just need to add some config to tell Gradle where the version catalog is in
buildSrc/settings.gradle.kts
I specify the Gradle plugins in
libs.version.toml
in Dokkatoo (the
gradlePlugin-
prefix is just a naming convention)
I'd recommend going back to using
kotlin-dsl
and precompiled script plugins, purely because they tend to be more succinct and easier to maintain. But a custom class + a plugin definition in
buildSrc/build.gradle.kts
will work perfectly well too.
e
You can help things a bit by specifying the KGP dependency in a version catalog
Ok! I guess if it's a sort of best approach I'll follow it.
I'd recommend going back to using
kotlin-dsl
and precompiled script plugins
Yeah I have noticed they're easer to read and implement. I need to get the multi-module setup working by tomorrow, so I guess I'll keep the binary one for now, and experiment on the weekend with the script one. One last question: how do I verify I'm really compiling with 1.9.0? Is there a task or a property that I can print out for this? I mean, I guess I could trust the dependency, but I'd like to be 100% sure I'm not importing older versions.
a
hmmm good question. There's a util value -
KotlinVersion.CURRENT
- that you can use in a build.gradle.kts.
never mind - that seems to pick up Gradle's embedded Kotlin version But maybe what you really want to find out is whether your library is compiled to be compatible with a certain Kotlin version? https://kotlinlang.org/docs/compatibility-modes.html
e
My question arises because I see this. Which is a bit weird
a
that'll be the combined list for both the classpath used to build the Gradle scripts, and the classpath used to build the project's source code
e
that'll be the combined list for both the classpath used to build the Gradle scripts
So basically Gradle 8.2.1 is using 1.8.20
I always think this would be less confusing if Gradle was written in Python or something non-JVM, because then it would be really clear which dependencies are for Gradle vs for the project :)
e
I've moved to use the version catalog now. I can use
libs
inside of
buildSrc/build.gradle.kts
, but not inside my binary plugin
Copy code
// Common test dependencies
kotlin.sourceSets.getByName("commonTest") {
  dependencies {
    implementation(kotlin("test"))
    implementation(libs.kotlinx.coroutines.test) // Error
  }
}
Is this expected?
a
there is a workaround, but I'd actually argue against not adding dependencies in convention plugins. Yes, it can be repetitive to have the same dependencies in the build.gradle.kts of each subproject, but it makes it a lot more clear and easier to discover which subprojects have which dependencies.
e
That's a fair point. Probably only the KMP targets configuration should be in the plugin. All dependencies should be declared for each module. All in all, coming from Maven it feels it's a bit half backed. In Maven I could have just created a
pluginManagement
entry with settings + applied it by id on each module.
a
Maven is certainly a lot more easier to understand and configure!
e
In the Dokkatoo project version catalogue I see
Copy code
[versions]
kotlin = "1.8.20" # should match Gradle's embedded Kotlin version
Is there a specific reason as to why you're explicitly matching the exact version?
a
good question, I can't remember! It doesn't look like that version is used. Possibly I defined that before learning about the
embeddedKotlinVersion
util val
in any case, Dokkatoo is a Gradle plugin, so it's best to try and match the embedded Kotlin version so that it's more likely it will be compatible. If you're just building a regular Kotlin library/app, then the embedded Kotlin version isn't so important - so long your project builds, it doesn't matter what version Gradle uses.
gratitude thank you 1
e
Maybe a question for @mbonnin. I was wondering why in Apollo's build plugins you're doing the following
Copy code
plugins.apply(SamWithReceiverGradleSubplugin::class.java)
extensions.configure(SamWithReceiverExtension::class.java) {
  annotations(HasImplicitReceiver::class.qualifiedName!!)
}
I understand that if I don't do that, Kotlin code breaks. But then the question is, why that plugin isn't applied by default?
m
Oh well blob upside down
It's a long story that'd required a long evening at the fireside and some beverages. Read https://github.com/gradle/gradle/pull/24286 if you have time 🙂
tldr; we did this so that we could more easily copy/paste from other build scripts but you don't have to, you can also use
it
instead of
this
and it'd work the same
e
Thanks! I'll give the thread a read now. I was looking at your binary plugins earlier today, trying to understand how they work, but I didn't want to apply stuff without understanding the impact.
a
the SamWithReceiver plugin & config is automatically applied when you use the
kotlin-dsl
plugin
☝️ 1
e
@Adam S so that would be another pro of using script plugins
a
you can apply the
kotlin-dsl
plugin and also still write binary plugins
m
Yep, that. But we don't want the
kotlinGradleDsl()
dependency because we don't need
String.invoke()
in our classpath (also there were other side effects of
kotlin-sdl
but can't remember the specifics)
e
Ok! I'll take some time to get a better understanding of the whole picture here. Every ten minutes I get some new information to process lol
m
Mmm most likely not
Let me see if I can regenerate them
✔️ 1
e
Thank you!
Ok that was useful! A couple of notes. At some point you mentioned, for
kotlin-embedded
and `kotlin-dsl`:
Like the ``embedded-kotlin`` plugin, it uses the same Kotlin version as your Gradle build. This is useful if you do not intend to distribute your plugins (i.e. convention plugins). If you need to distribute your plugin, make sure to use a Kotlin version that is compatible with the Gradle version you are targeting.
Although I get the why, it's still unclear to me how I can drive the Kotlin version. And btw, I suppose we're talking about the Kotlin runtime version used by the plugin, right?
m
how I can drive the Kotlin version
Standalone Gradle plugins are regular JVM projects. Just compile them with your Kotlin version of choice and set api/language version to be compatible with Gradle:
Copy code
plugins {
  id("org.jetbrains.kotlin.jvm").version("1.9.0") 
  id("java-gradle-plugin")
}

tasks.withType(KotlinCompile::class.java).configureEach {
  kotlinOptions {
    // Put whatever version the min Gradle version you want to support requires
    // See <https://docs.gradle.org/current/userguide/compatibility.html>
    apiVersion = "1.8"
    languageVersion = "1.8"
  }
}
(sorry for the edits, pressed enter a bit too fast)
e
Ohh now it makes more sense. So basically `kotlin-embedded`/`dsl` gets you a fast setup, but for someone that has to distribute the plugin it is better to go with a more fine grained setup
🎯 1
m
Exactly
gratitude thank you 1
e
I suppose for the fine grained approach, using
Copy code
compileOnly(gradleKotlinDsl())
is a bad idea then, because you're using stuff from your development Gradle version that may not be present on an older/newer version.
m
I do not use
gradleKotlinDsl()
at all. But if you want to I'm not sure actually
There's a very high chance Gradle forces its own version of
gradleKotlinDsl()
at runtime indeed but can't remember for sure
I'm not even sure you can get the
Gradle version
<=>
gradleKotlinDsl() version
mapping somewhere like there is for Kotlin at https://docs.gradle.org/current/userguide/compatibility.html. You might have to looks at the Gradle source
e
Given the Kotlin DSL dependency uses an entire different versioning, I suppose it takes whatever is built-in into the Gradle instance you're using
m
There's a possibility that Gradle doesn't need it to bootstrap itself so it's loaded with other regular buildscript dependencies and therefore can do proper dependency resolution Nevermind, if it's in the distribution, I can't see how it would be resolved externally
But I wouldn't bet on it
✔️ 1
Also you're right
gradleKotlinDsl()
is builtin in the Gradle distribution, I got confused with the kotlin-dsl plugin, which is not builtin. So yea, definitely would not bet on it 😅 .
I wonder if nokee redistributes that as well. I have some vague memories but this whole discussion is another reason why I don't want
gradleKotlinDsl()
in my classpath
101 Views