384 lines
21 KiB
Markdown
384 lines
21 KiB
Markdown
---
|
||
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<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](../../Программирование%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)
|