Files
obsidian/Kotlin/Инструментарий/Gradle и Kotlin DSL.md
T

21 KiB
Raw Blame History

title, description, tags
title description tags
Gradle и Kotlin DSL build.gradle.kts, конфигурации зависимостей, version catalog, таски и настройка компилятора
kotlin
tooling
gradle

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 Groovy Gradle Kotlin DSL
<dependency><scope>compile</scope> implementation / api
maven-compiler-plugin jvmToolchain / compilerOptions
<properties> для версий libs.versions.toml
annotation processor path конфигурация ksp
XML, декларативно код, императивно

Kotlin DSL - код, а не данные. В Maven pom.xml - декларативный документ: инструменты его читают и модифицируют надёжно. build.gradle.kts - программа: в ней можно написать цикл, условие, вызов функции. Это даёт гибкость (генерация тасков, вычисляемые версии) и создаёт проблему - статически понять, что делает сборка, нельзя. Отсюда configuration cache: Gradle вынужден кешировать результат исполнения скрипта.

implementation против compile в Maven - это про скорость сборки. В Maven compile-scope экспортирует зависимость транзитивно всегда: потребитель видит всё дерево. Gradle разделил на api и implementation, и это принципиально: изменение implementation-зависимости не вызывает перекомпиляцию потребителей. На больших проектах разница в минутах.

Toolchain решает проблему, которой в Maven нет в таком виде. Maven полагается на JDK, под которым запущен, плюс maven.compiler.release. В Kotlin нужно согласовать три вещи: JDK компиляции, jvmTarget Kotlin и targetCompatibility Java - и расхождение даёт ошибку «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; наличие обоих файлов в модуле даёт ошибку.

Далее: Структура проекта