Доброго времени, читатель! В предыдущей статье мы рассмотрели, как эффективно использовать стандартные инструменты Gradle в проектах для решения повседневных задач и немного коснулись подкапотного пространства. Под катом статьи проведём тур по Gradle-плагинам, разберёмся, для чего вы можете писать кастомные плагины уже сейчас, проникнемся процессом разработки на примере реализации плагина для Kotlin-кодогенерации и научимся обходить некоторые Gradle-грабли.  В первой части статьи проведём небольшой теоретический экскурс и рассмотрим способы реализации плагинов, а во второй части двинемся к практической задаче и проникнемся разработкой плагина для Kotlin-кодогенерации. Для реализации Gradle-задумок по ходу статьи я буду использовать Kotlin. Заваривайте ваш любимый напиток и поехали. Готовьсь, цельсь, пли! Как не обжечься при сборке Gradle-приложения, и настолько ли всё серьезно?   Gradle Plugin: Что, зачем и как? Дикая природа Gradle Task: руководство по выживанию Итак, Gradle-плагины представляют собой контейнеры, которые могут содержать в себе настраиваемую логику и различные задачи для сценариев сборки проекта.  Плагины полностью автономны и позволяют хранить логику для её повторного использования в нескольких проектах или Gradle-модулях. Они замечательно подходят для любых задач, требующих работы с исходным кодом. Такими задачами могут быть кодогенерация / генерация документации / проверка кода / запуск задач на CI / деплой и многое другое.  С точки зрения кода, плагин представляет собой реализацию примитивного интерфейса с единственным методом apply: Project-ом является Gradle-проект (или Gradle-модуль), куда подключается плагин. Сам по себе интерфейс Project больше похож на God Object, поскольку в нём доступен сразу весь Gradle API, что, в принципе, достаточно удобно.  Gradle предлагает разработчикам несколько способов реализации плагинов (как и практически всего в Gradle). Дальше рассмотрим каждый способ по-отдельности и определим его плюсы и минусы.  Самый простой вариант – реализовать плагин прямо внутри файла конфигурации. Да, просто берём и пишем класс по примеру выше в build.gradle(.kts). Если честно, трудно понять, какой пользой обладает такое решение, ведь Gradle уже предоставляет возможность обратиться project-у из файла конфигурации и накрутить его нужной логикой. А вот недостатков получаем сразу несколько. Во-первых, подключить такой плагин можно только в текущий скрипт, в связи с чем теряется и суть плагина в возможности его повторного использования. Во-вторых, плагин будет компилироваться каждый раз при конфигурации проекта, что негативно повлияет на производительность сборки. На мой субъективный взгляд, от такого варианта можно отказаться и при необходимости дописать нужную логику прямо в скрипте конфигурации. Давайте перейдём к более жизнеспособным вариантам.  Модуль buildSrc компилируется и поставляется на этап конфигурации проекта в виде jar, поэтому реализовав плагин в buildSrc, получаем возможность его использования в Gradle-модулях проекта. Однако из-за известных проблем c инвалидацией кеша, реализация плагинов в buildSrc имеет право на жизнь в том случае, если для вашего проекта buildSrc представляет собой около-константный модуль, в котором редко происходят изменения.   Также такой вариант также является более компромиссным в сравнении со standalone-плагинами с точки зрения затрат на реализацию и вполне может подойти для небольших проектов. Допустим, такой вариант нам подходит. Выполним подготовительный шаг и подключим в buildSrc плагин Kotlin DSL. Он сделает всю грязную работу по подключению Kotlin, подключит Gradle API и ещё м.  P.S. Поскольку для .kts скриптов используется embedded-версия Kotlin, которая лежит вместе с Gradle, то самостоятельно подключать свежую версию Kotlin для реализации плагинов не рекомендуется во избежание проблем с обратной совместимостью. На практике проблем не встречал, но потенциально может выстрелить. Кладём реализацию плагина в buildSrc/src/main/kotlin. Охапка дров, плагин готов. Теперь можно подключать плагин в проект с помощью apply:  Нагляднее будет подключать плагин c помощью id, поэтому давайте его зададим. Плагин Kotlin DSL транзитивно подключает Java Gradle Plugin Development Plugin, который предоставляет такую возможность: В результате будет создан следующий файл: src/main/resources/META-INF/gradle-plugins/ru.myorg.demo.my-plugin.properties  , который Gradle бережно положит в jar и дальше сможет соотносить id плагина с его реализацией. Теперь подключить плагин к проекту можно так: В Gradle присутствует интересная возможность реализовать плагин в виде скрипта. Скриптовый плагин по структуре идентичен скриптам конфигурации build.gradle(.kts) и может быть реализован как на Groovy, так и на Kotlin. При реализации на Kotlin существует достаточно весомое ограничение – скрипты должны находиться либо в buildSrc, либо в standalone Gradle-проекте, чтобы быть скомпилированными.  В случае с Groovy такого ограничения нет, и плагин можно реализовать, например, в рутовой папке проекта, а затем подключить в нужные модули с помощью apply from. Приятным бонусом script-плагинов является автоматическая генерация id. Это сделает за нас Gradle Plugin Development Plugin, исходя из названия скрипта. Важно: При реализации и использовании script-плагинов необходимо учесть следующие ограничения, связанные с версиями Gradle:  При реализации script-плагина в отдельном проекте и его подключении через композитную сборку, необходимо использовать Gradle версии 7.1 и выше из-за бага, при котором падала сборка после подключения плагина Kotlin DSL. Во всех проектах, куда подключается плагин с помощью композитной сборки, также необходимо использовать Gradle 7.1 и выше.  Script-плагины, написанные на Kotlin, можно подключать в проекты, использующие Gradle 6 и выше. Script-плагины, написанные на Groovy, можно подключать в проекты, использующие Gradle 5 и выше.  Script-плагины можно писать на Kotlin, начиная с Gradle 6.0, и на Groovу, начиная с Gradle 6.4. Лучше всегда держать версию Gradle в актуальном состоянии и не забивать голову лишними вопросами. Давайте проведём небольшой эксперимент. Попробуем вынести в script-плагин на Kotlin общую конфигурацию Android-модулей и сделаем это в buildSrc.  Подключаем Android Gradle Plugin: Теперь напишем сам плагин:  buildSrc/src/main/kotlin/android-common.gradle.kts: Жмём Gradle Sync и получаем следующее время конфигурации: BUILD SUCCESSFUL IN 2m 6s Почему так много? Я начал разбираться и первым делом зашёл в папку buildSrc/build, немного покопался и увидел там следующее: Оказалось, что для плагинов, которые были объявлены в блоке plugins {} также производится кодогенерация Kotlin-аксессоров для использования в .kts-скриптах. На выходе получаем несколько сотен сгенерированных файлов для плагина com.android.library. Кстати, именно это происходит при первом подключении Kotlin DSL в Gradle-проект, но лишь за тем отличием, что кодогенерация осуществляется внутри .gradle директории на вашем рабочем устройстве.  Из этой грустной истории можно сделать вывод, что c подключением сторонних плагинов в script-плагине необходимо быть вдвойне аккуратными, а лучше и вовсе отказаться от этой идеи в пользу классов. Потеря в удобстве, на мой взгляд, незначительная. Попробуем реализовать тот же функционал с помощью класса: В коде выше android {}  будет самописным Kotlin-екстеншном: Получаем следующее время конфигурации: BUILD SUCCESSFUL in 48s и никакого лишнего кодгена. Not bad, not terrible. Замеры проводил на рабочем проекте, поэтому ваши результаты могут отличаться. А чтобы плагин было удобно подключать, генерируем ему id и в buildSrc самостоятельно напишем Kotlin-екстеншн: Подключаем: Получилось ничем не хуже, и такой вариант вполне безопасно использовать.  В ходе статьи можно заметить, что некоторые плагины удобным образом подключаются с помощью Kotlin-екстеншнов.  Дело в том, что они хитро вшиты в сам Gradle, и написать собственный екстеншн получится только при реализации плагина в buildSrc. Если же плагин реализован в standalone-проекте, то при попытке использования самописного екстеншна получаем ошибку компиляции. Проблема актуальна вплоть до Gradle 7.1. Очень ждём, что это будет исправлено будущих релизах.  Теперь давайте двинемся дальше и рассмотрим полноценные standalone-плагины. Standalone-плагин подразумевает реализацию в отдельном Gradle-проекте. Такой способ является наиболее предпочтительным, поскольку позволяет избежать всех недостатков buildSrc и script-плагинов. Если вы не хотите создавать отдельный git-репозиторий и планируете использовать плагин внутри одного проекта, то можно воспользоваться возможностью композитной сборки.  Планомерно приступим к небольшой практической задаче. Создадим новый проект и подключим всё, что нужно: В зависимости добавим Kotlin Poet, с помощью которого будет реализована кодогенерация и Kotlin Gradle Plugin, который поможет интегрировать сгенерированный код в компиляцию JVM-проекта.  Теперь давайте определимся с параметрами плагина и с тем, как их передавать. Для примера в качестве параметра сделаем файл с каким-нибудь сообщением. Это сообщение будет использоваться в качестве значения переменной в сгенерированном классе. Также в параметрах будем передавать рутовую директорию для кодогенерации и название пакета для сгенерированного класса. Для передачи параметров в плагин в Gradle используются extension-контейнеры, которые, по своей сути, ничем не отличаются от обычных классов. В нашем случае extension-контейнер может выглядеть следующим образом: Контейнер должен быть abstract или open классом, поскольку Gradle будет хитро создавать его инстанс с помощью рефлексии.  Для хранения параметров вместо стандартных переменных следует использовать специальные Gradle properties. Они обеспечивают оптимизацию потребления памяти благодаря ленивой инициализации и дают возможность сконфигурировать плагин в случае, если входные параметры заранее неизвестны и будут установлены позже (например, после выполнения другой Gradle-таски). Для проперти также можно указать дефолтное значение на случай, если к моменту обращения в ней не лежит никакого значения. Создать и зарегистрировать extension-контейнер можно так: Последним параметром передается аргумент (или аргументы) для конструктора. Идеально, если единственным аргументом будет project, но спроектировать можно как угодно. Созданный extension можно использовать в скриптах конфигурации build.gradle(.kts) следующим образом: Теперь самое время создать саму Gradle-таску, которая будет выполнять кодогенерацию:     Аннотацией @TaskAction помечена функция, с которой таска начинает своё выполнение. Геттеры и сеттеры для Kotlin-пропертей помечены специальными Gradle-аннотациями, чтобы таска имела возможность выполняться инкрементально, то есть не выполняться, когда на то нет необходимости. Кому любопытно, полный исходный код доступен на моём Github. Теперь давайте сделаем так, чтобы сгенерированный файл успешно компилировался. Директорию, в которой осуществляется кодогенерация, добавим в основной sourceSet. Таким образом явно объявим, что в директории хранится исходный код, который должен быть скомпилирован. С помощью afterEvaluate дождёмся окончания конфигурации, чтобы убедиться, что sourceSets уже были созданы.   Регистрируем кодген-таску, а саму компиляцию вежливо просим её подождать: Всё готово к использованию. Получившийся плагин можно изучить под спойлером: Воспользуемся композитной сборкой, чтобы подключить плагин в проект: settings.gradle.kts: и сконфигурируем: Прежде чем нажать на Build, положим в корень проекта файл с сообщением. Собираем проект и видим созданный в build/src-gen созданный класс.  Ура! Теперь сгенерированный класс можно использовать в проекте. Самое время протестировать реализованное. В первую очередь, ребята из Gradle советуют тестировать плагин ручками. То есть, подключаем плагин с помощью композитной сборки к проекту и пользуемся всеми радостями дебаггинга. Например, чтобы отдебажить конфигурацию плагина, выбираем интересующую нас таску из раздела Run Configurations и жмём Debug. Аналогично можно дебажить любые Gradle-таски. Для автоматического тестирования Gradle-плагина предусмотрено три варианта: функциональные, интеграционные и модульные тесты.  С модульными тестами всё стандартно – обкладываемся моками, подключив для этого нужные библиотеки, и проверяем работу реализованных компонентов. Модульный тест отвязан от знания Gradle и проверяет корректность работы какого-либо компонента в изоляции. Для модульных тестов по умолчанию создаётся sourceSet test ,  поэтому никаких подготовительных шагов не требуется. Для функциональных и интеграционных тестов выполним подготовительный шаг и создадим кастомные sourceSets. Никто не запрещает делать всё в одном сете с модульными тестами, однако подход с раздельными sourceSet позволяет подключать только необходимые для конкретного вида тестов зависимости и в целом изолировать тесты друг от друга.  Чтобы запускать тесты из кастомных sourceSet, самостоятельно создадим соответствующие Gradle-таски. Для удобства запуска всех тестов сразу, свяжем их в стандартную таску check. В итоге конфигурация для интеграционных и функциональных тестов будет выглядеть следующим образом: build.gradle.kts (Gradle-проект с плагином): Теперь приступим к разработке интеграционного теста, в котором проверим взаимодействие плагина с внешней средой, а именно с файловой системой. Здесь мы всё ещё ничего не знаем о Gradle и проверяем работу бизнес-логики.  Создаём одноимённую для соответствующего sourceSet директорию в /src и реализуем тест.  src/integrationTest/kotlin/{packageName}: Теперь перейдём к самому интересному – функциональным тестам. Они позволяют проверить работу плагина целиком совместно с Gradle и его жизненным циклом. Для этого будем запускать настоящий Gradle Daemon. Конфигурация функциональных тестов практически ничем не отличается от конфигурации интеграционных тестов, за тем исключением, что больше не нужна зависимость на модуль с Gradle-плагином. Чтобы запустить Gradle Daemon, необходимо создать и сконфигурировать Gradle Runner. Для этого добавляем Gradle Runner API в classpath для функциональных тестов следующим образом: В тесте эмулируем структуру проекта, конфигурируем Gradle Runner, запускаем и смотрим что получилось. Сам тест лежит под спойлером: src/functionalTest/kotlin/{packageName}: Отлично! Плагин протестировали, можно похвалить себя чем-то приятным. Осталось дело за малым – задеплоить плагин.  Для этого воспользуемся плагином Maven Publish. Формируем публикацию и объявляем список репозиториев, в которые она сможет публиковаться: Gradle-плагины представляют собой действительно мощный инструмент для дополнения логики сборки необходимыми для вас задачами. К сожалению, в Gradle API по-прежнему много загадок и непонятных проблем, в том числе не до конца раскрытых в документации. Это создаёт препятствия на пути к удобству использования, однако команда Gradle над этим активно работает. На рабочих проектах мы широко используем Gradle-плагины для хранения общей логики сборки, выполнения специфической кодогенерации, а также выполнения различных инфраструктурных задач на CI. А какие задачи решаете вы с помощью Gradle-плагинов? Напишите в комментариях. Также я открыт к любым обсуждениям по материалу, буду рад замечаниям и предложениям. Всё изложенное в статье доступно на Github. Ниже представлю небольшой список opensource-плагинов, в исходниках которых можно подчерпнуть идеи для реализации: Releases Hub Plugin – плагин для проверки обновления зависимостей Swagger Codegen Plugin – плагин для кодогенерации swagger-контрактов. Dokka – плагин для генерации документации Kotlin-кода Спасибо за внимание! Android Dev