Глава 40. Разработка классов пользовательских задач.

Gradle поддерживает два типа задач. Один тип - это просто задача, которую вы задаете с помощью замыкания. Мы уже видели такие задачи в Главе 16 Основы сборочного скрипта. Для такого типа задач, замыкание определяет ее поведение. Этот тип хорош для реализации одноразовых задач в вашем сборочном скрипте.

Другой тип - расширенная задача, у которой поведение встроено прямо в нее саму и она предоставляет несколько свойств, которые вы можете использовать для его настройки. Мы их видели в Главе 19. Снова о задачах. Большинство плагинов Gradle используют расширенные задачи. При использовании расширенных задач, вам нет необходимости реализовывать поведение, как приходится при использовании простых. Вы просто объявлете задачу и настраиваете е свойства. Таким образом, расширенные задачи позволяют вам повторно использовать кусочки поведения во множестве различных мест, возможно даже в различных скриптах.

Поведение и свойства расширенной задачи задаются ее классом. Когда вы объявляете такую задачу, то указываете тип или ее класс.

Реализовать свой собственный класс задачи очень просто. Вы можете реализовать пользовательский класс задачи почти на любом языке, который вам нравится и который компилируется в байткод. В наших примерах мы намерены использовать Groovy в качестве языка реализации, но вы можете использовать Java или Scala. В общем случае, использование Groovy - самый легкий способ, потому что Gradle API предназначен для работы с ним.

40.1. Упаковка класса задачи.

Есть несколько мест, где вы можете расположить исходные коды класса задачи.

Сборочный скрипт

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

Проект buildSrc

Вы можете поместить исходные коды класса в папку rootProjectDir/buildSrc/src/main/groovy. Gradle позаботится о его компиляции и тестировании и сделает его доступным в пути к классам сборочного скрипта. Класс задачи виден каждому сборочному скрипту, используемому в сборке. Использование такого подхода отделяет объявление задачи - то есть, что задача должна делать, от ее реализации - то есть, как она это делает.

Смотрите Главу 43 Организация логики сборки, чтобы узнать больше о проекте buildSrc.

Автономный проект

Вы можете создать отдельный проект для вашего класса задачи. Этот проект выдает и публикует jar-файл, который вы затем можете использовать в нескольких сборках и делиться с другими людьми. В общем случае, этот jar-файл может включать несколько пользовательских плагинов или несколько связанных классов задач, упакованных в одиночную библиотеку. Или комбинацию этих двух подходов.

В наших примерах, мы начнем с класса задачи в сборочном скрипте, чтобы все было просто. Потом мы посмотрим на создание автономного проекта.

40.2. Написание простого класса задачи.

Чтобы реализовать пользовательский класс задачи, вам нужно наследоваться от DefaultTask.

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

build.gradle

class GreetingTask extends DefaultTask {
}
	  

Эта задачи не делает ничего полезного, так что давайте добавим немного поведения. Для этого мы добавим метод к задачи и пометим его аннотацией TaskAction. Gradle вызовет этот метод, когда задача будет выполняться. Вы не должны использовать метод для определения поведения задачи. Вы можете, например, вызвать doFirst() или doLast() с замыканием в конструкторе задачи, чтобы добавить поведение.

Пример 40.2. Задача hello world

build.gradle

task hello(type: GreetingTask)

class GreetingTask extends DefaultTask {
    @TaskAction
    def greet() {
        println 'hello from GreetingTask'
    }
}
	  

Вывод команды gradle -q hello

> gradle -q hello
hello from GreetingTaskn
	  

Давайте добавим свойство к задаче, чтобы мы могли настроить его. Задачи - это просто POGO (Plain Old Groovy Object (простые старые объекты Groovy)) и когда вы объявляете задачу, то можете устанавливать свойства или вызывать методы у объекта задачи. Здесь мы добавить свойство greeting и установим значением, когда объявим задачу greeting.

Пример 40.3. Настраиваемая задача hello world

build.gradle

// Use the default greeting
task hello(type: GreetingTask)

// Customize the greeting
task greeting(type: GreetingTask) {
    greeting = 'greetings from GreetingTask'
}

class GreetingTask extends DefaultTask {
    String greeting = 'hello from GreetingTask'

    @TaskAction
    def greet() {
        println greeting
    }
}
	  

Вывод команды gradle -q hello greeting

> gradle -q hello greeting
hello from GreetingTask
greetings from GreetingTask
	  

40.3. Автономный проект.

Теперь мы переместим нашу задачу в автономный проект, чтобы мы могли ее опубликовать и поделиться с другими. Этот проект всего навсего проект Groovy, который выдает jar-файл, содержащий класс задачи. Вот простой сборочный скрипт для проекта. Он применяет плагин Groovy и добавляет Gradle API в качестве зависимости времени компиляции.

Пример 40.4. Сборка для пользовательской задачи

build.gradle

