21 KiB
title, description, tags
| title | description | tags | |||
|---|---|---|---|---|---|
| Gradle и Kotlin DSL | build.gradle.kts, конфигурации зависимостей, version catalog, таски и настройка компилятора |
|
Gradle - стандарт сборки Kotlin-проектов. Kotlin DSL (build.gradle.kts) - это обычный Kotlin-код, поэтому IDE даёт автодополнение, переход к определению и проверку типов, чего не было в Groovy DSL.
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
application
}
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
testImplementation(kotlin("test"))
}
kotlin { jvmToolchain(21) }
application { mainClass.set("com.example.MainKt") }
tasks.test { useJUnitPlatform() }
Это полный рабочий файл. kotlin("jvm") - сокращение для id("org.jetbrains.kotlin.jvm"), kotlin("test") - для org.jetbrains.kotlin:kotlin-test версии, совпадающей с плагином.
Файлы сборки
| Файл | Назначение |
|---|---|
settings.gradle.kts |
имя проекта, список модулей, репозитории плагинов |
build.gradle.kts |
конфигурация модуля |
gradle.properties |
флаги Gradle, память JVM |
gradle/libs.versions.toml |
version catalog |
gradlew / gradlew.bat |
wrapper - фиксирует версию Gradle |
gradle/wrapper/gradle-wrapper.properties |
версия Gradle |
buildSrc/ или build-logic/ |
общая логика сборки |
// settings.gradle.kts
rootProject.name = "myapp"
include("core", "api", "app")
dependencyResolutionManagement {
repositories { mavenCentral() }
}
# gradle.properties
org.gradle.jvmargs=-Xmx4g
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
kotlin.daemon.jvmargs=-Xmx2g
kotlin.incremental=true
Четыре флага дают основной выигрыш в скорости: параллельная сборка модулей, кеш результатов, кеш конфигурации и инкрементальная компиляция Kotlin.
Конфигурации зависимостей
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-client-core:3.0.0") // нужна при компиляции и в рантайме
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") // + видна потребителям модуля
compileOnly("org.jetbrains:annotations:26.0.0") // только при компиляции
runtimeOnly("ch.qos.logback:logback-classic:1.5.18") // только в рантайме
testImplementation(kotlin("test"))
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
}
| Конфигурация | Компиляция модуля | Рантайм | Видна потребителям |
|---|---|---|---|
implementation |
да | да | нет |
api |
да | да | да |
compileOnly |
да | нет | нет |
runtimeOnly |
нет | да | да |
testImplementation |
только тесты | только тесты | нет |
ksp |
обработка аннотаций | нет | нет |
Разница implementation и api - главное решение в многомодульном проекте. implementation скрывает транзитивную зависимость: при её изменении перекомпилируется только ваш модуль. api экспортирует тип в публичное API модуля - без неё потребитель не увидит типы из зависимости в ваших сигнатурах.
Правило: api только тогда, когда тип из зависимости появляется в публичных сигнатурах. Иначе implementation - он быстрее пересобирается и не навязывает зависимость.
Для библиотеки нужен плагин java-library:
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
`java-library`
}
dependencies {
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}
Обратные кавычки вокруг java-library нужны из-за дефиса: это идентификатор Kotlin, а не строка.
Version catalog
Версии в одном месте, типизированный доступ из DSL:
# gradle/libs.versions.toml
[versions]
kotlin = "2.4.0"
coroutines = "1.11.0"
ktor = "3.0.0"
[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
[bundles]
async = ["coroutines-core", "ktor-client"]
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "2.4.0-2.0.0" }
// build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
}
repositories { mavenCentral() }
dependencies {
implementation(libs.bundles.async)
implementation(libs.coroutines.core)
testImplementation(libs.kotlin.test)
testImplementation(libs.coroutines.test)
}
Дефис в TOML превращается в точку в DSL: coroutines-core → libs.coroutines.core. Каталог подхватывается автоматически по пути gradle/libs.versions.toml, настройка не нужна.
Преимущество перед val kotlinVersion = "2.4.0" в начале файла: каталог работает во всех модулях и виден Dependabot/Renovate для автообновлений.
Настройка компилятора
// build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
languageVersion.set(KotlinVersion.KOTLIN_2_4)
apiVersion.set(KotlinVersion.KOTLIN_2_2)
allWarningsAsErrors.set(true)
progressiveMode.set(true)
freeCompilerArgs.addAll(
"-Xjsr305=strict",
"-opt-in=kotlin.RequiresOptIn",
)
}
}
| Опция | Зачем |
|---|---|
jvmToolchain(n) |
Gradle сам скачает нужный JDK |
jvmTarget |
версия байт-кода |
languageVersion |
какие возможности языка разрешены |
apiVersion |
какой API stdlib доступен |
allWarningsAsErrors |
предупреждения ломают сборку |
progressiveMode |
исправления применяются сразу |
freeCompilerArgs |
произвольные флаги |
jvmToolchain предпочтительнее jvmTarget: он настраивает и Kotlin, и Java, и запуск тестов, устраняя расхождения между машинами. Подробнее про флаги: kotlinc
Таски
// build.gradle.kts
tasks.register("generateVersion") {
val outputDir = layout.buildDirectory.dir("generated/version")
val version = project.version.toString()
outputs.dir(outputDir)
doLast {
val file = outputDir.get().file("Version.kt").asFile
file.parentFile.mkdirs()
file.writeText("const val APP_VERSION = \"$version\"\n")
}
}
tasks.named("compileKotlin") {
dependsOn("generateVersion")
}
tasks.test {
useJUnitPlatform()
maxParallelForks = Runtime.getRuntime().availableProcessors() / 2
testLogging {
events("passed", "failed", "skipped")
}
}
Важная деталь: значения проекта (project.version) читаются вне doLast, в фазе конфигурации. Обращение к project внутри doLast несовместимо с configuration cache и даёт предупреждение или ошибку.
Частые таски:
| Таск | Что делает |
|---|---|
build |
компиляция + тесты + сборка артефактов |
assemble |
только артефакты, без тестов |
test |
тесты |
clean |
удалить build/ |
run |
запуск (плагин application) |
dependencies |
дерево зависимостей |
tasks --all |
список всех тасков |
kotlinDslAccessorsReport |
доступные аксессоры DSL |
./gradlew build --no-daemon
./gradlew test --tests "com.example.UserServiceTest"
./gradlew dependencies --configuration runtimeClasspath
./gradlew build --scan # отчёт о производительности
./gradlew --profile build # локальный профиль сборки
Многомодульный проект
myapp/
├── settings.gradle.kts
├── build.gradle.kts # общая конфигурация
├── gradle/libs.versions.toml
├── core/build.gradle.kts
├── api/build.gradle.kts
└── app/build.gradle.kts
// корневой build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0" apply false
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
repositories { mavenCentral() }
dependencies {
"testImplementation"(kotlin("test"))
}
}
// app/build.gradle.kts
dependencies {
implementation(project(":core"))
implementation(project(":api"))
}
apply false в корне означает «добавить плагин в classpath, но не применять к корневому проекту» - корневой модуль обычно не содержит кода.
Строковая форма "testImplementation"(...) нужна внутри subprojects: типизированный аксессор там недоступен, потому что плагин ещё не применён на момент конфигурации.
Для серьёзных проектов вместо subprojects используют convention plugins в buildSrc/ или build-logic/ - они работают с configuration cache и дают нормальную типизацию. Подробнее: Структура проекта
Kotlin DSL против Groovy DSL
Groovy (build.gradle) |
Kotlin (build.gradle.kts) |
|---|---|
implementation 'group:art:1.0' |
implementation("group:art:1.0") |
ext.version = '1.0' |
val version = "1.0" |
task hello { ... } |
tasks.register("hello") { ... } |
apply plugin: 'java' |
plugins { java } |
sourceCompatibility = 1.8 |
java.sourceCompatibility = JavaVersion.VERSION_1_8 |
| динамический, без проверок | типизированный, с автодополнением |
| быстрее первая конфигурация | требует компиляции скрипта |
Kotlin DSL - рекомендуемый вариант с Gradle 8. Основная цена - первая сборка медленнее (скрипт компилируется), зато ошибки в конфигурации видны в IDE, а не при запуске.
Публикация
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
`java-library`
`maven-publish`
}
group = "com.example"
version = "1.0.0"
java {
withSourcesJar()
withJavadocJar()
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
pom {
name.set("My Library")
description.set("Описание библиотеки")
licenses {
license {
name.set("Apache-2.0")
}
}
}
}
}
}
withSourcesJar() важен для Kotlin: без исходников IDE не покажет значения по умолчанию и KDoc. ./gradlew publishToMavenLocal публикует в ~/.m2 для локальной проверки.
[!info] Отличия от Java
Java-мир Kotlin-мир Maven pom.xmlили Gradle GroovyGradle Kotlin DSL <dependency><scope>compile</scope>implementation/apimaven-compiler-pluginjvmToolchain/compilerOptions<properties>для версийlibs.versions.tomlannotation processor path конфигурация kspXML, декларативно код, императивно Kotlin DSL - код, а не данные. В Maven
pom.xml- декларативный документ: инструменты его читают и модифицируют надёжно.build.gradle.kts- программа: в ней можно написать цикл, условие, вызов функции. Это даёт гибкость (генерация тасков, вычисляемые версии) и создаёт проблему - статически понять, что делает сборка, нельзя. Отсюда configuration cache: Gradle вынужден кешировать результат исполнения скрипта.
implementationпротивcompileв Maven - это про скорость сборки. В Mavencompile-scope экспортирует зависимость транзитивно всегда: потребитель видит всё дерево. Gradle разделил наapiиimplementation, и это принципиально: изменениеimplementation-зависимости не вызывает перекомпиляцию потребителей. На больших проектах разница в минутах.Toolchain решает проблему, которой в Maven нет в таком виде. Maven полагается на JDK, под которым запущен, плюс
maven.compiler.release. В Kotlin нужно согласовать три вещи: JDK компиляции,jvmTargetKotlin иtargetCompatibilityJava - и расхождение даёт ошибку «Inconsistent JVM-target compatibility».jvmToolchain(n)настраивает всё сразу и скачивает JDK при необходимости.Version catalog - ответ на
<dependencyManagement>. В Maven версии централизуются через BOM и<properties>, в Gradle долго не было стандарта (каждый писал своиext-переменные).libs.versions.tomlтипизирован, виден в IDE и понятен инструментам автообновления.Сравнить: JVM
[!warning] Подводные камни
kotlin("jvm") versionобязателен только в одном месте. Указание версии в нескольких модулях даёт ошибку «Plugin request for plugin already on the classpath must not include a version». В подмодулях -kotlin("jvm")без версии, в корне - с версией иapply false.Не указывайте stdlib вручную.
implementation(kotlin("stdlib"))избыточен: плагин добавляет его сам, причём правильной версии. Явное указание другой версии даёт конфликт.
jvmTargetбез toolchain ломается при смене JDK. Локально JDK 21, в CI 17 - сборка упадёт.jvmToolchain(21)делает результат воспроизводимым.Configuration cache не терпит обращения к
projectвdoLast:// build.gradle.kts // неправильно: project читается во время выполнения tasks.register("bad") { doLast { println(project.version) } } // правильно: значение захвачено в фазе конфигурации tasks.register("good") { val v = project.version.toString() doLast { println(v) } }Первый вариант даёт ошибку «invocation of 'Task.project' at execution time is unsupported».
apiвместоimplementationпо привычке замедляет сборку. Транзитивно экспортированная зависимость заставляет перекомпилировать всех потребителей при её изменении. На большом проекте это разница между инкрементальной сборкой за 5 секунд и полной за 2 минуты.
gradlewнужно коммитить, включаяgradle-wrapper.jar. Без него сборка на другой машине использует локальный Gradle другой версии. Некоторые политики безопасности запрещают JAR в репозитории - тогда нуженgradle/wrapper/gradle-wrapper.propertiesплюс проверка контрольной суммы черезvalidation-actionв CI.Дефис в имени из каталога превращается в точку.
coroutines-testв TOML - этоlibs.coroutines.testв DSL. Попытка написатьlibs.coroutines-testне скомпилируется, аlibs.coroutinesTestне найдётся.Плагины компилятора должны совпадать по версии с Kotlin.
kotlin("plugin.serialization") version "2.3.0"при основном плагине 2.4.0 даёт ошибку несовместимости. Держите обе версии в[versions] kotlin.Версия KSP привязана к версии Kotlin. Формат
2.4.0-2.0.0: первая часть - версия Kotlin, вторая - KSP. Обновление Kotlin требует одновременного обновления KSP. Подробнее: KSP
./gradlew clean buildпочти всегда лишнее.cleanотключает инкрементальную компиляцию и кеш, превращая 10-секундную сборку в минутную. Используйте только при реальных проблемах с кешем.Нехватка памяти демона проявляется как медленная сборка, а не как ошибка. При
-Xmxменьше нужного JVM тратит время на GC. Для среднего проектаorg.gradle.jvmargs=-Xmx4gиkotlin.daemon.jvmargs=-Xmx2g- разумный минимум.Устаревшая
apiVersionвместе сallWarningsAsErrorsроняет сборку. В Kotlin 2.4 значениеKOTLIN_2_1даёт «API version 2.1 is deprecated and its support will be removed in a future version», а-Werrorпревращает предупреждение в ошибку. Поддерживается примерно три предыдущих версии - при обновлении Kotlin проверяйте, что указаннаяapiVersionещё не устарела.
tasks.createустарел в пользуtasks.register.createконфигурирует таск немедленно, при каждой сборке;register- лениво, только если таск нужен. На проектах с сотнями тасков разница заметна.Groovy- и Kotlin-скрипты нельзя смешивать в одном файле. Переход на Kotlin DSL - это переписывание всего
build.gradleвbuild.gradle.kts; наличие обоих файлов в модуле даёт ошибку.
Далее: Структура проекта