Pull to refresh

Подробно о задачах Gradle

Reading time24 min
Views121K


Перевод второй главы свободно распространяемой книги Building and Testing with Gradle

Задача (task) является основным компонентом процесса сборки в файле билда Gradle. Задачи представляют собой именованные наборы инструкций билда, которые Gradle запускает выполняя сборку приложения. При сравнении с другими билд-системами, задачи могут показаться знакомой абстракцией. Однако Gradle предоставляет более развитую модель, в отличие от той, которая вам уже может быть знакома. По сравнению с традиционными возможностями объявления операций билда, связанных зависимостями, задачи Gradle являются полнофункциональными объектами, которыми вы при желании можете управлять программно.

Рассмотрим какими способами можно определить задачу, два ключевых подхода к определению задач и программный интерфейс, который мы можем использовать для гибкой настройки.

Объявление задач


Существует простой способ создания задачи. Всё что нужно — указать имя задачи:

Пример 1. Объявление задачи по одному только имени

task hello

Выполнив команду gradle hello, получаем результат:

Пример 2. Отчёт Gradle о новой задаче

------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

Build Setup tasks
-----------------
init - Initializes a new Gradle build. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]

Help tasks
----------
dependencies - Displays all dependencies declared in root project '__project'.
dependencyInsight - Displays the insight into a specific dependency in root project '__project'.
help - Displays a help message
projects - Displays the sub-projects of root project '__project'.
properties - Displays the properties of root project '__project'.
tasks - Displays the tasks runnable from root project '__project'.

Other tasks
-----------
hello


Операция задачи (Task Action)


Выполнение нашей задачи по команде gradle hello всё же не произведёт никакого результата, поскольку мы не присвоили ей ни одной операции (action). Операцию можно присвоить используя оператор сдвиг влево:

Пример 3. Добавление простейшей операции

task hello << {
    println 'hello, world'
}

Операторы, такие как << («сдвиг влево» из Java), могут быть перегружены в Groovy для изменения поведения в зависимости от объектов с которыми они работают. В данном случае << перегружен в Gradle для добавления блока кода в список операций, которые выполняет задача. Сдвиг влево является эквивалентом метода doLast(), который мы рассмотрим ниже.

Теперь у нас есть гибкая возможность добавления кода операции аддитивным способом, ссылаясь на объект задачи, который мы создали:

Пример 4. Последовательное добавление операций задачи по одной

task hello

hello << {
    print 'hello, '
}

hello << {
    println 'world'
}

Теперь мы снова получили знакомый нам результат выполнения билда:

Пример 5. Результат выполнения билда с операциями, добавленными по одной

d:\project>gradle hello
:hello
hello, world

BUILD SUCCESSFUL

Total time: 1.916 secs

Данное поведение тривиально, однако оно раскрывает важный принцип: задачи не являются статическими неизменяемыми объявлениями операций билда; задачи — полнофункциональные объекты программной среды Gradle. Кроме добавления в них операций аддитивным способом в произвольных местах файла билда, у нас есть ещё и другие возможности. Посмотрим какие.

Конфигурация задачи


Новые пользователи Gradle обычно путаются в синтаксисе конфигурации когда пытаются определить операции задач. Продолжая предыдущий пример мы можем расширить его, добавив блок конфигурации:

Пример 6. Комбинирование конфигурации и операции задачи

task initializeDatabase

initializeDatabase << {
    println 'connect to database'
}

initializeDatabase << {
    println 'update database schema'
}

initializeDatabase {
    println 'configuring database connection'
}

Запустив такой файл билда, мы получим результат, который покажется нам нелогичным:

Пример 7. Результат выполнения файла билда, созданного выше

d:\project>gradle initializeDatabase
configuring database connection
:initializeDatabase
connect to database
update database schema

BUILD SUCCESSFUL

Total time: 3.088 secs

Для обозначения блока кода между парой фигурных скобок, в Groovy используется термин «замкнутое выражение» или «замыкание» (closure). Функции-замыкания подобны объектам, которые можно передавать методу как параметр или присваивать переменной, с возможностью последующего выполнения. Они будут повсеместно встречаться вам в Gradle, поскольку в высшей степени подходят в роли блоков, где можно определить конфигурационный код и код операций билда.

Последнее замкнутое выражение выглядит как очередной блок операции билда, и мы ожидаем что вывод его сообщения будет последним, но не первым. Оказывается замыкание, добавленное к имени задачи без оператора сдвиг влево совсем не добавляет новую операцию. Вместо этого добавился блок конфигурации. Конфигурационный блок задачи выполняется во время конфигурационной фазы жизненного цикла Gradle, которая предшествует фазе выполнения, во время которой выполняются операции задачи.

Каждый раз, когда Gradle запускает билд, процесс проходит через три фазы жизненного цикла: инициализация, конфигурация и выполнение. Выполнение — фаза, во время которой задачи билда выполняются в порядке, указанном в настройках их зависимостей. Конфигурация — фаза в которой объекты задачи собираются во внутреннюю объектную модель, обычно называемую направленным ациклическим графом. Инициализация — фаза, в которой Gradle принимает решение, какие объекты будут принимать участие в билде. Последняя фаза важна в многопроектных билдах.

