Шпаргалка по Gradle
Как мне кажется, большинство людей начинают разбираться с gradle только тогда, когда в проекте что-то надо добавить или что-то внезапно ломается — и после решения проблемы «нажитый непосильным трудом» опыт благополучно забывается. Причём многие примеры в интернете похожи на ускоспециализированные заклинания, не добавляющие понимания происходящего:
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.habr.hello"
minSdkVersion 20
targetSdkVersion 28
}
buildTypes {
release {
minifyEnabled false
}
}
}
Я не собираюсь подробно описывать, для чего нужна каждая строчка выше — это частные детали реализации андроид-плагина. Есть кое-что более ценное — понимание того, как всё организовано. Информация раскидана по различным сайтам/официальной документации/исходникам градла и плагинов к нему — в общем, это чуть более универсальное знание, которое не хочется забывать.
Дальнейший текст можно рассматривать как шпаргалку для тех, кто только осваивает gradle или уже забыл.
Полезные ссылки
Android studio/IDEA старательно прячет команды gradle от разработчика, а ещё при изменении build.gradle файликов начинает тупить или перезагружать проект.
В таких случаях вызывать gradle из консоли оказывается намного проще и быстрее. Враппер для gradle обычно идёт вместе с проектом и прекрасно работает в linux/macos/windows, разве что в последнем надо вызывать bat-файлик вместо враппера.
Вызов задач
./gradlew tasks
пишет доступные задачи.
./gradlew subprojectName:tasks --all
Можно вывести задачи отдельного подпроекта, а ещё с опцией --all
будут выведены все задачи, включая второстепенные.
Можно вызвать любую задачу, при этом будут вызваны все задачи, от которых она зависит.
./gradlew app:assembleDevelopDebug
Если лень писать название целиком, можно выкинуть маленькие буковки:
./gradlew app:assembleDD
Если градл не сможет однозначно угадать, какую именно задачу имели ввиду, то выведет список подходящих вариантов.
Логгинг
Количество выводимой в консоль информации при запуске задачи сильно зависит от уровня логгинга.
Кроме дефолтного есть -q, -w, -i, -d
, ну или --quiet, --warn, --info, --debug
по возрастанию количества информации. На сложных проектах вывод с -d может занимать больше мегабайта, а поэтому его лучше сразу сохранять в файл и там уже смотреть поиском по ключевым словам:
./gradlew app:build -d > myLog.txt
Если где-то кидается исключение, для stacktrace опция -s
.
Можно и самому писать в лог:
logger.warn('A warning log message.')
логгер является имплементацией SLF4J.
Groovy
Происходящее в build.gradle
файликах — просто код на groovy.
Groovy как язык программирования почему-то не очень популярен, хотя, как мне кажется, он сам по себе достоин хотя бы небольшого изучения. Язык появился на свет ещё в 2003 году и потихоньку развивался. Интересные особенности:
- Практически любой java код является валидным кодом на groovy. Это очень помогает интуитивно писать работающий код.
- Одновременно вместе со статической, в груви поддерживается динамическая типизация, вместо
String a = "a"
можно смело писатьdef a = "a"
или дажеdef map = ['one':1, 'two':2, 'list' = [1,false]]
- Есть замыкания, для которых можно динамически определить контекст исполнения. Те самые блоки
android {...}
принимают замыкания и потом исполняют их для какого-то объекта. - Есть интерполяция строк
"$a, ${b}"
, multiline-строки"""yep, ${c}"""
, а обычные java-строки обрамляются одинарными кавычками:'text'
- Есть подобие extension-методов. В стандартной коллекции языка уже есть методы типа any, every, each, findAll. Лично мне названия методов кажутся непривычными, но главное что они есть.
- Вкусный синтаксический сахар, код становится намного короче и проще. Можно не писать скобки вокруг аргументов функции, для объявления списков и хеш-табличек приятный синтаксис:
[a,b,c], [key1: value1, key2: value2]
В общем, почему языки типа Python/Javascript взлетели, а Groovy нет — для меня загадка. Для своего времени, когда в java даже лямбд не было, а альтернативы типа kotlin/scala только-только появлялись или ещё не существовали, Groovy должен был выглядеть реально интересным языком.
Именно гибкость синтаксиса groovy и динамическая типизация позволила в gradle создавать лаконичные DSL.
Сейчас в официальной документации Gradle примеры продублированы на Kotlin, и вроде как планируется переходить на него, но код уже не выглядит таким простым и становится больше похожим на обычный код:
task hello {
doLast {
println "hello"
}
}
vs
tasks.register("hello") {
doLast {
println("hello")
}
}
Впрочем, переименование в Kradle пока не планируется.
Стадии сборки
Их делят на инициализацию, конфигурацию и выполнение.
Идея состоит в том, что gradle собирает ациклический граф зависимостей и вызывает только необходимый минимум их них. Если я правильно понял, стадия инициализации происходит в тот момент, когда исполняется код из build.gradle.
Например, такой:
copy {
from source
to dest
}
Или такой:
task epicFail {
copy{
from source
to dest
}
}
Возможно, это неочевидно, но вышеуказанное будет тормозить инициализацию. Чтобы не заниматься копированием файлов при каждой инициализации, нужно в задаче использоваль блок doLast{...}
или doFirst{...}
— тогда код завернётся в замыкание и его позовут в момент выполнения задачи.
task properCopy {
doLast {
copy {
from dest
to source
}
}
}
или так
task properCopy(type: Copy) {
from dest
to source
}
В старых примерах вместо doLast
можно встретить оператор <<
, но от него потом отказались из-за неочевидности поведения.
task properCopy << {
println("files copied")
}
tasks.all
Что забавно, с помощью doLast
и doFirst
можно навешивать какие-то действия на любые задачи:
tasks.all {
doFirst {
println("task $name started")
}
}
IDE подсказывает, что у tasks
есть метод whenTaskAdded(Closure ...)
, но метод all(Closure ...)
работает намного интереснее — замыкание вызывается для всех существующих задач, а так же на новых задачах при их добавлении.
Создадим задачу, которая распечатает зависимости всех задач:
task printDependencies {
doLast {
tasks.all {
println("$name dependsOn $dependsOn")
}
}
}
или так:
task printDependencies {
doLast {
tasks.all { Task task ->
println("${task.name} dependsOn ${task.dependsOn}")
}
}
}
Если tasks.all{}
вызвать во время выполнения (в блоке doLast
), то мы увидим все задачи и зависимости.
Если сделать то же самое без doLast
(т.е., во время инициализации), то у распечатанных задач может не хватать зависимостей, так как они ещё не были добавлены.
Ах да, зависимости! Если другая задача должна зависеть от результатов выполнения нашей, то стоит добавить зависимость:
anotherTask.dependsOn properCopy
Или даже так:
tasks.all{ task ->
if (task.name.toLowerCase().contains("debug")) {
task.dependsOn properCopy
}
}
inputs, outputs и инкрементальная сборка
Обычная задача будет вызываться каждый раз. Если указать, что задача на основе файла А генерирует файл Б, то gradle будет пропускать задачу, если эти файлы не изменились. Причём gradle проверяет не дату изменения файла, а именно его содержимое.
task generateCode(type: Exec) {
commandLine "generateCode.sh", "input.txt", "output.java"
inputs.file "input.txt"
output.file "output.java"
}
Аналогично можно указать папки, а так же какие-то значения: inputs.property(name, value)
.
task description
При вызове ./gradlew tasks --all
стандартные задачи имеют красивое описание и как-то сгруппированы. Для своих задач это добавляется очень просто:
task hello {
group "MyCustomGroup"
description "Prints 'hello'"
doLast{
print 'hello'
}
}
task.enabled
можно «выключить» задачу — тогда её зависимости будут всё равно вызваны, а она сама — нет.
taskName.enabled false
несколько проектов (модулей)
multi-project builds в документации
В основном проекте можно расположить ещё несколько модулей. Например, такое используется в андроид проектах — в рутовом проекте почти ничего нет, в подпроекте включается android плагин. Если захочется добавить новый модуль — можно добавить ещё один, и там, например, тоже подключить android плагин, но использовать другие настройки для него.
Ещё пример: при публикации проекта с помощью jitpack в рутовом проекте описывается, с какими настройками публиковать дочерний модуль, который про факт публикации может даже не подозревать.
Дочерние модули указываются в settings.gradle:
include 'name'
Подробнее про зависимости между проектами можно почитать здесь
buildSrc
Если кода в build.gradle
много или он дублируется, его можно вынести в отдельный модуль. Нужна папка с магическим именем buildSrc
, в которой можно расположить код на groovy или java. (ну, вернее, в buildSrc/src/main/java/com/smth/
код, тесты можно добавить в buildSrc/src/test
). Если хочется что-то ещё, например, написать свою задачу на scala или использовать какие-то зависимости, то прямо в buildSrc
надо создать build.gradle
и в нём указать нужные зависимости/включить плагины.
К сожалению, с проектом в buildSrc
IDE может тупить c подсказками, там придётся писать импорты и классы/задачи оттуда в обычный build.gradle
тоже придётся импортировать. Написать import com.smth.Taskname
— не сложно, просто надо это помнить и не ломать голову, почему задача из buildSrc
не найдена).
По этой причине удобно сначала написать что-то работающее прямо в build.gradle
, и только потом переносить код в buildSrc
.
Свой тип задачи
Задача наследуется от DefaultTask
, в которой есть много-много полей, методов и прочего. Код AbstractTask, от которой унаследована DefaultTask.
Полезные моменты:
- вместо ручного добавления
inputs
иoutputs
можно использовать поля и аннотации к ним:@Input, @OutputFile
и т.п. - метод, который будут запускать при выполнении задачи:
@TaskAction
. - удобные методы типа
copy{from ... , into... }
всё ещё можно вызвать, но придётся их явно вызывать для проекта:project.copy{...}
Когда для нашей задачи кто-то в build.gradle
пишет
taskName {
... //some code
}
у задачи вызывается метод configure(Closure)
.
Я не уверен, что это правильных подход, но если у задачи есть несколько полей, взаимное состояние которых сложно контролировать геттерами-сеттерами, то кажется вполне удобным переопределить метод следующим образом:
override def configure(Closure closure){
def result = super().configure(closure)
// здесь проверить состояние полей/установить что-нибудь
return result;
}
Причём даже если написать
taskName.fieldName value
то метод configure
всё равно будет вызван.
Свой плагин
Подобно задаче, можно написать свой плагин, который будет что-то настраивать или создавать задачи. Например, происходящее в android{...}
— полностью заслуга тёмной магии андроид плагина, который вдобавок создаёт целую кучу задач типа app: assembleDevelopDebug на все возможные сочетания flavor/build type/dimenstion. Ничего сложного в написании своего плагина нет, для лучшего понимания можно посмотреть код других плагинов.
Есть ещё третья ступенька — можно код расположить не в buildSrc
, а сделать его отдельным проектом. Потом с помощью https://jitpack.io или ещё чего-то опубликовать плагин и подключать его аналогично остальным.
The end
В примерах выше могут быть опечатки и неточности. Пишите в личку или отмечайте с ctrl+enter
— исправлю. Конкретные примеры лучше брать из документации, а на эту статью смотреть как на списочек того «как можно делать».