Шпаргалка по 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 — исправлю. Конкретные примеры лучше брать из документации, а на эту статью смотреть как на списочек того "как можно делать".

    Похожие публикации

    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 5 953 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 3

      0
      Ну вот, а я так надеялся, что наконец то разберусь, как работает эта чертова распределяющая шляпа gradle…
        0
        Да уж быстрее бы котлин перейти, груви какое-то отторжение вызывает
          0

          Теоретически, статическая типизация котлина должна давать преимущества, но я попробовал перевести код на котлин и мне совсем не понравилось: код стал более нагруженным, динамическая суть происходящего лезет отовсюду и появляются разные костыли для её обхода. Надеюсь, в будущем доведут до ума.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое