Skip to content

Why the Build Scripts Are Kotlin, Too

This page is about Gradle's Kotlin DSL (build.gradle.kts, settings.gradle.kts) — the language 2P-Kt's build is written in. It is unrelated to the Prolog-term-building DSL discussed in the Prolog DSL reference (:dsl-core/:dsl-solve, for writing terms and queries as Kotlin code); the two happen to both be "a DSL embedded in Kotlin", but one configures Gradle and the other constructs logic terms, and they share no code.

settings.gradle.kts even says so explicitly, at the very top of the repository:

> __NOTICE:__ Prefer _Kotlin_ as the scripting language for Gradle

The alternative was Groovy, and it costs you at scale

Gradle has always supported two build-script languages: Groovy (the original, dynamically typed) and Kotlin (added later, statically typed). For a one-module toy project the difference barely shows. For a build with over thirty interdependent modules (see Why a ~30-module Gradle build) and a fair amount of genuinely custom build logic, it shows constantly. A few concrete examples straight out of this repository's own build scripts:

  • Typed task references, checked at compile time. core/build.gradle.kts configures Detekt, Dokka, and ktlint tasks by type, not by name-as-a-string:
import io.gitlab.arturbosch.detekt.Detekt
import org.danilopianini.gradle.gitsemver.SemanticVersion
import org.jetbrains.dokka.gradle.tasks.DokkaBaseTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
import org.jlleitschuh.gradle.ktlint.tasks.BaseKtLintCheckTask
                    }
                    outputs.file(infoKtFile)
                    outputs.upToDateWhen { infoKtFile.exists() && infoKtFile.readText().contains(version.toString()) }
                }

In Groovy's dynamic DSL, tasks.withType(Detekt) still works, but there is no compiler checking that Detekt is spelled right, imported from the right plugin, or that .configureEach { ... }'s closure body refers to properties Detekt actually has — mistakes surface at build time, potentially only in CI, rather than as a red squiggle in the IDE while writing the script. - Type-safe accessors for the version catalog. libs.versions.jvm.get() and libs.plugins.ktMpp.mavenPublish.get().pluginId (both used verbatim in this repo's build scripts) are generated, statically-typed accessors into gradle/libs.versions.toml — autocomplete works, a typo'd dependency alias is a compile error in the build script itself, not a runtime "could not find dependency". - Real Kotlin where the build genuinely needs logic. core/build.gradle.kts writes a generated Info.kt file (embedding the project's version into a Kotlin object) as part of the build, with a require(...) check that the module's version matches the root project's:

            }

            val infoKtFile =
                kotlin.srcDirs
                    .first()
                    .absoluteFile
                    .resolve("$tuPrologPackageDir/Info.kt")

            val createInfoKt =
                tasks.register("createInfoKt") {

and solve-classic/build.gradle.kts reads Gradle properties with typed defaults to size the JVM test process:

}

Both are ordinary Kotlin — string templates, require, the standard library — reusing exactly the language and standard library every contributor to the production code already knows, rather than asking them to context-switch into Groovy's separate (and looser) semantics just to touch the build.

Why this matters specifically for a project this shape

2P-Kt is a Kotlin-multiplatform codebase maintained by people who already read and write Kotlin all day; making the build scripts Kotlin too means IDE navigation ("go to definition" on a plugin ID, a task type, a dependency alias), refactoring support, and static type-checking all keep working when you jump from application code into build configuration — which happens often in a build this modular, since so much of the shared configuration (the multiProjectHelper templates described in Why a ~30-module Gradle build) is itself non-trivial Kotlin code, not simple declarative property lists. Choosing Groovy for the scripts and Kotlin for everything else would have meant maintaining build logic in a second language purely for historical reasons, for no benefit specific to this project.