Прим. пер.
Из глоссария: Gradle DAG — Directed Acyclic Graph (направленный ациклический граф) — направленный, не содержащий циклов, граф. Вершины графа представлены задачами Gradle, которые будут выполняться. Метод dependsOn, устанавливающий зависимость данной задачи от другой, добавляет другую задачу, как новую вершину (если она ещё не присутствует в графе) и создаёт направленное ребро между двумя вершинами. Любая связь, созданная с помощью dependsOn, проверяется на наличие циклов. Не должно быть такого пути, при котором выход из определённой вершины, пройдя последовательность рёбер графа, приведёт к первоначальной вершине.

Замыкания конфигурации аддитивны точно так же, как и замыкания операций. Поэтому мы можем написать код файла билда для предыдущего примера способом приведенным ниже, при этом результат выполнения будет тот же, что и раньше:

Пример 8. Добавление конфигурационных блоков

task initializeDatabase

initializeDatabase << {
    println 'connect to database'
}

initializeDatabase << {
    println 'update database schema'
}

initializeDatabase {
    print 'configuring '
}

initializeDatabase {
    println 'database connection'
}


Конфигурационный блок — подходящее место для присвоения значений переменных и структур данных, которые используются операцией задачи в дальнейшем, когда она запустится в билде (если только запустится). Структура конфигурации даёт вам возможность превратить задачи вашего билда в сущности развитой объектной модели, заполненные информацией о билде. В этом и состоит отличие задач Gradle от простого набора операций билда, которые выполняются в определённой последовательности. Без такого отличия между конфигурацией и операцией, пришлось бы усложнять настройки зависимостей, что привело бы к потере надёжности и к снижению выразительности средств для связывания основных структур данных билда.

При запуске Gradle-файла, конфигурационный код билда выполняется полностью, независимо от того, будет ли какая-либо из задач запускаться в фазе выполнения.

Задачи являются объектами


В этом месте у вас уже могла появиться догадка о том, что Gradle, прежде чем выполнить билд, создаёт его внутреннюю объектную модель. Именно так всё и происходит. Каждая задача, которую вы объявляете, в действительности, становится объектом-задачей в пределах всего проекта билда. У объекта-задачи, как и у любого другого объекта, есть свойства и методы. И ещё мы можем управлять типом каждого объекта-задачи, обращаясь к функциональности, определённой в нём. Несколько примеров помогут нам разобраться.

По умолчанию, каждой новой задаче присваивается тип DefaultTask. Подобно тому, как каждый класс наследуется от java.lang.Object в Java, в Gradle каждая задача наследуется от данного типа — даже те задачи, которые расширяют возможности DefaultTask путём создания нового типа. На самом деле, DefaultTask-задачи не делают ничего специфичного, вроде компиляции кода или копирования файлов. Однако они содержат функционал, который требуется для взаимодействия с программной моделью проекта Gradle. Рассмотрим методы и свойства, которые имеет каждая задача в Gradle.

Методы DefaultTask


dependsOn(task)

Для вызывающей задачи добавляет задачу-зависимость. Задача-зависимость всегда запускается перед задачей, которая от неё зависит. Метод можно вызывать несколькими способами. Пример кода ниже показывает, как мы можем определить зависимость задачи loadTestData от createSchema:

Пример 9. Различные способы вызова метода dependsOn

task createSchema

// Объявляем зависимость 'loadTestData' от 'createSchema'
// Остальные зависимости, определённые ранее, остаются неизменными
task loadTestData {
    dependsOn createSchema
}

// Альтернативный способ указания той же зависимости
task loadTestData {
    dependsOn << createSchema
}

// Делаем то же самое, используя одиночные кавычки (которые обычно не нужны)
task loadTestData {
    dependsOn 'createSchema'
}

// Явный вызов метода объекта-задачи
task loadTestData
loadTestData.dependsOn createSchema

// Краткая нотация для определения зависимостей
task loadTestData(dependsOn: createSchema)

Задача может зависеть от нескольких задач. Если задача loadTestData зависит от задач createSchema и compileTestClasses, мы пишем код следующим образом:

Пример 10. Различные способы вызова метода dependsOn для множественных зависимостей

task compileTestClasses
task createSchema

// Объявление зависимостей по одной
task loadTestData {
    dependsOn << compileTestClasses
    dependsOn << createSchema
}

// Передаём зависимости, как список переменной длины
task loadTestData {
    dependsOn compileTestClasses, createSchema
}

// Явно вызываем метод объекта-задачи
task loadTestData
loadTestData.dependsOn compileTestClasses, createSchema

// Краткая нотация для определения зависимостей
// Обратите внимание на синтаксис списков Groovy
task loadTestData(dependsOn: [ compileTestClasses, createSchema ])

doFirst(closure)

Добавляет блок исполняемого кода в начало операции задачи. Во время фазы выполнения запускается блок операции каждой задачи, участвующей в билде. Метод doFirst позволяет вам добавлять части логики в начало существующей операции, даже если эта операция уже определена в файле билда или внешнем модуле (plug-in), к которому у вас нет доступа. Многократные вызовы doFirst добавляют новые блоки с кодом операций в начало последовательности выполнения задачи.

