Всем привет! На связи Дима Котиков, и мы завершаем цикл статей о том, как облегчить себе жизнь и уменьшить 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
Перенос IosExtensions.kt и KmpDependenciesExtensions.kt в модуль convention-plugins/project

Теперь ничто не мешает убрать подключение модуля convention-plugins/base из файла settings.gradle.kts корневого проекта:

Исключение модуля 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/base подключены в convention-plugins/project, а также в build.gradle.kts корневого проекта остался подключенным мок-плагин base.plugin. Давайте его удалим:

Удаление base.plugin из build.gradle.kts корневого проекта 
Удаление base.plugin из build.gradle.kts корневого проекта 

Пробуем синхронизироваться, запустить проект — все равно работает. Все потому, что плагин, подключенный через includeBuild, транзитивно отдает свою логику. Частично эту проблему можно решить, если в convention-plugins/project/build.gradle.kts поменять содержимое plugins-блока:

Смена конфигурации plugins-блока в convention-plugins/project/build.gradle.kts
Смена конфигурации plugins-блока в convention-plugins/project/build.gradle.kts

Тогда при попытке собрать проект перестанут быть видны convention-плагины, описанные в файлах -build.gradle.kts, но это все равно не решает проблему с тем, что остаются видными extension-функции из модуля convention-plugins/base.

Пока я не нашел способа эффективно скрыть код из подключенного как includeBuild проекта без применения модификаторов видимости на классах. Делитесь в комментариях, если знаете.

В общий код можно вынести объявление точки входа в compose-desktop на случай, если в проекте предполагается несколько app-модулей. Сейчас в composeApp/build.gradle.kts это выглядит вот так:

Объявление точки входа для desktop-таргета
Объявление точки входа для 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
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<TargetFormat> = listOf(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
) {
    configure<DesktopExtension> {
        application {
            this.mainClass = mainClass
             
            nativeDistributions {
                targetFormats(*targetFormats.toTypedArray())
                this.packageName = packageName
                this.packageVersion = version
            }
        }
    }
}

Применим наш extension в composeApp/build.gradle.kts:

Применение composeDesktopApplication в composeApp/build.gradle.kts
Применение composeDesktopApplication в composeApp/build.gradle.kts

Пробуем собрать, видим ошибку:

Ошибка конфигурации DesktopExtension
Ошибка конфигурации DesktopExtension

Ошибка указывает, что DesktopExtension не найден в проекте. Скорее всего, он достается другим способом, но каким? Чтобы выяснить, вернемся к accessor-файлу и декомпилируем его в java-класс:

Путь до инструмента декомпиляции kotlin-файла в java
Путь до инструмента декомпиляции kotlin-файла в java
Декомпилированный accessor
Декомпилированный 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<TargetFormat> = listOf(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
) {
    extensions.getByType<ComposeExtension>().extensions.configure<DesktopExtension> {
        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 и навести порядок в скриптах сборки, это не так уж и сложно! :) 

Ссылки на предыдущие части:

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

  2. Создание плагинов и переиспользуемых частей в .gradle.kts-файлах и Kotlin extension-функциях

  3. Создание Convention Plugin-ов на базе Kotlin-классов

Комментарии (2)


  1. littlesavage
    27.09.2024 12:36
    +1

    Пара моментов по convention-плагинам и kotlin-dsl, о которых мало кто говорит.
    1. ID плагинов. Все обычно использут короткие ID, а-ля "android.base.config". Я считаю, что абсолютно у всех плагинов должен быть FQN, в том числе и у convention-плагинов. Т.е. "io.github.dmitriy1892.conventionplugins.android.base.config". Длиннее, но в таком виде плагины будет гораздо проще публиковать. А один из самых действенных способов борьбы со скоростью сборки — это публикация convention-плагинов во внутренний репозиторий.
    2. Название и расположение файлов. Вы создаете файл "android.base.config.gradle.kts" в корне. Для плагина с FQN пришлось бы создавать файл с именем "io.github.dmitriy1892.conventionplugins.android.base.config.gradle.kts". Но это не единственный способ создания плагина. Другой способ — это создать этот файл в io/github/dmitriy1892/conventionplugins/android.base.config.gradle.kts и в него добавить строку package: `package io.github.dmitriy1892.conventionplugins` . Так имя файла будет короче, все классы будут в одном пакете, у плагина будет FQN ID и в нем можно будет использовать классы из того же пакета без использования `import`.


    1. dkotikov Автор
      27.09.2024 12:36

      Спасибо за дельный комментарий!

      Согласен, при публикации плагинов FQN - это must have.
      Вместе с тем, для плагинов, которые используются только в рамках конкретного проекта , если FQN длинный, такой нейминг может трудно запоминаться или быть не совсем удобным в использовании.

      По второму пункту - да, можно располагать -gradle.kts файлы и в подпапках, а не в корневой папке, но важно не забыть при использовании плагина дописывать путь через точку где он лежит, т.е. если у файла такое расположение - io/github/dmitriy1892/conventionplugins/android.base.config.gradle.kts , то при использовании в plugins-блоке в build.gradle.kts -файлах нужно указывать так:

      plugins {   
          id("io.github.dmitriy.conventionplugins.android.base.config")
      }

      Скорее всего, из-за такого не совсем очевидного момента (и возможных неудобств в использовании, если запихать файл слишком глубоко в директории), подход с хранением -gradle.kts файлов в не-рутовых папках и не возымел широкого распространения :)