--- title: Gradle и Kotlin DSL description: build.gradle.kts, конфигурации зависимостей, version catalog, таски и настройка компилятора tags: - kotlin - tooling - gradle --- Gradle - стандарт сборки Kotlin-проектов. Kotlin DSL (`build.gradle.kts`) - это обычный Kotlin-код, поэтому IDE даёт автодополнение, переход к определению и проверку типов, чего не было в Groovy DSL. ```kotlin // 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/` | общая логика сборки | ```kotlin // 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. ## Конфигурации зависимостей ```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`: ```kotlin // 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" } ``` ```kotlin // 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 для автообновлений. ## Настройка компилятора ```kotlin // 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](kotlinc.md) ## Таски ```kotlin // 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 ``` ```kotlin // корневой 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")) } } ``` ```kotlin // app/build.gradle.kts dependencies { implementation(project(":core")) implementation(project(":api")) } ``` `apply false` в корне означает «добавить плагин в classpath, но не применять к корневому проекту» - корневой модуль обычно не содержит кода. Строковая форма `"testImplementation"(...)` нужна внутри `subprojects`: типизированный аксессор там недоступен, потому что плагин ещё не применён на момент конфигурации. Для серьёзных проектов вместо `subprojects` используют convention plugins в `buildSrc/` или `build-logic/` - они работают с configuration cache и дают нормальную типизацию. Подробнее: [Структура проекта](Структура%20проекта.md) ## 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, а не при запуске. ## Публикация ```kotlin // 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("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 | > | `compile` | `implementation` / `api` | > | `maven-compiler-plugin` | `jvmToolchain` / `compilerOptions` | > | `` для версий | `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 - ответ на ``.** В Maven версии централизуются через BOM и ``, в Gradle долго не было стандарта (каждый писал свои `ext`-переменные). `libs.versions.toml` типизирован, виден в IDE и понятен инструментам автообновления. > > Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md) > [!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`:** > ```kotlin > // 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](KSP.md) > > **`./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`; наличие обоих файлов в модуле даёт ошибку. Далее: [Структура проекта](Структура%20проекта.md)