Метод doFirst можно вызывать напрямую для объекта-задачи, передавая ему замыкание, которое содержит код, который будет выполнен перед текущей операцией задачи.

Как мы уже говорили, замыкание — это блок Groovy кода, заключённый между парой фигурных скобок. Замыкание можно передавать методу как любой другой объект. Возможность пердавать методам замкнутые выражения является стилевой особенностью Groovy.

Пример 11. Вызов метода doFirst

task setupDatabaseTests << {
    // Здесь определена текущая операция задачи
    println 'load test data'
}

setupDatabaseTests.doFirst {
    println 'create schema'
}

Пример 12. Результат выполнения прошлого файла билда

d:\project>gradle setupDatabaseTests
:setupDatabaseTests
create schema
load test data

BUILD SUCCESSFUL

Total time: 1.935 secs

doFirst можно также вызывать в конфигурационном блоке задачи. Как мы уже говорили, конфигурационный блок — это часть исполняемого кода, которая запускается во время конфигурационной фазы билда, перед тем как будут выполнены операции задачи. Когда мы рассматривали выше конфигурацию задач, у вас мог возникнуть вопрос: где можно использовать конфигурационные блоки? Следующий пример покажет вам, как можно вызывать методы задачи внутри конфигурационного блока, что в перспективе делает очень выразительным синтаксис формата изменения поведения задачи:

Пример 13. Вызов метода doFirst внутри конфигурационного блока задачи

task setupDatabaseTests << {
    println 'load test data'
}

setupDatabaseTests {
    doFirst {
        println 'create schema'
    }
}

Повторные вызовы doFirst аддитивны. Код операции каждого предыдущего вызова сохраняется, и новое замыкание добавляется в начало списка, готовое выполниться в соответствующем порядке. Например, если нам нужно настроить базу данных для интеграционного тестирования (разбив этапы настройки по частям), можно использовать следующий код:

Пример 14. Повторные вызовы doFirst обладают свойством аддитивности

task setupDatabaseTests << {
    println 'load test data'
}

setupDatabaseTests.doFirst {
    println 'create database schema'
}

setupDatabaseTests.doFirst {
    println 'drop database schema'
}

Пример 15. Результат выполнения предыдущего примера

d:\project>gradle setupDatabaseTests
:setupDatabaseTests
drop database schema
create database schema
load test data

BUILD SUCCESSFUL

Total time: 3.126 secs

В предыдущем примере разделение инициализации на три отдельных замыкания с вызовом doFirst() было, конечно же, несколько искусственным шагом. Однако, бывают случаи, когда исходный код задачи недоступен. Например, задача определена в другом файле билда, модифицировать который невозможно или непрактично. Рассматриваемый способ программной модификации недоступной логики открывает нам широкие возможности.

До сих пор в наших примерах использовался очень простой синтаксис, который раскрывает принципы работы Gradle за счёт многократных добавлений замыканий. Вероятнее всего, в реальном билде мы организуем задачу следующим образом (всё так же, вместо настоящих тестовых операций мы используем операторы println):

Пример 16. Повторные вызовы doFirst после рефакторинга

// Исходное определение задачи (может быть недоступно для редактирования)
task setupDatabaseTests << {
    println 'load test data'
}

// Наши изменения задачи (в файле, где мы можем вносить изменения)
setupDatabaseTests {
    doFirst {
        println 'create database schema'
    }
    doFirst {
        println 'drop database schema'
    }
}

Обратите внимание на то что мы собрали вместе несколько вызовов doFirst внутри одного конфигурационного блока, после того как начальная операция уже была добавлена в задачу setupDatabaseTests.

doLast(closure)

Метод doLast очень похож на метод doFirst, с той лишь разницей, что он добавляет поведение в конец операции, а не в начало. Если вам нужно запустить блок кода после того как некоторая задача закончит выполнение, вы можете поступить следующим образом:

Пример 17. Использование метода doLast

task setupDatabaseTests << {
    println 'create database schema'
}

setupDatabaseTests.doLast {
    println 'load test data'
}

Пример 18. Повторные вызовы doLast аддитивны

task setupDatabaseTests << {
    println 'create database schema'
}

setupDatabaseTests.doLast {
    println 'load test data'
}

setupDatabaseTests.doLast {
    println 'update version table'
}

Как уже говорилось ранее, в параграфе Операция задачи, оператор << является ещё одним способом вызова метода doLast().

onlyIf(closure)

Метод onlyIf представляет собой предикат, который определяет, будет ли выполнена задача. Значением предиката считается значение, возвращаемое замыканием. При помощи данного метода вы можете деактивировать выполнение задачи, которая иначе запустится в обычном порядке запуска последовательности зависимостей билда.

В Groovy последнее выражение внутри замыкания определяет его возвращаемое значение, даже если отсутствует оператор return. Метод Groovy, в котором определено только одно выражение, является функцией, возвращающей значение этого выражения.

Пример 19. Билд-файл, в котором используется метод onlyIf

task createSchema << {
    println 'create database schema'
}

