Прочие оптимизации кода Gradle Convention Plugins, выводы по результатам использования подхода
Всем привет! На связи Дима Котиков, и мы завершаем цикл статей о том, как облегчить себе жизнь и уменьшить boilerplate в gradle-файлах. В предыдущих статьях мы подготовили и настроили базовый модуль для написания Gradle Convention Plugins, написали несколько convention-плагинов в файлах -.gradle.kts, сделали еще один модуль и создали convention-плагины на базе kotlin-классов. В заключительной части мы немного порефакторим написанный код, попытаемся настроить области видимости convention-плагинов и extension-функций для конфигурации сборки, а также подведем итоги.
Рефакторинг зависимостей в composite builds
В разработке нет ничего более постоянного, чем временное рефакторинг. Взглянем на содержимое модуля convention-plugins/base
и увидим, что некоторые extension-функции и плагины мы бы не хотели отдавать пользователям в руки, а хотели бы использовать только при написании кастомных плагинов. Например, плагины базовых конфигураций и extension-ы вроде Project.libs
, Project.androidConfig
, которые и так доступны в файлах build.gradle.kts
модулей проекта через сгенерированные accessor-ы или из-за и так подключенных плагинов.
Мы бы не хотели терять красивые extension-функции для указания зависимостей для каждого таргета и функцию для конфигурации iOS Framework. Можно просто и безболезненно перенести файлы IosExtensions.kt
и KmpDependenciesExtensions.kt
в модуль convention-plugins/project
, что и сделаем:
Перенос IosExtensions.kt
и KmpDependenciesExtensions.kt
в модуль convention-plugins/project
Теперь ничто не мешает убрать подключение модуля convention-plugins/base
из файла settings.gradle.kts
корневого проекта:
Исключение модуля convention-plugins/base
из settings.gradle.kts
корневого проекта
Синхронизируем проект, пытаемся запустить — все работает. Для проверки того, что код из convention-plugins/base
недоступен, пробуем добавить в composeApp/build.gradle.kts
плагин android.base.config
и extension-функцию androidConfig
:
Проверка отключения convention-plugins/base
Пытаемся синхронизировать проект, запустить… И он синхронизируется и запускается, хотя мы хотели бы падать. Это происходит потому, что плагины из convention-plugins/base
подключены в convention-plugins/project
, а также в build.gradle.kts
корневого проекта остался подключенным мок-плагин base.plugin
. Давайте его удалим:
Удаление base.plugin
из build.gradle.kts
корневого проекта
Пробуем синхронизироваться, запустить проект — все равно работает. Все потому, что плагин, подключенный через includeBuild
, транзитивно отдает свою логику. Частично эту проблему можно решить, если в convention-plugins/project/build.gradle.kts
поменять содержимое plugins-блока:
Смена конфигурации plugins-блока в convention-plugins/project/build.gradle.kts
Тогда при попытке собрать проект перестанут быть видны convention-плагины, описанные в файлах -build.gradle.kts
, но это все равно не решает проблему с тем, что остаются видными extension-функции из модуля convention-plugins/base
.
Пока я не нашел способа эффективно скрыть код из подключенного как includeBuild
проекта без применения модификаторов видимости на классах. Делитесь в комментариях, если знаете.
В общий код можно вынести объявление точки входа в compose-desktop на случай, если в проекте предполагается несколько app-модулей. Сейчас в composeApp/build.gradle.kts
это выглядит вот так:
Объявление точки входа для desktop-таргета
Вынесем это в отдельный extension — добавим зависимости на compose-плагины в convention-plugins/project/build.gradle.kts
, как и в convention-plugins/base/build.gradle.kts
, и создадим отдельный файл — ComposeMultiplatformExtensions.kt
в модуле convention-plugins/project
.
Чтобы сконфигурировать extension, мы должны понять, что именно конфигурировать. Провалимся в реализацию функции compose.desktop {}
, откроется сгенерированный файл accessor:
Accessor-файл со сгенерированной функцией compose.desktop {}
и extension property
Видим, что содержимое функции скрыто. Попробуем в лоб сконфигурировать DesktopExtension
, который у нас приходит в функции ComposeExtension.desktop()
. Заполним ComposeMultiplatformExtensions.kt
:
package io.github.dmitriy1892.conventionplugins.project.extensions
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.compose.desktop.DesktopExtension
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
fun Project.composeDesktopApplication(
mainClass: String,
packageName: String,
version: String = libs.versions.appVersionName.get(),
targetFormats: List = listOf(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
) {
configure {
application {
this.mainClass = mainClass
nativeDistributions {
targetFormats(*targetFormats.toTypedArray())
this.packageName = packageName
this.packageVersion = version
}
}
}
}
Применим наш extension в composeApp/build.gradle.kts
:
Применение composeDesktopApplication
в composeApp/build.gradle.kts
Пробуем собрать, видим ошибку:
Ошибка конфигурации DesktopExtension
Ошибка указывает, что DesktopExtension
не найден в проекте. Скорее всего, он достается другим способом, но каким? Чтобы выяснить, вернемся к accessor-файлу и декомпилируем его в java-класс:
Путь до инструмента декомпиляции kotlin-файла в java
Декомпилированный accessor
Видим, что на вход функции приходит ComposeExtension
, который достается из Project.extensions
, потом у ComposeExtension
вызывается getExtensions()
, — и только уже в этих экстеншенах конфигурируется desktop
. Теперь можем вернуться в ComposeMultiplatformExtensions.kt
и откорректировать внутрянку нашей extension-функции:
package io.github.dmitriy1892.conventionplugins.project.extensions
import io.github.dmitriy1892.conventionplugins.base.extensions.libs
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.compose.ComposeExtension
import org.jetbrains.compose.desktop.DesktopExtension
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
fun Project.composeDesktopApplication(
mainClass: String,
packageName: String,
version: String = libs.versions.appVersionName.get(),
targetFormats: List = listOf(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
) {
extensions.getByType().extensions.configure {
application {
this.mainClass = mainClass
nativeDistributions {
targetFormats(*targetFormats.toTypedArray())
this.packageName = packageName
this.packageVersion = version
}
}
}
}
Пробуем синхронизироваться — успешно. Собираем desktop-таргет командой ./gradlew :composeApp:run
— все работает.
Выводы, плюсы и минусы подхода
Пришла пора подвести черту, указать на достоинства и недостатки подхода.
Плюсы подхода:
При применении convention-плагинов может существенно сократиться размер файлов
build.gradle.kts
из-за выделения части конфигураций в плагины и extension-функции.Создание и настройка нового модуля упрощаются за счет применения convention-плагинов с обобщенной логикой.
Основная логика с настройкой сборки модулей собрана в одном месте. В нашем примере — в двух модулях.
Миграция на новые версии плагинов
gradle-wrapper
/agp
/kmp
с какими-либо breaking changes становится легче, потому что изменения нужно будет внести точечно в конкретных convention-плагинах и extension-функциях вместо кучи файловbuild.gradle.kts
модулей.
Минусы подхода:
Увеличение скорости сборки — сначала должны собраться модули с нашими плагинами, подключенные как composite builds, а только потом основной проект.
При изменении обобщенных плагинов есть риск сломать сборку в модулях и других плагинах, которые его используют.
Чтобы понять, что происходит в
build.gradle.kts
-файлах основного проекта, нужно смотреть, из чего состоит convention plugin, что в нем сконфигурировано и подключено из внешних зависимостей.Слишком мудрено написанные плагины могут требовать много времени на то, чтобы в них разобраться.
Выводы:
Стоит здраво оценить, нужно ли внедрять convention-плагины в конкретном проекте. Для пет-проектов с 1—2 модулями это может быть избыточным, но для production-проектов с большим количеством модулей подход однозначно будет полезен за счет обобщения логики и вытекающих из этого плюсов.
Нужно разбивать общие части в плагины так, чтобы модули можно было гибко сконфигурировать. Плагины для feature-модулей могут быть избыточными для core/common-модулей.
Нужно внимательно относиться к включению в плагины внешних зависимостей, так как зависимости могут быть нужны не во всех модулях, в которые они подключаются.
Важно знать, когда остановиться. Не стоит переусложнять внутреннюю реализацию convention-плагинов.
Стоит перестать бояться Gradle и навести порядок в скриптах сборки, это не так уж и сложно! :)
Ссылки на предыдущие части:
Gradle Convention Plugins: как облегчить себе жизнь и уменьшить boilerplate в gradle-файлах
Создание плагинов и переиспользуемых частей в .gradle.kts-файлах и Kotlin extension-функциях
Создание Convention Plugin-ов на базе Kotlin-классов