apply plugin: 'groovy'

dependencies {
    compile gradleApi()
    compile localGroovy()
}
	  

Примечание: Код этого примера можно найти в папке samples/customPlugin/plugin дистрибутива Gradle '-all'.

Мы просто следуем соглашению где должны размещаться исходные коды класса задачи.

Пример 40.5. Пользовательская задача

src/main/groovy/org/gradle/GreetingTask.groovy

package org.gradle

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

class GreetingTask extends DefaultTask {
    String greeting = 'hello from GreetingTask'

    @TaskAction
    def greet() {
        println greeting
    }
}
	  

40.3.1. Использование вашего класса задачи в другом проекте.

Чтобы использовать класс задачи в сборочном скрипте, вам нужно добавить его в путь к классам. Чтобы сделать это, используйте блок buildscript { }, как описано в Секции 43.6 Внешние зависимости скрипта. В следующем примере показано как вы можете это сделать, когда jar-файл, содержащий класс задачи, был опубликован в локальное хранилище:

Пример 40.6. Использование пользовательской задачи в другом проекте

build.gradle

buildscript {
    repositories {
        maven {
            url uri('../repo')
        }
    }
    dependencies {
        classpath group: 'org.gradle', name: 'customPlugin',
                  version: '1.0-SNAPSHOT'
    }
}

task greeting(type: org.gradle.GreetingTask) {
    greeting = 'howdy!'
}
	  

40.3.2. Написание тестов для вашего класса задачи.

Вы можете использовать класс ProjectBuilder для создания экземпляров Project, когда тестируете ваш класс задачи.

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

src/test/groovy/org/gradle/GreetingTaskTest.groovy

class GreetingTaskTest {
    @Test
    public void canAddTaskToProject() {
        Project project = ProjectBuilder.builder().build()
        def task = project.task('greeting', type: GreetingTask)
        assertTrue(task instanceof GreetingTask)
    }
}
	  

40.4. Инкрементные задачи.

Инкрементные задачи - инкубационная возможность.

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

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

С Gradle очень легко реализовать задачу, которая пропускается, когда все ее входные и выходные данные актуальны (смотрите 19.9. Проверки на устаревание (также известные как Инкрементная Сборка).). Однако, бывают случаи, когда только несколько входных файлов изменились со времени прошлого выполнения и вы бы хотели избежать повторной обработки всех не изменившихся входных данных. Это особенно полезно для задач трансформации, которые преобразовывают входные файлы один к одному.

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

40.4.1. Реализация инкрементной задачи.

Чтобы задачи обрабатывала входные данные инкрементно, она должна содержать инкрементное действие. Это такой метод действия, который содержит одиночный параметр IncrementalTaskInputs, который указывает Gradle, что действие будет обрабатывать только изменившиеся входные данные.

Действию инкрементной задачи можно передавать IncrementalTaskInputs.outOfDate(org.gradle.api.Action) для обработки любого неактуального входного файла и IncrementalTaskInputs.removed(org.gradle.api.Action), которое выполняется для любого входного файла, удаленного со времени последнего запуска.

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

build.gradle

class IncrementalReverseTask extends DefaultTask {
    @InputDirectory
    def File inputDir

    @OutputDirectory
    def File outputDir

    @Input
    def inputProperty

    @TaskAction
    void execute(IncrementalTaskInputs inputs) {
        println inputs.incremental ? "CHANGED inputs considered out of date"
                                   : "ALL inputs considered out of date"
        if (!inputs.incremental)
            project.delete(outputDir.listFiles())

        inputs.outOfDate { change ->
            println "out of date: ${change.file.name}"
            def targetFile = new File(outputDir, change.file.name)
            targetFile.text = change.file.text.reverse()
        }

        inputs.removed { change ->
            println "removed: ${change.file.name}"
            def targetFile = new File(outputDir, change.file.name)
            targetFile.delete()
        }
    }
}
	  

Примечание: Код этого примера можно найти в папке samples/userguide/tasks/incrementalTask дистрибутива Gradle '-all'

Если по какой-либо причине задача запущена не инкрементно, например с опцией --rerun-tasks, выполнится действие outOfDate, даже если есть удаленные входные файлы. Вам стоит подумать о том, чтобы обрабатывать этот случай в самом начале, как сделано в примере выше.

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

В задаче может содержаться только одно действие инкрементной задачи.

40.4.2. Какие входные данные считаются неактуальными?

Когда у Gradle есть история предыдущего выполнения задачи и те изменения в контексте выполнения задачи, которые связаны с входными файлами, тогда Gradle имеет возможность вычислить какие входные файлы задаче необходимо снова обработать. В таком случае, действие IncrementalTaskInputs.outOfDate(org.gradle.api.Action) будет выполнено для любого входного файла, который был добавлен или изменен и IncrementalTaskInputs.removed(org.gradle.api.Action) для любого удаленного входного файла.

