Gradle Convention Plugins: как облегчить себе жизнь и уменьшить boilerplate в gradle-файлах

от автора

Привет, Хабр! Я Дима Котиков, ведущий android-разработчик в Т-Банке. Работаю в команде приложения Долями. Разработкой под Android начал увлекаться в 2020 году, а потом хобби переросло в работу. Люблю разбираться в технологиях, разрабатывать под Android и KMP и латте на фундучном молоке 🙂

Я расскажу о том, как облегчить работу с Gradle с использованием Gradle Convention Plugins. Всю информацию я разбил на серию статей для удобства. Они будут полезны всем, кто пользуется Gradle в качестве сборщика проектов. В первой части поговорим о проблеме с build.gradle-файлами и сделаем начальную настройку для написания Gradle Convention Plugins.

build.gradle.kts — монстр

Каждый android-разработчик так или иначе сталкивается со сборщиком проектов Gradle и видит в своих модулях файлы build.gradle или build.gradle.kts. Дальше примеры будут на базе build.gradle.kts-файлов для kotlin-multiplatform-проекта, но в целом информация применима и к build.gradle.

Скрытый текст
import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.dsl.TargetFormat import com.android.build.api.dsl.ManagedVirtualDevice import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree    plugins {     alias(libs.plugins.multiplatform)     alias(libs.plugins.compose.compiler)     alias(libs.plugins.compose)     alias(libs.plugins.android.application)     alias(libs.plugins.kotlinx.serialization) } kotlin {     androidTarget {         compilations.all {             compileTaskProvider {                 compilerOptions {                     jvmTarget.set(JvmTarget.JVM_1_8)                     freeCompilerArgs.add("-Xjdk-release=${JavaVersion.VERSION_1_8}")                 }             }         }         //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html         @OptIn(ExperimentalKotlinGradlePluginApi::class)         instrumentedTestVariant {             sourceSetTree.set(KotlinSourceSetTree.test)             dependencies {                 debugImplementation(libs.androidx.testManifest)                 implementation(libs.androidx.junit4)             }         }     }     jvm()        listOf(         iosX64(),         iosArm64(),         iosSimulatorArm64()     ).forEach {         it.binaries.framework {             baseName = "ComposeApp"             isStatic = true         }     }     sourceSets {         commonMain.dependencies {             implementation(project(":shared-uikit"))                implementation(compose.runtime)             implementation(compose.foundation)             implementation(compose.material3)             implementation(compose.components.resources)             implementation(compose.components.uiToolingPreview)             implementation(libs.coil)             implementation(libs.coil.network.ktor)             implementation(libs.kotlinx.coroutines.core)             implementation(libs.ktor.core)             implementation(libs.kotlinx.serialization.json)             implementation(libs.kotlinx.datetime)         }         commonTest.dependencies {             implementation(kotlin("test"))             @OptIn(ExperimentalComposeLibrary::class)             implementation(compose.uiTest)             implementation(libs.kotlinx.coroutines.test)         }         androidMain.dependencies {             implementation(compose.uiTooling)             implementation(libs.androidx.activityCompose)             implementation(libs.kotlinx.coroutines.android)             implementation(libs.ktor.client.okhttp)         }         jvmMain.dependencies {             implementation(compose.desktop.currentOs)             implementation(libs.kotlinx.coroutines.swing)            implementation(libs.ktor.client.okhttp)         }         iosMain.dependencies {             implementation(libs.ktor.client.darwin)         }     } }   android {     namespace = "io.github.dmitriy1892.gradleconventionpuginssample"     compileSdk = 34     defaultConfig {         minSdk = 24         targetSdk = 34         applicationId = "io.github.dmitriy1892.gradleconventionpuginssample.androidApp"         versionCode = 1         versionName = "1.0.0"         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"     }     sourceSets["main"].apply {         manifest.srcFile("src/androidMain/AndroidManifest.xml")         res.srcDirs("src/androidMain/res")     }     //https://developer.android.com/studio/test/gradle-managed-devices     @Suppress("UnstableApiUsage")     testOptions {         managedDevices.devices {             maybeCreate<ManagedVirtualDevice>("pixel5").apply {                 device = "Pixel 5"                 apiLevel = 34                 systemImageSource = "aosp"             }         }     }     compileOptions {         sourceCompatibility = JavaVersion.VERSION_1_8         targetCompatibility = JavaVersion.VERSION_1_8     }     buildFeatures {         //enables a Compose tooling support in the AndroidStudio         compose = true     } }   compose.desktop {     application {         mainClass = "MainKt"         nativeDistributions {             targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)             packageName = "io.github.dmitriy1892.gradleconventionpuginssample.desktopApp"             packageVersion = "1.0.0"         }     } }

Код выглядит монструозно. Если проект многомодульный, в каждом модуле содержится build.gradle.kts-файл примерно с таким же наполнением.

Все бы ничего, но есть моменты, которые могут доставить проблем и заставить вручную пройти по всем модулям, если держать конфигурации модулей как в примере. Так может случиться, когда нужно:

1. Поднять версии библиотек, minSdk/targetSdk/compileSdk или Java;

2. Поднять версию Gradle-wrapper или Android Gradle Plugin, что в случае изменения API классов или функций gradle-конфигураций может повлечь необходимость изменений в файлах build.gradle.kts каждого модуля;

3. Обработать deprecation-ы разного рода конфигураций в новых версиях Gradle Plugin, AGP, KMP и тому подобное:

4. Копипастить все из build.gradle.kts при необходимости создать модуль вручную и нудно настраивать под конкретный модуль.

Хочется избавиться от этого списка проблем, повторяющихся частей, соблюсти DRY. На помощь может прийти Gradle Convention Plugins.

Gradle Convention Plugins

Gradle Convention Plugins — это инструмент, позволяющий переиспользовать конфигурации сборки, уменьшить Boilerplate в build.gradle.kts-файлах и проще управлять изменениями при смене версий библиотек, плагинов и тому подобного. А еще он упрощает поддержку конфигураций сборки.

Для использования механизма нужно создать модуль для Convention Plugins, выделить базовые конфигурации, применить их при написании Convention Plugins, зарегистрировать плагины в build.gradle.kts нашего модуля с плагинами и использовать в проекте. 

На каждом моменте остановимся подробнее. Используем подход с построением Composite Builds — это подход, в котором наши плагины и конфигурации сборки лежат в отдельных независимых модулях и подключаются к процессу сборки основного проекта. При этом модули с плагинами собираются раньше, чтобы предоставить плагины для сборки нашего проекта.

Пример того, как можно писать Convention Plugins, есть в моем проекте на GitHub, начальный код в ветке initial.

Нам нужно подготовиться, чтобы написать Convention Plugins:

  1. Создать gradle-модуль, не зависящий от каких-либо других в проекте, — в нем мы будем размещать наши будущие Convention Plugins.

  1. Подключить модуль с плагинами в корневой проект так, чтобы он собирался до сборки остальных модулей проекта. Результат сборки — скомпилированные Convention Plugins, будем использовать в модулях основного проекта.

  2. Сконфигурировать build.gradle.kts и settings.gradle.kts этого модуля правильным образом, чтобы Gradle понимал, что это модуль с плагинами.

По сути, мы будем использовать механизм композитной сборки Gradle — у нас будет независимый модуль, который по системе понятий Gradle будет независимым проектом. После сборки такого проекта его скомпилированные артефакты могут быть использованы другим gradle-проектом, его подмодулями и подпроектами, который подключил наш независимый проект как композит. 

Начинаем настройку для написания Gradle Convention Plugins:

1. Создадим папку convention-plugins в проекте, в ней создадим папку base, в которой будут содержаться базовые конфигурации и extension-функции для наших плагинов. Добавим пустые gradle-файлы base-модуля.

Заготовка базового модуля под плагины

Заготовка базового модуля под плагины

2. Зайдем в settings.gradle.kts и пропишем наш модуль как includeBuild:

Подключение модуля convention plugins в settings.gradle.kts проекта

Подключение модуля convention plugins в settings.gradle.kts проекта

Подключение модуля Convention Plugin отличается от подключения обычного модуля функцией includeBuild, а путь прописывается не через двоеточия, а через slash (`/`).
Вот тут как раз и будет работать механизм композитной сборки: сначала соберется модуль или проект, указанный в includeBuild, а потом — основной проект.

3. Конфигурируем build.gradle.kts и settings.gradle.kts для нашего модуля с плагинами.

Конфигурируем settings.gradle.kts — пропишем необходимые репозитории с плагинами и зависимостями, из которых будем запрашивать нужные нам зависимости для использования в наших будущих Convention Plugin-ах:

 import java.net.URI     pluginManagement {      repositories {          google()          gradlePluginPortal()          mavenCentral()          maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")      }  }    dependencyResolutionManagement {      repositories {          google()          mavenCentral()          maven {              url = URI("https://androidx.dev/storage/compose-compiler/repository/")          }      }      versionCatalogs {          create("libs") {              from(files("../../gradle/libs.versions.toml"))          }      }  }  rootProject.name = "base"

Мы указали репозитории для плагинов и зависимостей, прописали создание Version Catalog и указали путь до libs.versions.toml-файла.

Добавляем в libs.versions.toml-файл переменную с версией Java и зависимости нужных нам плагинов и конфигурируем build.gradle.kts модуля convention-plugins/base:

Добавление зависимостей плагинов и версии Java в libs.versions.toml

Добавление зависимостей плагинов и версии Java в libs.versions.toml

Дублирую зависимости для тех, кто идет по шагам при чтении статьи:

# Plugins for composite build  gradleplugin-android = { module = "com.android.tools.build:gradle", version.ref = "agp" }  gradleplugin-compose = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose" }  gradleplugin-composeCompiler = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "compose" }  gradleplugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }

Конфигурируем build.gradle.kts модуля convention-plugins/base:

 import org.jetbrains.kotlin.gradle.dsl.JvmTarget  import org.jetbrains.kotlin.gradle.tasks.KotlinCompile     plugins {      `kotlin-dsl`  }      group = "io.github.dmitriy1892.conventionplugins.base"      dependencies {      implementation(libs.gradleplugin.android)      implementation(libs.gradleplugin.compose)      implementation(libs.gradleplugin.composeCompiler)      implementation(libs.gradleplugin.kotlin)      // Workaround for version catalog working inside precompiled scripts      // Issue - https://github.com/gradle/gradle/issues/15383      implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))  }      private val projectJavaVersion: JavaVersion = JavaVersion.toVersion(libs.versions.java.get())     java {      sourceCompatibility = projectJavaVersion      targetCompatibility = projectJavaVersion  }  tasks.withType<KotlinCompile>().configureEach {      compilerOptions.jvmTarget.set(JvmTarget.fromTarget(projectJavaVersion.toString()))  } 

В коде мы сделали следующее:

  • В блоке plugins подключили плагин kotlin-dsl, чтобы использовать Gradle Kotlin Dsl для написания наших Convention Plugins;

  • В блоке dependencies добавили плагины, которые будем использовать в наших будущих Convention Plugins. Сразу отмечу, что добавлять плагины через блок plugins не получится: такова специфика модуля с Convention Plugin;

  • В блоке dependencies добавили Workaround, чтобы в наших плагинах были доступны Version Catalogs. Пока нет возможности по-другому использовать Version Catalogs, есть issue.

  • Задали версию Java и kotlin-компилятора.

Создаем первый пустой Convention Plugin и подключаем его к корневому build.gradle.kts нашего проекта. Это нужно для того, чтобы работали extension-функции в build.gradle.kts-файлах модулей корневого проекта, в который мы подключили наш модуль с Convention Plugins.

Создание пустого base.plugin Convention Plugins

Создание пустого base.plugin Convention Plugins
Подключение плагина к проекту

Подключение плагина к проекту

Предварительную настройку закончили — переходим к созданию базовых Сonvention pugins в .gradle.kts-файлах.

О том, как создавать плагины и переиспользуемые части в .gradle.kts-файлах и Kotlin extension-функций для упрощения написания плагинов — в следующей статье. Не переключайтесь! 


ссылка на оригинал статьи https://habr.com/ru/articles/843648/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *