I’ve recently been trying to get a project going with the following stack:

In principle this didn’t seem an unreasonable stack. JavaFX is meant to be the latest and greatest JVM UI Framework. It requires JPMS usage, but then JPMS is meant to be the new “correct” way to develop for the JVM. Gradle is the oldest and most mature JVM build system that isn’t awful (no I don’t want to program in XML). It has good IntelliJ support. Kotlin is developed by IntelliJ and uses gradle as its build system; I’ve been programming with it for a year or two now and found it mature, well designed and a pleasure to use.

In practice I found it surprisingly painful to get things to a) work and b) work the way I wanted them to. So if you’re doing something similar you can now learn from my pain.

Tips:

Add plugins as runtime dependencies in buildSrc/

buildSrc/build.gradle.kts

plugins {
  `kotlin-dsl`
}

repositories {
  gradlePluginPortal()
}

dependencies {
  testRuntimeOnly("org.javamodularity:moduleplugin:1.5.0")
  testRuntimeOnly("org.openjfx:javafx-plugin:0.0.8")
}

This was the most useful thing for me in debugging plugin behaviour. Having a buildSrc/build.gradle.kts with the kotlin-dsl plugin makes IntelliJ add the gradle jars & sources to External Libraries. Adding any plugins you use as runtime dependencies in buildSrc/ means they also get added to External Libraries. This allows you to browse their source, hyperlink around it and set breakpoints. I find it invaluable in debugging plugin behaviour.

(This tip may become irrelevant if a future version of IntelliJ automatically adds these sources to External Libraries - vote on IDEA-197182 to help make that happen.)

Plugin application order matters

I am using the org.javamodularity:moduleplugin to manage the JPMS behaviour because gradle has no out of the box support. It needs to read the java main sourceSet to find & read module-info.java. If you change the sourceSet directories it will pick up on that change - but you need to apply moduleplugin after you change the srcSets, not before. Otherwise it picks up the standard src dirs and cannot find the file.

Irritatingly when it doesn’t find module-info.java it just silently does not apply itself, leaving you to try and understand the downstream errors, rather than failing good and hard and explaining the problem to you.

Use moduleplugin version 1.5.0 not 1.6.0

I encountered two issues with version 1.6.0:

  • It has a breaking change (on a minor version increment…) and javafx-plugin:0.0.8 depends on a class that is no longer present in 1.6.0
  • It gets the module path wrong when trying to run a standard mixed (java & kotlin) module

Things you just need to know about the gradle kotlin dsl

Configuring sub project plugins in a root project that should not apply the plugins is hard and confusing. Trial and error have shown me the following:

Import configurations:

val api by configurations
val implementation by configurations
val testImplementation by configurations

dependencies {
  api(kotlin("stdlib"))
  implementation("...")
  testImplementation("...")
}

Import tasks:

val test by tasks.existing(Test::class)
tasks {
  test {
    useJUnitPlatform()
  }
}

Configure java:

configure<JavaPluginExtension> {
  sourceCompatibility = javaVersion
  targetCompatibility = javaVersion

  configure<SourceSetContainer> {
    named("main") { java.setSrcDirs(setOf("src")) }
    named("test") { java.setSrcDirs(setOf("tests")) }
  }
}

Configure kotlin:

configure<KotlinJvmProjectExtension> {
  sourceSets {
    named("main") { kotlin.setSrcDirs(setOf("src")) }
    named("test") { kotlin.setSrcDirs(setOf("tests")) }
  }
}

Consider applying config by applied plugin

If you’ve got a multi-project build it may be convenient to configure sub projects by whether or not they have some plugin applied to them. In my root project I do this:

subprojects {
  pluginManager.withPlugin("kotlin") {
    // All config common to kotlin projects
  }
}

Then in any sub project I just need this in build.gradle.kts to apply all my common kotlin config:

plugins {
  kotlin("jvm")
}

This has the additional benefit that, because you explicitly applied the plugin, you have access to the plugin’s dsl in that sub project’s build.gradle.kts.

Share functions between multiple build.gradle.kts by putting them in buildSrc/

Any code in buildSrc/<main src dir> is available to all build.gradle.kts in the project and sub projects.

For instance if I create a file in buildSrc/<main src dir> called DependencyVersions.kt like so:

fun kotlintest(module: String) = "io.kotlintest:kotlintest-$module:3.4.2"
fun arrowkt(module: String) = "io.arrow-kt:arrow-$arrowModule:0.10.2"
fun kotlinCoroutines(module: String) = "org.jetbrains.kotlinx:kotlinx-coroutines-$module:1.3.2"

then in any build.gradle.kts I can use those functions to depend on modules as so:

dependencies {
  implementation(kotlintest("core"))
  implementation(arrowkt("core"))
  implementation(kotlinCoroutines("core"))
  implementation(kotlinCoroutines("javafx"))
}

Prevent intermediate directories becoming projects

By default, if you include deeply nested projects like this:

settings.gradle.kts

include(
  ":app",
  ":core",
  ":ui:api",
  ":ui:javafx"
)

the intermediate directories (in this case ui) will become gradle projects, despite lacking a build.gradle.kts and any other files. This can cause very confusing errors like this one:

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring project ':app'.
> A problem occurred configuring project ':ui:api'.
   > Could not open cache directory add8lpbh91wftlbit7lhn37cw (/home/runner/.gradle/caches/6.0.1/gradle-kotlin-dsl/add8lpbh91wftlbit7lhn37cw).
      > org.gradle.api.internal.initialization.DefaultClassLoaderScope@47f3a892 must be locked before it can be used to compute a classpath!

You can fix this by including the specific project and setting its dir explicitly, as so:

settings.gradle.kts

include(
  ":app",
  ":core",
  ":ui-api",
  ":ui-javafx"
)
project(":ui-api").projectDir = file("ui/api")
project(":ui-javafx").projectDir = file("ui/javafx")

Example Project

These ideas can be seen implemented at https://github.com/Mahoney-example/example-gradle-kotlin-javafx