task loadTestData(dependsOn: createSchema) << {
    println 'load test data'
}

loadTestData.onlyIf {
    System.properties['load.data'] == 'true'
}

Пример 20. Два варианта запуска файла билда. Обратите внимание на разницу в результатах

d:\project>gradle loadTestData
:createSchema
create database schema
:loadTestData SKIPPED

BUILD SUCCESSFUL

Total time: 4.361 secs
d:\project>gradle -Dload.data=true loadTestData
:createSchema
create database schema
:loadTestData
load test data

BUILD SUCCESSFUL

Total time: 2.005 secs

При помощи метода onlyIf вы можете включать и отключать отдельные задачи, используя логику, выражаемую Groovy-кодом, что не ограничиваться одной лишь проверкой простого свойства System, которое мы использовали в примере. У вас есть возможности открывать файлы для чтения, вызывать Веб-сервисы, проверять логины-пароли и делать многое другое, что можно делать в коде.

Свойства DefaultTask


didWork

Свойство типа boolean, указывающее, завершилась ли задача успешно. Не все задачи устанавливают значение didWork к моменту завершения. Однако некоторые задачи, такие как Compile, Copy и Delete, устанавливают значение данного свойства для передачи информации о том что их операции выполнены либо успешно, либо с ошибками. Вычисление значения, указывающего на то, что задача уже выполнилась, специфично для разных задач. Вы можете установить значение didWork в вашей задаче для отражения результатов выполнения созданного вами кода сборки:

Пример 21. Отправка электронного письма для случая, когда компиляция прошла успешно

apply plugin: 'java'

task emailMe(dependsOn: compileJava) << {
    if (tasks.compileJava.didWork) {
        println 'SEND E-MAIL ANNOUNCING SUCCESS'
    }
}

Пример 22. Результаты выполнения билда с испльзованием didWork

d:\project>gradle emailMe
:compileJava
:emailMe
SEND E-MAIL ANNOUNCING SUCCESS

BUILD SUCCESSFUL

Total time: 4.232 secs

enabled

Свойство типа boolean, указывающее на то, будет ли выполняться задача. Вы можете отключить выполнение задачи, установив свойству enabled значение false. Зависимости задачи выполнятся в том же порядке, как если бы задача не была отключена.

Пример 23. Отключение задачи

task templates << {
    println 'process email templates'
}

task sendEmails(dependsOn: templates) << {
    println 'send emails'
}

sendEmails.enabled = false

Пример 24. Билд с отключенной задачей. Обратите внимание, зависимость всё так же запускается

d:\project>gradle -b enabled.gradle sendEmails
:templates
process email templates
:sendEmails SKIPPED

BUILD SUCCESSFUL

Total time: 3.271 secs

Параметр командной строки -b указывает Gradle на отличный от файла по умолчанию файл билда. По умолчанию Gradle ищет файл с называнием build.gradle, но данный параметр командной строки позволяет нам указать другой файл билда.

path

Свойство строчного типа, содержащее полный путь задачи. По умолчанию, путём задачи является имя задачи с символом двоеточие впереди.

Пример 25. Одноуровневый файл билда, который отображает путь единственной задачи определённой в нём

task echoMyPath << {
    println "THIS TASK'S PATH IS ${path}"
}

Пример 26. Результаты выполнения предыдущего файла билда

d:\project>gradle echoMyPath
:echoMyPath
THIS TASK'S PATH IS :echoMyPath

BUILD SUCCESSFUL

Total time: 3.135 secs

Двоеточие впереди указывает на то, что задача определена на верхнем уровне файла билда. Расположение задач на верхнем уровне, однако, не является обязательным. Gradle поддерживает зависимые подпроекты, или вложенные билды. Если задача определёна во вложенном билде с названием subProject, путь будет :subProject:echoMyPath.

logger

Ссылка на внутренний объект Gradle logger. В Gradle logger реализует интерфейс org.slf4j.Logger с несколькими дополнительными уровнями логирования. Ниже описаны уровни логирования, поддерживаемые объектом logger. Установка уровню логирования одного из значений ниже включает логирование на всех последующих уровнях, кроме WARN и QUIET:
  • DEBUG. Для подробных сообщений логирования, которые нужны разработчику билда, однако не должны выводиться в момент выполнения билда в нормальном режиме. Если выбран данный уровень, Gradle автоматически использует расширенный формат, который в каждом сообщении вставляет метку времени, уровень логирования, и имя задачи, производящей логирование. Остальные уровни используют более краткий формат сообщений.
  • INFO. Нужен для менее информативных сообщений билда, играющих второстепенную роль во время выполнения билда.
  • LIFECYCLE. Малоинформативные сообщения об изменениях в жизненном цикле билда и процессе выполнениия самого инструмента, запустившего сборку проекта. Обычно генерируются самим Gradle. Данный уровень используется по умолчанию, когда Gradle запускается без опции командной строки -q. Данный уровень логирования назначается сообщениям, выводимым оператором println.
  • WARN. Малоинформативные, но важные сообщения, информирующие о потенциальных проблемах билда. Когда уровень логирования установлен в WARN, сообщения уровня QUIET не выводятся.
  • QUIET. Сообщения, которые выводятся даже если вывод сообщений был отключен параметром командной строки -q. (Выполнение билда с параметром -q делает QUIET уровнем логирования по умолчанию). Данный уровень логирования назначается сообщениям, выводимым оператором System.out.println. Когда уровень логирования установлен в QUIET, сообщения уровня WARN не выводятся.
  • ERROR. Редкие, но важные сообщения, кототые выводятся на всех уровнях логирования. Сообщения информируют о завершении билда с ошибками. Если ERROR — текущий уровень логирования, вызовы System.out.println не будут выводиться в консольном окне.

