Интересная заметка от Madis Pink в блоге ZeroTurnaround Rebel Labs. Если кто-то вас разбудит посреди ночи и спросит: «какую фичу в Gradle должен знать каждый?» — с уверенностью отвечай, что это buildSrc
. Это особый магический Gradle-проект внутри твоего репозитория, доступный всем файлам build.gradle
в виде библиотеки.
Описанный далее подход позволяет писать код на удобном тебе JVM-языке, и результат использовать прямо в своих сборочных скриптах. Как бонус, можно покрыть юнит-тестами особо хитрые моменты в этих скриптах. Добро пожаловать под кат!
Привет, мир buildSrc!
Подключить эту фичу просто. Создаем директорию с именем buildSrc
в корне проекта (в той же директории, где лежит settings.gradle
):
mkdir buildSrc
К этому проекту одновременно применятся и плагин java
, и плагин groovy
. Поэтому дальше мы просто делаем стандартную директорию src/main/java
и начинаем добавлять туда код.
mkdir -p buildSrc/src/main/java
Теперь можно синхронизировать проект с IDE и создать новый Java-класс в свежеиспеченном проекте buildSrc
:
package myapp.gradle;
public class Fun {
public static void sayHello() {
System.out.println("Hello from buildSrc!");
}
}
Остался последний шаг: позвать метод Fun.sayHello
из какого-нибудь файла build.gradle
. Давайте сделаем простой таск hello
в корневом проекте и посмотрим на выхлоп:
import myapp.gradle.Fun
task hello {
doLast {
Fun.sayHello()
}
}
$ ./gradlew hello
> Task :hello
Hello from buildSrc!
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Вот и всё. Метод sayHello
— это обычный Java-метод. Поскольку файл build.gradle
работает как JVM-приложение, можно звать его из Groovy как любой другой Java-метод.
Пишем свои собственные таски
Переиспользование кода — это хорошо, но весь этот мусор из doLast {}
при создании нового таска выглядит мерзко. С помощью buildSrc
мы сможем выбросить мусор, создав вместо него специальный класс таска. Свой Gradle-таск — это просто Java-класс, который:
- Наследуется от
org.gradle.api.DefaultTask
- Имеет публичный метод с аннотацией
@TaskAction
. Этот код исполняется сразу после запуска нашего таска.
Давайте отрефакторим метод Fun.sayHello
в отдельный таск:
package myapp.gradle;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;
public class HelloTask extends DefaultTask {
@TaskAction
public void run() {
System.out.println("Hello from task " + getPath() + "!");
}
}
Использовать этот таск из Gradle-скриптов проще некуда:
import myapp.gradle.HelloTask
task hello(type: HelloTask)
$ ./gradlew hello
> Task :hello
Hello from task :hello!
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed
Превращаем свои таски в полноценные плагины
В большом проекте приходится определять десятки тасков, встраивающихся в запутанный граф связей. В этом случае имеет смысл создать свой собственный плагин. Это плагин должен создавать все нужные таски и связывать их воедино.
package myapp.gradle;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class MyPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getTasks().create("hello", HelloTask.class);
}
}
Теперь мы можем не использовать таски напрямую, а подключать этот специальный плагин прямо в сборочный файл:
apply plugin: myapp.gradle.MyPlugin
И да, оно работает!
$ ./gradlew hello
> Task :hello
Hello from task :hello!
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
Теперь у нас есть работающий вручную написанный плагин, который можно использовать в любом из подпроектов. Конечно, описанный выше пример с распечаткой текста на экране — невообразимо малая часть того, что можно устроить в своем проекте с помощью самописных плагинов. Если хочется разобраться в плагинах глубже, очень рекомендую вот эти доклады с Gradle Summit 2017:
- Designing and writing Gradle plugins by Benjamin Muschko
- Extending the Gradle Android Plugin by Eyal Lezmy
Бонус для дочитавших до конца: управление зависимостями с помощью buildSrc
Днем автор этой заметки — разработчик в проекте JRebel for Android. Это инструмент для разработчиков, который состоит из плагина для IDE, Gradle-плагина, приложения для Android и обычного приложения на Java SE. Чтобы собрать все эти артефакты, имеется огромный Gradle-проект, состоящий из 90+ подпроектов. Все эти подпроекты имеют во многом похожие зависимости, например, commons-io
и slf4j-api
. Если тебе приходится собирать org.slf4j:slf4j-api:1.7.25
раз за разом, снова и снова, это быстро превращается в нудятину, по типа регулярного наведения порядка дома. То же самое про игру в угадывание правильной версии этих зависимостей.
Обычно для управления ими предлагается использовать блок ext {}
прямо в корне проекта. Пример есть вот в этом ответе на StackOverflow. Обратная сторона такого подхода — в IDE отвалятся автодополнение и навигация по коду.
Мы пошли другим путем — определили зависимости как строковые константы в файле Deps.java
внутри buildSrc
. С этим подходом в IDE работает и автодополнение, и навигация! Давайте посмотрим, как это выглядит на примере небольшого Android-проекта:
package myapp.gradle;
public class Deps {
public static final String androidPlugin = "com.android.tools.build:gradle:3.0.0-beta6";
public static final String kotlinVersion = "1.1.50";
public static final String kotlinPlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:" + kotlinVersion;
public static final String kotlinRuntime = "org.jetbrains.kotlin:kotlin-stdlib-jre7:" + kotlinVersion;
public static final String appCompat = "com.android.support:appcompat-v7:26.1.0";
public static final String constraintLayout = "com.android.support.constraint:constraint-layout:1.0.2";
public static final String junit = "junit:junit:4.12";
}
Теперь можно ссылаться на них внутри любых файлов build.gradle
вот так:
Когда кладешь конфигурацию сборки в buildSrc
, это ощущается… немного неправильным, что ли. Но преимущества такого подхода сильно перевешивают.
Будете ли вы пользоваться фокусом с buildSrc
? Напишите в комментариях! Кроме того, автора можно достать через Twitter @madisp и задать все наболевшие неудобные вопросы. Все примеры из этого поста доступны на GitHub.
Пост публикуется с разрешения компании ZeroTurnaround и автора — Madis Pink. Сотрудники ZeroTurnaround часто присутствуют в программном коммитете конференций JUG.ru Group, выступают как спикеры на конференциях и часто приезжают просто как гости. Читателей Хабра мы тоже приглашаем поучаствовать в JPoint, который пройдет 6-7 апреля 2018 года.
Комментарии (5)
MyDogTom
22.11.2017 15:13Попробовал трюк управления зависимостей через buildSrc, но с использованием Kotlin (ради string template). В итоге навигация и авто дополнение работают, но подсветка значения — нет. Вместо значения получаю:
public final val playServicesGcm: String defined in com.myapp.gradle.Libs
В случае с Java корректно показывает значение. Жаль :(olegchir Автор
22.11.2017 17:48Нелегкая судьба первопроходца :) Спасибо!
А может, зарепортить в JetBrains? Если есть минимальный пример.
Beholder
24.11.2017 17:48Попробуйте указать аннотацию
@JvmField
, потому что в Kotlin вы объявляете property (а это getter-метод), а не поле. Ну или писать что-то вродеgetPlayServicesGcm()
. Но думаю, учитывая возможность написания Gradle-скриптов на самом Kotlin, это всё не самые элегантные решения.
ohotNik_alex
Плагин для грэдла самый обычный. Смотрим «maven против gradle» батл — там аналогиные действия. Не понимаю в чем профит фиксированного имени плагина. Можете пояснить разницу?
olegchir Автор
Плагины можно вводить тремя способами:
* Прямо в сборочный скрипт
* В диреторию buildSrc
* Сделать отдельный проект
Имхо buildSrc — это разумный компромисс между первым и третьим. Плагин уже видно отовсюду и он не мозолит глаза в зонтичном сборочном файле, но еще не нужно соблюдать все ритуалы по поддержанию самостоятельного проекта.
Кроме того, buildSrc дает дополнительный уровень стандартизации, который упрощает людям жизнь в динамичном мире, в котором что угодно может лежать где угодно. С точки зрения нового разработчика, впервые сталкивающегося с проектом (а это почти все люди с Гитхаба, которые первый и последний раз забежали в проект, чтобы поправить единственный баг, который мешает лично им), четкое понятное место куда смотреть и патчить — лучше чем произвольное.