Однако, во многих случаях Gradle не в состоянии вычислить какие входные файлы необходимо снова обработать. Например:

  • История предыдущего выполнения недоступна.
  • Вы собираете другой версией Gradle. На текущий момент, Gradle не использует историю задачи из другой версии.
  • Критерий upToDateWhen, добавленный к задаче, возвращает false.
  • Входное свойство изменилось со времени предыдущего выполнения.
  • Один или более выходных файлов изменились со времени предыдущего выполнения.

В любом из этих случаев, Gradle будет считать, что все входные файлы неактуальны. Действие IncrementalTaskInputs.outOfDate(org.gradle.api.Action) будет выполнено для каждого входного файла, а действие IncrementalTaskInputs.removed(org.gradle.api.Action) вообще не будет выполнено.

Вы можете проверить, смог ли Gradle вычислить инкрементные изменения во входных файлах, с помощью IncrementalTaskInputs.isIncremental().

40.4.3. Инкрементная задача в действии

Имея инкрементную задачу из примера 40.8, мы можем исследовать различные сценарии изменения. Обратите внимание, что различные изменяющие задачи ('updateInputs', 'removeInput' и так далее) присутствуют только в демонстрационных целях: обычно они не будут частью вашего сборочного скрипта.

Во-первых, рассмотрим IncrementalReverseTask, выполняемую на наборе входных данных первый раз. В этом случае, все входные данные будут считаться неактуальными:

Пример 40.9. Запуск инкрементной задачи в первый раз

build.gradle

task incrementalReverse(type: IncrementalReverseTask) {
    inputDir = file('inputs')
    outputDir = file("$buildDir/outputs")
    inputProperty = project.properties['taskInputProperty'] ?: "original"
}
	  

Разметка сборки

incrementalTask/
  build.gradle
  inputs/
    1.txt
    2.txt
    3.txt
	  

Вывод команды gradle -q incrementalReverse

> gradle -q incrementalReverse
ALL inputs considered out of date
out of date: 1.txt
out of date: 2.txt
out of date: 3.txt
	  

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

Пример 40.10. Запуск инкрементной задачи с неизменившимися входными данными

Вывод команды gradle -q incrementalReverse

> gradle -q incrementalReverse
	  

Когда входной файл изменен каким-либо образом или добавлен новый входной файл, тогда при повторном выполнении действие IncrementalTaskInputs.outOfDate(org.gradle.api.Action) сообщает об этих файлах:

Пример 40.11. Запуск инкрементной задачи с обновленными входными файлами

build.gradle

task updateInputs() {
    doLast {
        file('inputs/1.txt').text = "Changed content for existing file 1."
        file('inputs/4.txt').text = "Content for new file 4."
    }
}
	  

Вывод команды gradle -q updateInputs incrementalReverse

> gradle -q updateInputs incrementalReverse
CHANGED inputs considered out of date
out of date: 1.txt
out of date: 4.txt
	  

Когда существующий входной файл удален, тогда повторное выполнение задачи сообщит о нем в действии IncrementalTaskInputs.removed(org.gradle.api.Action):

Пример 40.12. Запуск инкрементной задачи с удаленным входными файлом

build.gradle

task removeInput() {
    doLast {
        file('inputs/3.txt').delete()
    }
}
	  

Вывод команды gradle -q removeInput incrementalReverse

> gradle -q removeInput incrementalReverse
CHANGED inputs considered out of date
removed: 3.txt
	  

Когда выходной файл удален (или изменен), Gradle не может вычислить какие входные файлы неактуальны. В этом случае, обо всех входных файлах сообщает действие IncrementalTaskInputs.outOfDate(org.gradle.api.Action) и ни об одном IncrementalTaskInputs.removed(org.gradle.api.Action):

Пример 40.13. Запуск инкрементной задачи с удаленным выходным файлом

build.gradle

task removeOutput() {
    doLast {
        file("$buildDir/outputs/1.txt").delete()
    }
}
	  

Вывод команды gradle -q removeOutput incrementalReverse

> gradle -q removeOutput incrementalReverse
ALL inputs considered out of date
out of date: 1.txt
out of date: 2.txt
out of date: 3.txt
	  

Когда изменяется входное свойство задачи, Gradle не способен вычислить как оно влияет на выходные данные, так что все входные файлы считаются неактуальными. Так же как и в случае удаленного выходного файла, обо всех входных файлах сообщает действие IncrementalTaskInputs.outOfDate(org.gradle.api.Action) и не об одном IncrementalTaskInputs.removed(org.gradle.api.Action):

Пример 40.14. Запуск инкрементной задачи с измененным входным свойством

Вывод команды gradle -q -PtaskInputProperty=changed incrementalReverse

> gradle -q -PtaskInputProperty=changed incrementalReverse
ALL inputs considered out of date
out of date: 1.txt
out of date: 2.txt
out of date: 3.txt