Пример 27. Задача демонстрирует эффект применения всех уровней логирования. Несколько более сложный код Groovy устанавливает уровень логирования для каждой из возможных опций, определённых в списке. Таким образом, каждый раз осуществляется вывод сообщений на каждом из уровней логирования

task logLevel << {

    def levels = ['DEBUG', 'INFO', 'LIFECYCLE', 'QUIET', 'WARN', 'ERROR']

    levels.each { level ->
        logging.level = level
        def logMessage = "SETTING LogLevel=${level}"

        logger.error logMessage
        logger.error '-' * logMessage.size()
        logger.debug 'DEBUG ENABLED'
        logger.info  'INFO ENABLED'
        logger.lifecycle 'LIFECYCLE ENABLED'
        logger.warn  'WARN ENABLED'
        logger.quiet 'QUIET ENABLED'
        logger.error 'ERROR ENABLED'
        println 'THIS IS println OUTPUT'
        logger.error ' '
    }
}

Пример 28. Результат выполнения прошлого файла билда

d:\project>gradle logLevel
19:35:44.677 [LIFECYCLE] [class org.gradle.TaskExecutionLogger]
19:35:44.699 [ERROR] [org.gradle.api.Task] SETTING LogLevel=DEBU
19:35:44.732 [ERROR] [org.gradle.api.Task] ---------------------
19:35:44.747 [DEBUG] [org.gradle.api.Task] DEBUG ENABLED
19:35:44.760 [INFO] [org.gradle.api.Task] INFO ENABLED
19:35:44.775 [LIFECYCLE] [org.gradle.api.Task] LIFECYCLE ENABLED
19:35:44.788 [WARN] [org.gradle.api.Task] WARN ENABLED
19:35:44.801 [QUIET] [org.gradle.api.Task] QUIET ENABLED
19:35:44.812 [ERROR] [org.gradle.api.Task] ERROR ENABLED
19:35:44.857 [QUIET] [system.out] THIS IS println OUTPUT
19:35:44.868 [ERROR] [org.gradle.api.Task]
SETTING LogLevel=INFO
---------------------
INFO ENABLED
LIFECYCLE ENABLED
WARN ENABLED
QUIET ENABLED
ERROR ENABLED
THIS IS println OUTPUT

SETTING LogLevel=LIFECYCLE
--------------------------
LIFECYCLE ENABLED
WARN ENABLED
QUIET ENABLED
ERROR ENABLED
THIS IS println OUTPUT

SETTING LogLevel=QUIET
----------------------
QUIET ENABLED
ERROR ENABLED
THIS IS println OUTPUT

SETTING LogLevel=WARN
---------------------
WARN ENABLED
QUIET ENABLED
ERROR ENABLED
THIS IS println OUTPUT

SETTING LogLevel=ERROR
----------------------
ERROR ENABLED


BUILD SUCCESSFUL

Total time: 2.184 secs


logging

Свойство logging даёт нам возможность управлять уровнем логирования. Как уже было показано в примере для свойства logger, уровень логирования билда можно получать и изменять, используя свойство logging.level.

description

Свойство description, как видно из названия, описывает назначение задачи небольшим количеством метаданных, доступных для понимания человека. Значение description можно указать несколькими способами:

Пример 29. Одновременно задаём описание задачи и поведение

task helloWorld(description: 'Says hello to the world') << {
    println 'hello, world'
}

Пример 30. Два способа объявления поведения задачи и задания описания

task helloWorld << {
    println 'hello, world'
}

helloWorld {
    description 'Says hello to the world'
}

// Ещё один способ
helloWorld.description 'Says hello to the world'


temporaryDir

Свойство temporaryDir возвращает объект File, который ссылается на временную директорию текущего файла билда. Такая директория создаётся для временного хранения промежуточных результатов работы задачи, или для организации файлов, которые задача будет обрабатывать.

Динамические свойства


Как мы уже видели, задачи содержат набор внутренних свойств, играющих важную роль для пользователей Gradle. В добавок к этому, мы можем также определить в задаче новые свойства. Объект-задача работает как хеш-таблица, которая может хранить любые имена свойств и значения, которые мы присваиваем свойствам (при условии, что наши имена свойств не конфликтуют с именами встроенных свойств).

Рассмотрим пример: задача createArtifact зависит от задачи copyFiles. Цель copyFiles — собрать файлы из нескольких источников и скопировать их во временную директорию, которую createArtifact в дальнейшем преобразует в артифакт установки. Список файлов звисит от параметров билда, но для соответствия специфическим требованиям установленного приложения, в артифакте должен храниться манифест, перечисляющий файлы. Здесь очень удобно использовать динамическое свойство:

