I'm having difficulties setting up my kotlin multiproject project. I've already spent hours of reading the documentation, but I get the feeling that I am getting nowhere. Maybe someone here could help me out and tell me how I need to adjust my build script.
My project setup is (or should be):
root
|-> src
|-> commonMain
|-> kotlin
|-> commonTest
|-> kotlin
|-> jvmMain
|-> kotlin
|-> jvmTest
|-> kotlin
|-> nativeMain
|-> kotlin
|-> cpp
|-> nativeTest
|-> kotlin
|-> cpp
The directories named "cpp" under nativeMain and nativeTest will contain additional platform specific code written in c++, which will depend on the C library generated by Kotlin/Native.
Currently, I'm trying to achieve the following: Generate a jar file that contains all the classes from {commonMain, commonTest, jvmMain, jvmTest}. Specifically, I want to include JUnit's ConsoleLauncher in my test-jar, so I have added a dependency on implementation("org.junit.platform:junit-platform-console-standalone:1.10.1") to my jvmTest sourceSet.
Running the gradle task jvmTest successfully launches all my test, but it does not generate a jar file (apparently, at least I can't find it). Is there a way to generate the jar with a gradle task?
Here is my build.gradle.kts script:
plugins {
java
id("java-library")
kotlin("multiplatform") version "1.9.20"
}
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
}
kotlin {
jvm("jvm") {
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
linuxX64("linux")
mingwX64("windows")
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
binaries {
sharedLib {
baseName = if(name == "windows") "libnative" else "native"
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib"))
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.6")
implementation("org.junit.platform:junit-platform-console-standalone:1.10.1")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val windowsMain by getting {
dependsOn(sourceSets["commonMain"])
}
val windowsTest by getting {
dependsOn(sourceSets["commonTest"])
}
val linuxMain by getting {
dependsOn(sourceSets["commonMain"])
}
val linuxTest by getting {
dependsOn(sourceSets["commonTest"])
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting {
dependsOn(sourceSets["commonMain"])
}
val jvmTest by getting {
dependsOn(sourceSets["commonTest"])
dependencies {
implementation("org.junit.platform:junit-platform-console-standalone:1.10.1")
implementation(kotlin("test"))
implementation(kotlin("test-junit5"))
// needed by IDEA?
implementation("org.junit.jupiter:junit-jupiter-engine:5.10.1")
implementation("org.junit.jupiter:junit-jupiter-params:5.10.1")
implementation("org.junit.jupiter:junit-jupiter-api:5.10.1")
}
}
}
}
tasks.withType<Wrapper> {
gradleVersion = "8.4"
distributionType = Wrapper.DistributionType.ALL
}
tasks.named<Test>("jvmTest") {
useJUnitPlatform()
filter {
isFailOnNoMatchingTests = false
}
testLogging {
showExceptions = true
showStandardStreams = true
events = setOf(
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
//org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED
)
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
afterSuite(
KotlinClosure2({
desc: TestDescriptor, result: TestResult ->
if (desc.parent == null) {
// Only execute on the outermost suite
val color = if(result.resultType == TestResult.ResultType.SUCCESS) Colors.green else Colors.red
val reset = Colors.reset
println("")
println(" **** Result: $color ${result.resultType} $reset ****")
println(" > Tests: ${result.testCount}")
println(" > Passed: ${result.successfulTestCount}")
println(" > Failed: ${result.failedTestCount}")
println(" > Skipped: ${result.skippedTestCount}")
}
})
)
}
}
Additionally, when I create a separate gradle task, it doesn't seem to find the same sourceSets as the kotlin plugin. With this gradle task, I only see the sourceSets "main" and "test", which (I suppose) don't contain any code:
tasks.register("printSourceSetsInfo") {
doLast {
sourceSets.all { sourceSet ->
println("Source set: ${sourceSet.name}")
println(" - Output directory: ${sourceSet.output}")
println(" - Source directories: ${sourceSet.allSource.srcDirs}")
println(" - Resources directories: ${sourceSet.resources.srcDirs}")
println(" - Compile classpath: ${sourceSet.compileClasspath}")
println(" - Runtime classpath: ${sourceSet.runtimeClasspath}")
println()
true
}
}
}
tasks.register("packageTests", Jar::class) {
val jvmTestSourceSet = sourceSets.findByName("jvmTest") ?: sourceSets.findByName("testJvm") ?: sourceSets.findByName("test")
if (jvmTestSourceSet != null) {
from(jvmTestSourceSet.output)
archiveFileName = "acteo-kotlin-tests.jar"
destinationDirectory = file("build/libs/")
} else {
println("JVM test source set not found. Check your project configuration.")
println("Available source sets: ${sourceSets.names.joinToString(", ")}")
//throw GradleException("JVM test source set not found")
}
}
I find this strange/confusing and I wonder, how I would set up specific tasks to generate my cpp code at some stage (as I would need to access the source sets in a dedicated cpp block instead of the kotlin block). Maybe someone can give me a hint as well? But maybe this is something for a separate question once I get to the point of writing additional cpp code...
Gradle is a powerful build tool and everything you need is there, but it can take a bit of digging at times. Realistically, you have to look through the code of the Kotlin Gradle plugin to see how it's put together and to write extra tasks just how you want them.
In a Kotlin multiplatform project, all configuration sits on the Kotlin multiplatform extension which can be accessed using kotlin
in the build.gradle.kts file. This extension is the central place where all the Kotlin multiplatform configuration sits and everything you want should be somewhere in that object.
Additionally, when I create a separate gradle task, it doesn't seem to find the same sourceSets as the kotlin plugin.
The short answer is: when you use the top-level sourceSets
accessor in your build.gradle.kts file, you are accessing the Java source sets, not the Kotlin ones.
As you probably know, Gradle in its original intent was to be a build tool for Java programs, and there are a range of plugins provided by Gradle designed for Java programs, such as the Java plugin, that follow well-known conventions.
When a Kotlin multiplatform plugin has a JVM target, it applies the Java base plugin to make use of some of those conventions. But Kotlin multiplatform also sets up its own parallel system of source sets and compilations and in general those should be used for writing additional tasks.
You can access Kotlin source sets inside the Kotlin extension like so (as you did when configuring your project):
kotlin {
sourceSets {
// Configure source sets
}
}
Is there a way to generate the jar with a gradle task?
Absolutely there is. This kind of task is really the raison d'ĂȘtre of Gradle.
You're right that a test JAR is not created by default. But you can create any JAR you want by writing a task of type Jar
(docs), as you had begun to.
In Kotlin you can write:
tasks.register<Jar>("createTestJar") {
archiveClassifier.set("test")
from(kotlin.jvm().compilations.get("test").output)
}
Specifically, I want to include JUnit's ConsoleLauncher in my test-jar
You'll also soon discover that dependencies are not packaged into a JAR by default. In fact it is discouraged because in general it is the job of the consumer of a JAR to get hold of whatever dependencies are needed. But there are certainly some good reasons to package dependencies into what's called a shadow or fat JAR.
This answer is too long already so I won't suggest any more code to you but you can start reading more in the Gradle documentation. Just be sure to pick up the Kotlin compilation outputs, not the Java plugin ones.
You may also want to look at configurations which are Gradle's way of grouping dependencies (in order to control which groups of dependencies go into your JAR and which don't).