Пример 31. Билд-файл, в котором показан пример динамического свойства

task copyFiles {
    // Где угодно находим файлы, копируем их
    // (здесь для наглядности используем фиксированный список)
    fileManifest = [ 'data.csv', 'config.json' ]
}

task createArtifact(dependsOn: copyFiles) << {
    println "FILES IN MANIFEST: ${copyFiles.fileManifest}"
}

Пример 32. Результат выполнения файла билда в прошлом примере

d:\project>gradle -q createArtifact
FILES IN MANIFEST: [data.csv, config.json]

Типы задач


Как мы уже говорили ранее, в параграфе Задачи являются объектами, каждая задача имеет тип. Кроме типа DefaultTask, есть ещё другие типы задач для копирования, архивирования, запуска программ и других действий. Объявление типа задачи во многом похоже на наследование от базового класса в объектно-ориентированном языке. Таким образом, реализовав наследование, вы тут же получаете определённые свойства и методы в вашей задаче. Подобный синтаксис значительно укорачивает определение задач, при том что возможности по-прежнему остаются большими.

Рассмотрение полной документации по задачам находится за рамками излагаемого здесь материала, но всё же мы рассмотрим несколько важных типов с примерами использования.

Copy


Задача Copy копирует файлы из одного места в другое. В простейшем случае — копирует файлы из одной директории в другую, с некоторыми дополнительными ограничениями по включению или исключению файлов, используя маски имён:

Пример 33. Простейший пример использования задачи Copy

task copyFiles(type: Copy) {
    from 'resources'
    into 'target'
    include '**/*.xml', '**/*.txt', '**/*.properties'
}

Jar


Задача Jar создаёт Jar-файл из файлов ресурсов. Задача данного типа c известным названием Jar определена в модуле 'java'. Задача упаковывает *.class-файлы и ресурсы в Jar-файл с названием проекта, при этом использует обычный манифест. Результат сохраняется в директорию build/libs. Данная задача в высокой степени обладает гибкостью.

Пример 34. Простейший пример использования задачи Jar

apply plugin: 'java'

task customJar(type: Jar) {
    manifest {
        attributes firstKey: 'firstValue', secondKey: 'secondValue'
    }
    archiveName = 'hello.jar'
    destinationDir = file("${buildDir}/jars")
    from sourceSets.main.output
}

Обратите внимание — имя архива и целевая папка легко конфигурируются. Таким же образом можно менять значения файла манифеста, используя простой синтаксис словарей Groovy. Содержимое JAR-файла определяется строкой from sourceSets.main.output, которая включает .class-файлы. Метод from идентичен методу, который используется в примере CopyTask, что обнаруживает одну интересную деталь: задача Jar наследуется от задачи Copy. Зная эту особенность, вы можете ещё не заглянув в документацию сделать некоторые выводы о широких возможностях и порядке структуры классов, лежащей в основе задачи Jar.

destinationDir присваивается очень простое выражение. Было бы естественнее, если бы свойству destinationDir присваивалась строка. Но свойство работает с объектами java.io.File. На помощь приходит метод file(), который всегда доступен в коде билд файла Gradle. Он конвертирует строку в объект File.

Помните, вы всегда можете найти документацию docs/dsl/index.html, где описаны стандартные возможности Gradle, такие как задача Jar. Описание всех возможностей задачи Jar лежит за рамками нашей главы.

JavaExec


Задача JavaExec запускает Java-класс c методом main(). Запуск консольного Java-приложения может быть сопряжён с неудобствами. Однако данная задача избавляет от неудобств, интегрируя консольные Java-приложения в ваш билд:

Пример 35. Задача Gradle запускает консольное Java-приложение (пример взят из javaexec-task)

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile 'commons-codec:commons-codec:1.6'
    runtime 'commons-codec:commons-codec:1.6'
}

task encode(type: JavaExec, dependsOn: classes) {
    main = 'org.gradle.example.commandline.MetaphoneEncoder'
    args = "The rain in Spain falls mainly in the plain".split().toList()
    classpath sourceSets.main.output.classesDir
    classpath configurations.runtime
}

package org.gradle.example.commandline;

import org.apache.commons.codec.language.Metaphone;

public class MetaphoneEncoder {
    public static void main(String args[]) {
        Metaphone codec = new Metaphone();
        String phrase = "";
        String encoded = "";
        for (String s : args) {
            phrase += s.toUpperCase() + " ";
            encoded += codec.encode(s) + " ";
        }

        System.out.println("PHRASE =" + phrase);
        System.out.println("ENCODED=" + encoded);
    }
}

В задаче encode свойству classpath присваивается configurations.runtime. configurations это коллекция зависимостей, объединённых по общим признакам. В нашем случае runtime содержит те зависимости, которые должны быть доступны для приложения во время выполнения. Такие зависимости отличны от зависимостей, которые необходимы для компиляции (compile), запуска тестов, или зависимостей, которые нужны для компиляции и запуска одновременно, но представлены такой средой выполнения, как сервер приложений. Свойство configurations в Gradle это коллекция всех конфигураций, определённых в билде, каждая из которых в свою очередь является коллекцией реальных зависимостей.

В файле билда объявлена внешняя зависимость от библиотеки Apache Commons Codec. Мы компилируем наш Java-файл, затем формируем командную строку запуска приложения, используя путь скомпилированного .class файла и JAR-зависимость. В файле билда указываем класс, в котором запустится метод main() (org.gradle.example.commandline.MetaphoneEncoder), задаём ему параметры командной строки в форме списка, и указываем необходимые элементы classpath. В данном случае мы можем условно сослаться на основные классы, доступные в sourceSets, и на все зависимости, объявленные в конфигурации runtime. Задача описанная в примере будет работать даже если мы определим множество других зависимостей из разных репозиториев, включая статические зависимости в директории проекта.

Пользовательские типы задач


Иногда возможностей встроенных задач Gradle может быть не достаточно для решения вашей задачи. Тогда создание пользовательской задачи будет самым выразительным способом, который можно применить при разработке вашего билда. Gradle позволяет сделать это несколькими способами. Мы рассмотрим два наиболее распространённых.

Определение пользовательского типа задачи в файле билда


Допустим, в вашем билде нужно выполнить различные запросы к базе данных MySQL. В Gradle такая задача решается несколькими способами, но вы пришли к выводу, что создание пользовательской задачи будет наиболее выразительным решением. Простейший способ создания задачи — объявить её так, как показано в примере ниже:

Пример 36. Пользовательская задача для выполнения запросов в базе данных MySQL (из примера custom-task

task createDatabase(type: MySqlTask) {
  sql = 'CREATE DATABASE IF NOT EXISTS example'
}

task createUser(type: MySqlTask, dependsOn: createDatabase) {
  sql = "GRANT ALL PRIVILEGES ON example.* TO exampleuser@localhost IDENTIFIED BY 'passw0rd'"
}

task createTable(type: MySqlTask, dependsOn: createUser) {
  username = 'exampleuser'
  password = 'passw0rd'
  database = 'example'
  sql = 'CREATE TABLE IF NOT EXISTS users (id BIGINT PRIMARY KEY, username VARCHAR(100))'
}

class MySqlTask extends DefaultTask {
  def hostname = 'localhost'
  def port = 3306
  def sql
  def database
  def username = 'root'
  def password = 'password'
  
  @TaskAction
  def runQuery() {
    def cmd
    if(database) {
      cmd = "mysql -u ${username} -p${password} -h ${hostname} -P ${port} ${database} -e "
    }
    else {
      cmd = "mysql -u ${username} -p${password} -h ${hostname} -P ${port} -e "
    }
    project.exec {
      commandLine = cmd.split().toList() + sql
    }
  }
}

Пользовательская задача MySqlTask наследуется от класса DefaultTask. Все пользовательские задачи должны наследоваться от класса DefaultTask, либо производного от него класса. (Пользовательская задача может наследоваться и от другого типа задачи, отличного от DefaultTask. См. выше параграф Типы задач, где описаны наиболее важные встроенные типы задач.) В терминах Groovy, в задаче объявлены свойства (такие как hostname, database, sql и т.д.). Далее объявлен метод runQuery(), который помечен аннотацией @TaskAction. При выполнении задачи runQuery() запустится.

Фактические задачи билда, определённые в начале файла билда, объявлены как задачи типа MySqlTask. Таким образом, они автоматически наследуют свойства и операцию базового класса задач. Для большинства свойств определены значения по умолчанию (однако для таких свойств как username и password значения, конечно же, специфичны для билда), потому остаётся лишь небольшая часть того, что нужно сконфигурировать, прежде чем выполнить каждую из задач. Для задач createDatabase и createUser конфигурируется всего лишь один SQL-запрос, остальные же значения в дальнейшем используются по умолчанию.

Задача createTable переопределяет свойства username, password и database. Таким образом, зависимости задачи создают новую базу данных и пользователя, отличные от административных настроек по умолчанию. Паттерн, который при необходимости переопределяет настройки конфигурации по умолчанию, широко применяется В Gradle.

Определение пользовательского типа задачи в дереве исходников


Если пользовательская задача очень велика, её код может существенно усложнять файл билда. Как было показано в примере выше, задача может состоять из нескольких строк простого кода. Однако на определённом этапе задача может развиться в свою собственную иерархию классов c зависимостями от внешнего API и необходимостью применить автоматизированное тестирование. Билд является кодом, а сложный код билда нужно рассматривать, как полноправного обитателя мира разработки кода. Такая задача в Gradle решается просто.

Когда логика пользовательской задачи перерастает разумные пределы файла билда, мы можем её перенести в директорию buildSrc, которая находится в корне проекта. Директория эта автоматически компилируется и добавляется в classpath билда. Мы изменим предыдущий пример, в котором будем использовать buildSrc:

Пример 37. Билд-файл использующий пользовательскую задачу, определённый во внешнем файле

task createDatabase(type: MySqlTask) {
  sql = 'CREATE DATABASE IF NOT EXISTS example'
}

task createUser(type: MySqlTask, dependsOn: createDatabase) {
  sql = "GRANT ALL PRIVILEGES ON example.* TO exampleuser@localhost IDENTIFIED BY 'passw0rd'"
}

task createTable(type: MySqlTask, dependsOn: createUser) {
  username = 'exampleuser'
  password = 'passw0rd'
  database = 'example'
  sql = 'CREATE TABLE IF NOT EXISTS users (id BIGINT PRIMARY KEY, username VARCHAR(100))'
}

Пример 38. Определение пользовательской задачи в директории buildSrc

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class MySqlTask extends DefaultTask {
  def hostname = 'localhost'
  def port = 3306
  def sql
  def database
  def username = 'root'
  def password = 'password'
  
  @TaskAction
  def runQuery() {
    def cmd
    if(database) {
      cmd = "mysql -u ${username} -p${password} -h ${hostname} -P ${port} ${database} -e "
    }
    else {
      cmd = "mysql -u ${username} -p${password} -h ${hostname} -P ${port} -e "
    }
    project.exec {
      commandLine = cmd.split().toList() + sql
    }
  }
}

Заметим, что определение задачи в директории buildSrc полностью совпадает с кодом, включённым в скрипт билда в позапрошлом примере. Тем не менее, теперь у нас появляется работоспособная структура проекта, пригодная для совершенствования кода простой задачи, наращивания объектной модели, написания тестов и всего остального, что мы обычно делаем разрабатывая код.

Есть четыре способа, куда вы можете поместить пользовательский билд-код Gradle. Первый — добавить код собственно, в билд-скрипт, в блок операции задачи. Второй — создать внешний файл в директории buildSrc, как было только что сделано в последнем примере. Третий способ — импортировать внешний файл с билд-скриптом в наш основной билд-скрипт. Четвёртый — импорт внешнего модуля, написанного на Java или Goovy. Создание модулей в Gradle — отдельная тема, которую мы затрагивать не будем.

Пример 39. Структура проекта Gradle, использующего пользовательский код, помещённый в директорию buildSrc

.
├── build.gradle
├── buildSrc
│   └── src
│       └── main
│           └── groovy
│               └── org
│                   └── gradle
│                       └── example
│                           └── task
│                               └── MySqlTask.groovy


Откуда берутся задачи


До настоящего момента мы создавали задачи путём непосредственного написания кода в билд-скриптах Gradle, либо в директории buildSrc в виде кода Groovy. Такой подход хорош для изучения задач, так как даёт подробный обзор всех их особенностей. Всё же, большинство задач, которые вы будете использовать, не будут написаны вами. Они будут импортироваться из внешних модулей.

В простейшем примере сборки консольного приложения HelloWorld на Java, файл билда выглядит следующим образом:

apply plugin: 'java'

Применив модуль Java, билд-скрипт автоматически наследует набор задач, код которых вам не виден. Вы можете изменять поведение наследованных задач в блоках конфигурации, либо используя рассмотренные выше методы doFirst() и doLast(), для которых вам придётся писать код. Ключевой стратегией Gradle являются широкие возможности для расширения при малой сложности. Gradle предлагает вам большой набор функциональности посредством задач, подробности реализации которых вам не нужно знать, которые вы запускаете используя Gradle DSL (DSL — Domain Specific Language), а не множество запутанных инструкций кода Groovy.

Кроме того, в Gradle есть несколько встроенных задач, таких как tasks и properties. Такие задачи не импортируются из модулей или вашего кода. Они являются стандартом Gradle DSL.

Заключение


В даной главе мы достаточно подробно рассмотрели задачи. Мы разобрались как их можно конфигурировать, создавать в файле скрипта, и получили представление о том, как Gradle разделяет действия для конфигурирования и выполнения между двумя фазами жизненного цикла. Увидели, что задачи являются полноценными объектами Groovy с богатым API. Рассмотрели API задач в достаточном объёме чтобы вы начали воспринимать их как сущности, которые можно менять программно. Мы также рассмотрели некоторые стандартные типы задач, которые даже без дополнительных настроек предоставляют вполне работающую функциональность.

Наконец, мы разобрались как можно создавать ваши собственные задачи программным путём. Для большинства пользователей стандартных возможностей модулей и встроенных задач Gradle достаточно, и для создания билдов пользовательский код можно не писать. Однако случаются исключения. Одна из ключевых возможностей Gradle — облегчить вам расширение возможностей билда без создания в билд-скриптах хаоса с большим объёмом трудноподдерживаемого кода Groovy. Данная возможность была продемонстрирована в примерах пользовательских задач, которые мы рассмотрели.

В Gradle задачи являются фундаментальными единицами процесса сборки. Про них можно рассказать намного больше, чем может охватить введение. Всё же, имея в арсенале знания из данной главы, вы уже достаточно подготовлены, чтобы начать использовать задачи и продолжать дальнейшее их изучение.

Имеющих идеи по улучшению текста,
а так же нашедших ошибки,
милости просим на GitHub.


За нахождение опечаток отдельная благодарность читателю Nikita_Rogatnev.
Tags:
Hubs:
+25
Comments4

Articles

Change theme settings