Глава 19. Снова о задачах.

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

19.1. Определение задач.

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

Пример 19.1. Определение задач

build.gradle

task(hello) {
    doLast {
        println "hello"
    }
}

task(copy, type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}
	  

Вы можете использовать строки для имен задач:

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

build.gradle

task('hello') {
    doLast {
        println "hello"
    }
}

task('copy', type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}
	  

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

Пример 19.3. Определение задач с помощью альтернативного синтаксиса

build.gradle

tasks.create(name: 'hello') {
    doLast {
        println "hello"
    }
}

tasks.create(name: 'copy', type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}
	  

В этом примере мы добавляем задачи в коллекцию tasks. Взгляните на TaskContainer, чтобы увидеть разновидности метода create().

19.2. Нахождение задач.

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

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

build.gradle

task hello

println hello.name
println project.hello.name
	  

Еще задачи доступны из коллекции tasks.

Пример 19.5. Получение доступа к задачам с помощью коллекции tasks

build.gradle

task hello

println tasks.hello.name
println tasks['hello'].name
	  

Вы можете получить доступ к задачам из любого проекта, используя путь задачи, используя метод tasks.getByPath(). Вы можете вызвать метод getByPath() с именем задачи, или относительным путем, или абсолютным путем.

Пример 19.6. Получение доступа к задачам по пути

build.gradle

project(':projectA') {
    task hello
}

task hello

println tasks.getByPath('hello').path
println tasks.getByPath(':hello').path
println tasks.getByPath('projectA:hello').path
println tasks.getByPath(':projectA:hello').path
	  

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

> gradle -q hello
:hello
:hello
:projectA:hello
:projectA:hello
	  

Чтобы узнать больше опций нахождения задач, смотрите TaskContainer.

19.3. Настройка задач.

В качестве примера, давайте взглянем на задачу Copy, предоставляемую Gradle. Для создания задачи Copy для вашего сборочного скрипта, вы можете объявить ее:

Пример 19.7. Создание задачи copy

build.gradle

task myCopy(type: Copy)
	  

Здесь создается задача copy без поведения по умолчанию. Настроить задачу можно с помощью ее API (смотрите Copy). Следующие примеры показывают несколько различных способов для получения одинаковой конфигурации.

Для ясности, обратите внимание, что название задачи 'myCopy', но ее тип 'Copy'. У вас может быть несколько задач с одним и тем же типом, но на различными именами. Вы поймете, что это дает вам огромную силу в решении сковозных проблем всех задач определенного типа.

Пример 19.8. Настройка задача - различные способы

build.gradle

Copy myCopy = task(myCopy, type: Copy)
myCopy.from 'resources'
myCopy.into 'target'
myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')
	  

Это походит на тот способ, которым мы настраиваем объекты в Java. Вы должны повторять контекст (myCopy) в утверждениях конфигурации каждый раз. Это выглядит избыточным и не очень красивым для чтения.

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

Пример 19.9. Настройка задача - с помощью замыкания

build.gradle

task myCopy(type: Copy)

myCopy {
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}
	  

Этот способ работает для любой задачи. Строка 3 в примере просто сокращение для метода tasks.getByName(). Важно обратить внимание, что если вы передаете замыкание в метод getByName(), это замыкание применяется во время конфигурации задачи, не во время выполнения.

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

Пример 19.10. Определение задачи с замыканием

build.gradle

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

19.4. Добавление зависимостей к задаче.

Не забывайте про фазы сборки

У задачи есть конфигурация и действия. Когда вы используете <<, вы просто используете сокращение для определения действия. Код, определенный в конфигурационной секции вашей задачи, выполнится во время фазы конфигурации сборки, в не смотря на то, на какую задачу это нацелено. Для деталей о жизненном цикле сборки, смотрите Главу 22 Жизненный цикл сборки.

Существует несколько способов определения зависимостей задачи. В Секции 16.5 Зависимости задач вам было представлен способ определения зависимости с использование имен задач. Имена задач могут ссылаться на задачи в том же самом проекте, что и задача или на задачи из других проектов. Для того, чтобы сослаться на задачу из другого проекта, вы подставляете путь проекта, которому принадлежит эта задача, перед ее именем. Следующий пример добавляет зависимость projectA:taskX от projectB:taskY:

Пример 19.11. Добавление зависимости на задачу из другого проекта

build.gradle

project('projectA') {
    task taskX(dependsOn: ':projectB:taskY') {
        doLast {
            println 'taskX'
        }
    }
}

project('projectB') {
    task taskY {
        doLast {
            println 'taskY'
        }
    }
}
	  

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

> gradle -q taskX
taskY
taskX
	  

Вместо использования имени задачи, вы можете определить зависимость с помощью объекта Task, как показано в этом примере:

Пример 19.12. Добавление зависимости, используя объект задачи

build.gradle

task taskX {
    doLast {
        println 'taskX'
    }
}

task taskY {
    doLast {
        println 'taskY'
    }
}

taskX.dependsOn taskY
	  

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

> gradle -q taskX
taskY
taskX
	  

Для более продвинутого использования, вы можете определить зависимость задачи, используя замыкание. При вычислении, замыкание передается задачи, чьи зависимости подсчитываются. Замыкание должно возвращать одиночный объект Task или коллекцию таких объектов, которые затем рассматриваются в качестве зависимостей задачи. Следующий пример добавляет зависимость задачи taskX ко все задачам в проекте, чье имя начинается с lib:

Пример 19.13. Добавление зависимости, используя замыкание

build.gradle

task taskX {
    doLast {
        println 'taskX'
    }
}

taskX.dependsOn {
    tasks.findAll { task -> task.name.startsWith('lib') }
}

task lib1 {
    doLast {
        println 'lib1'
    }
}

task lib2 {
    doLast {
        println 'lib2'
    }
}

task notALib {
    doLast {
        println 'notALib'
    }
}
	  

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

> gradle -q taskX
lib1
lib2
taskX
	  

Чтобы узнать больше информации о зависимостях задачи, смотрите Task API.

19.5. Порядок задач.

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

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

Порядок задачи может быть полезным в нескольких сценариях:

  • Принудить к последовательному порядку задач: например, 'build' никогда не запускается перед 'clean'.
  • Запусть проверку достоверности сборки как можно раньше: например, проверить, что аутентификационные данные корректны перед тем как запускать работу по сборке выпуска.
  • Получить обратную связь быстрее, запуская быструю проверку достоверности до долгой: например, юнит тесты должны запускаться до интеграционных.
  • Задача, которая собирает результаты всех задач определенного типа: например, задача отчета по тестам объединяет выводы всех выполненных задач тестов.

Доступны два правила порядка: 'должна запуститься после' и 'стоит запуститься после'.

Когда вы используете правило порядка 'должна запуститься после', вы указываете, что задача taskB должна запускаться после taskA всякий раз, когда будуте запущены они обе. Это выражается как taskB.mustRunAfter(taskA). Правиль порядка 'стоит запуститься после' очень похоже на предыдущее, но менее строгое, так как будет проигнорировано в двух ситуациях. Во-первых, если использование правила порождает цикл. Во-вторых, когда используется параллельном выполнение и все зависимости задачи были удовлетворены, не считая задачи 'стоит запуститься после', тогда эта задача будет выполнена невзирая на то, что ее зависимости 'стоит запускаться после' были запущены или нет. Вы должны использовать 'стоит запускаться после', когда порядок полезен, но не обязателен.

С наличием этих правил, все еще возможно выполниться задачу taskA без taskB и наоборот.

Пример 19.14. Добавление порядка задачи 'должна запуститься после'

build.gradle

task taskX {
    doLast {
        println 'taskX'
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}
taskY.mustRunAfter taskX
	  

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

> gradle -q taskY taskX
taskX
taskY
	  
Пример 19.15. Добавление порядка задачи 'стоит запуститься после'

build.gradle

task taskX {
    doLast {
        println 'taskX'
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}
taskY.shouldRunAfter taskX
	  

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

> gradle -q taskY taskX
taskX
taskY
	  

В примерах выше все еще возможно выполнить задачу taskY так, что в результате не будет запущена задача taskX:

Пример 19.16. Порядок задачи не подразумевает выполнение задачи

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

> gradle -q taskY
taskY
	  

Для указания порядка 'должна запуститься после' или 'стоит запуститься после' между двумя задачами, используйте методы Task.mustRunAfter(java.lang.Object[]) и Task.shouldRunAfter(java.lang.Object[]). Эти методы принимают на вход экземпляр задачи, имя или другие данные, примаемые Task.dependsOn(java.lang.Object[]).

Обратите внимание, что 'B.mustRunAfter(A)' или 'B.shouldRunAfter(A)' не подразумевает зависимости выполнения между задачами:

  • Существует возможность запустить задачи A и B независимо. Правило порядка используется только тогда, когда обе задачи запланированы на выполнение.
  • При запуске с --continue, для задачи B есть возможность выполниться в случает неудачного выполнения задачи A.

Как уже упоминалось раньше, правило порядка 'стоит запуститься после' будет проигнорировано, если из-за него образуется цикл:

Пример 19.17. Правило порядка 'стоит запуститься после' игнорируется из-за образования цикла

build.gradle

task taskX {
    doLast {
        println 'taskX'
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}
task taskZ {
    doLast {
        println 'taskZ'
    }
}
taskX.dependsOn taskY
taskY.dependsOn taskZ
taskZ.shouldRunAfter taskX
	  

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

> gradle -q taskX
taskZ
taskY
taskX
	  

19.6. Добавление описания к задаче.

Вы можете добавить описание к вашей задаче. Это описание отображается, когда выполняется команда gradle tasks.

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

build.gradle

task copy(type: Copy) {
   description 'Copies the resource directory to the target directory.'
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}
	  

19.7. Замена задач.

Иногда вы хотите заменить задачу. Например, вы хотите подменить задачу, добавленную плагином Java, на пользовательскую другого типа. Вы можете этого добиться с:

Пример 19.19. Перезапись задачи

build.gradle

task copy(type: Copy)

task copy(overwrite: true) {
    doLast {
        println('I am the new one.')
    }
}
	  

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

> gradle -q copy
I am the new one.
	  

Задач с типом Copy будет заменена на определенную вами задачу, потому что она использует то же самое имя. Когда вы определяете новую задачу, то должны установить свойство overwrite в true. В противном случае, Gradle выбросит исключение, говорящее о том, что задача с таким именем уже существует.

19.8. Пропуск задач.

Gradle предлагает множество способов пропустить выполнение задачи.

19.8.1. Используя предикат.

Вы можете использовать метод onlyIf() для присоединения предиката к задаче. Действия задачи будут выполнены только если предикат будет вычислен в true. Он реализуется в виде замыкания. Замыкание передается задаче как параметр и должно возвращать true, если задача должна быть выполнена и false, если должна быть пропущена. Предикат вычисляется непосредственно перед выполнением задачи.

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

build.gradle

task hello {
    doLast {
        println 'hello world'
    }
}

hello.onlyIf { !project.hasProperty('skipHello') }
	  

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

> gradle hello -PskipHello
:hello SKIPPED

BUILD SUCCESSFUL

Total time: 1 secs
	  

19.8.2. Используя StopExecutionException.

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

Пример 19.21. Пропуск задачи с использованием StopExecutionException

build.gradle

task compile {
    doLast {
        println 'We are doing the compile.'
    }
}

compile.doFirst {
    // Here you would put arbitrary conditions in real life.
    // But this is used in an integration test so we want defined behavior.
    if (true) { throw new StopExecutionException() }
}
task myTask(dependsOn: 'compile') {
    doLast {
        println 'I am not affected'
    }
}
	  

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

> gradle -q myTask
I am not affected
	  

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

19.8.3. Включение и выключение задач.

У каждой задачи есть флаг enabled по умолчанию установленный в true. Установка его в false препятствует выполнению любого действия задачи.

Пример 19.22. Включение и выключение задач

build.gradle

task disableMe {
    doLast {
        println 'This should not be printed if the task is disabled.'
    }
}
disableMe.enabled = false
	  

Вывод команды gradle disableMe

> gradle disableMe
:disableMe SKIPPED

BUILD SUCCESSFUL

Total time: 1 secs
	  

19.9. Проверки на устаревание (также известные как Инкрементная Сборка).

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

Gradle поддерживает такое поведение из коробки, благодаря возможности, называемой инкрементная сборка. Определенно вы уже видели ее в действии: она активна почти каждый раз, когда текст UP-TO-DATE появляется рядом с именем задачи, при запуске сборки.

Как работает инкрементальная сборка? И что надо сделать, чтобы использовать ее в ваших собственных задачах? Давайте посмотрим.

19.9.1. Входные и выходные данные задачи.

В самом общем случае, задаче принимает данные на вход и генерирует некоторые результаты. Если мы используем пример компиляции, рассмотренный ранее, вы можем увидеть, что файлы исходных кодов - входные данные и, в случае Java, сгенерированные class-файлы - выходные. Другие входные данные могут включать вещи наподобие содержать ли отладочную информацию.

Рисунок 19.1. Пример входных и выходных данных задачи
Рисунок 19.1. Пример входных и выходных данных задачи

Важная характеристика входа в том, что он влият на один или более выходов, как вы можете видеть на рисунке. Различный байт-код генерируется в зависимости от содержимого файлов исходных кодов и минимальной версии среды выполнения Java, на которой вы хотите запустить этот код. Это делает их входными данными задачи. Но запускается ли компиляция в другом потоке или нет, в зависимости от свойства fork, не влияет на то, какой байт-код будет сгенерирован. В терминологии Gradle, fork просто внутреннее свойство задачи.

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

Для авторов сборки все просто: вам необходимо сказать Gradle какие из свойств задачи являются входными и какие выходными данными. Если свойство задачи влияет на выход, удостоверьтесь, что зарегистрировали его как вход, в противном случае, задачи будет считать свежей, не являясь таковой. С другой стороны, не регистрируйте свойства, как входные данные, если они не влияют на выход, иначе задача потенциально может быть выполнена без необходимости. Также будьте осторожны при работе с недетерминистическими задачами, которые могут выдавать различные выходные данные для одинаковых входных: они не должны быть настроены на инкрементальную сборку, так как проверки на устаревание не сработают.

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

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

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

  1. Создать типизированные поля и свойства (с помощью методов получения) для каждого входа и выхода задачи.
  2. Добавить соответствующую аннотацию к каждому из этим полей или методов получения.

Gradle поддерживает три основным категории входных и выходных данных:

  • Простые значения

    Такие вещи как строки и числа. Обобщая, простым значением может быть любое значение, которое реализует Serializable.

  • Типы файловой системы

    Они состоят из стандартного класса File, но также и производных от типа Gradle FileCollection и любого другого, что может быть передано методу Project.file(java.lang.Object) - для одиночных свойств файла/папки - или методу Project.files(java.lang.Object[]).

  • Вложенные значения

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

Например, представьте, что у вас есть задача, которая обрабатывает шаблоны различных типов, таких как FreeMarker, Velocity, Moustache и так далее. Она принимает шаблоны файлов исходных кодов и объединяет их с некоторыми данными модели для генерации заполненных шаблонных файлов.

У этой задачи есть три входа и один выход:

  • Шаблоны исходных файлов.
  • Данные модели.
  • Движок шаблонов.
  • Куда записать выходные файлы.

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

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

buildSrc/src/main/java/org/example/ProcessTemplates.java

package org.example;

import java.io.File;
import java.util.HashMap;
import org.gradle.api.*;
import org.gradle.api.file.*;
import org.gradle.api.tasks.*;

public class ProcessTemplates extends DefaultTask {
    private TemplateEngineType templateEngine;
    private FileCollection sourceFiles;
    private TemplateData templateData;
    private File outputDir;

    @Input
    public TemplateEngineType getTemplateEngine() {
        return this.templateEngine;
    }

    @InputFiles
    public FileCollection getSourceFiles() {
        return this.sourceFiles;
    }

    @Nested
    public TemplateData getTemplateData() {
        return this.templateData;
    }

    @OutputDirectory
    public File getOutputDir() { return this.outputDir; }

    // + setter methods for the above - assume we’ve defined them

    @TaskAction
    public void processTemplates() {
        // ...
    }
}
	  

buildSrc/src/main/java/org/example/TemplateData.java

package org.example;

import java.util.HashMap;
import java.util.Map;
import org.gradle.api.tasks.Input;

public class TemplateData {
    private String name;
    private Map<String, String> variables;

    public TemplateData(String name, Map<String, String> variables) {
        this.name = name;
        this.variables = new HashMap<>(variables);
    }

    @Input
    public String getName() { return this.name; }

    @Input
    public Map<String, String> getVariables() {
        return this.variables;
    }
}
	  

Вывод команды gradle processTemplates

> gradle processTemplates
:processTemplates

BUILD SUCCESSFUL
	  

Вывод команды gradle processTemplates

> gradle processTemplates
:processTemplates UP-TO-DATE

BUILD SUCCESSFUL
	  

Много о чем можно поговорить в этом примере, так что давайте проработает каждое входное и выходное свойство по очереди:

  • templateEngine

    Представляет движок, которые используется для обработки исходных шаблонов, например, FreeMarker, Velocity и так далее. Вы можете реализовать его в виде строки, но в этом случае, мы использовали пользовательское перечисление, потому что оно дает намного больше информации и безопасности. Так как перечисления автоматически реализуют Serializable, мы можем рассматривать его как простое значение и использовать аннотацию @Input, так же, как мы сделали со свойством типа String.

  • sourceFiles

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

  • templateData

    Для этого примера мы используем пользовательский класс, представляющий данные модели. Однако, он не реализует Serializable, вследствие этого мы не можем использовать аннотацию @Input. Однако, это не является проблемой, так как свойства внутри TemplateData - строка и HashMap с сериализуемыми параметрами типа - сериализуемые и к ним можно добавить аннотацию @Input. Мы используем @Nested на templateData тем самым позволяя Gradle узнать, что это значение с вложенными входными свойствами.

  • outputDir

    Папка, в которую будут сгенерированы файлы. Как и с входными файлами, есть несколько аннотаций для выходных файлов и папок. Свойство, представляющее одиночную папку, требует @OutputDirectory. Вскоре вы узнаете и о других аннотациях.

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

Этот пример интересен еще и потому, что он работает с коллекцией исходных файлов. Что случится, если изменится только один исходный файл? Обработает ли задача все исходные файлы снова или только изменившийся? Это зависит от реализации задачи. Если только изменившийся, значит сама задача тоже инкрементальная, но это уже другая история. Gradle помогает авторами задачи реализовать эту возможность с помощью инкрементальных входов задачи.

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

Таблица 19.1. Аннотации типа свойства инкрементальной сборки
АннотацияОжидаемый тип свойстваОписание
@InputЛюбой сериализуемый типПростое входное значение
@InputFileFile*Одиночный входной файл (не папка)
@InputDirectoryFile*Одиночная входная папка (не файл)
@InputFilesIterable<File>*Итерируемые входные файлы и папки
@ClasspathIterable<File>*

Итерируемые входные файлы и папки, которые представляют путь к классам Java. Эта аннотация позволяет задаче игноррировать не относящиеся к делу изменения, такие как различные имена для одних и тех же файлов. Это похоже на добавление аннотации @PathSensitive(ОТНОСИТЕЛЬНЫЙ), но проигнорирует имена jar-файлов, добавленные непосредственно к пути к классам и будет считать изменение порядка файлов за изменение в пути к классам.

Чтобы оставаться совместимым с версиями Gradle до 3.2, свойства путей к классам также должны быть аннотированы @InputFiles.

@OutputFileFile*Одиночный выходной файл (не папка)
@OutputDirectoryFile*Одиночная выходная папка (не файл)
@OutputFilesMap<String, File>** или Iterable<File>*Итерируемые выходные файлы (не папки)
@OutputDirectoriesMap<String, File>** или Iterable*Итерируемые выходные папки (не файлы)
@NestedЛюбой пользовательский типПользовательский тип, который может не реализовывать Serializable, но хотя бы одно поле или свойство помечено одной из аннотаций из этой таблицы. Она даже может быть другая аннотация @Nested.
@ConsoleЛюбой типПоказывает, что свойство не является ни входом, ни выходом. Она просто влияет на вывод консоли задачи некоторым способом, таким как увеличение или уменьшение многословности задачи.
@InternalЛюбой типПоказывает, что свойство используется внутри, но не является ни входом, ни выходом.

*Фактически, File может быть любым типом, принимаемым Project.file(java.lang.Object) и Iterable<File> может быть любым типом, принимамемы Project.files(java.lang.Object[]). Они включают в себя экземпляры Callable, такие как замыкания, разрешая ленивое вычисление значений свойств. Типы FileCollection и FileTree тоже являюются типами Iterable<File>.

**То же, что и выше, File может быть любым типом, принимаемым Project.file(java.lang.Object). Тип Map может быть обернут в типы Callable, такие как замыкания.

Таблица 19.2. Дополнительные аннотации, используемые для дальнейшего уточнения аннотаций типа свойства
АннотацияОписание
@SkipWhenEmptyИспользуется вместе с @InputFiles или @InputDirectory, чтобы сказать Gradle пропустить задачу, если соответствующие файлы или папки пусты, вместе со всеми остальными входными файлами объявленными с этой аннотацией. У задач, которые были пропущены вследствие того, что все их входные файлы, объявленные с этой аннотацией, были пусты, итогом будет 'no source (нет исходных данных)'. Например, NO-SOURCE будет выведено на консоль.
@OptionalИспользуется с любой аннотацией типа свойства перечисленной в документации API Optional. Эта аннотация отключает проверки валидации для соответствующего свойства. Смотрите секцию о валидации, чтобы узнать больше.
@OrderSensitiveИспользуется с @InputFiles или @InputDirectory, чтобы сказать Gradle, что изменение в порядке файлов должно делать задачу устаревшей.

Использовать эту функциональность не рекомендуется. Она будет удалена в Gradle 4.0. Вместо этого для свойств пути к классам используйте @Classpath

@PathSensitiveИспользуется с любым свойством входного файла, чтобы сказать Gradle рассматривать важными только полученные части путей файлов. Например, если свойство аннотировано с @PathSensitive(PathSensitivity.NAME_ONLY), тогда перемещение файлов без изменения их содержимого, не сделает задачу устаревшей.

Аннотации наследуются от всех родительских типов, включая реализации интерфейсов. Аннотации типа свойства перезаписывают другие аннотации типа свойства, объявленные в родительском типе. Таким образом, свойство @InputFile может стать свойством @InputDirectory в дочернем типе задачи.

Аннотации на свойстве, объявленные в типе, перезаписывают похожие аннотации, объявленные в суперклассе и любых реализуемых интерфейсах. Аннотации суперкласса приоритетнее аннотаций, объявленных в реализуемых интерфейсах.

Аннотации Console и Internal из таблицы - особые случаи, так как они не объявляют ни входы задачи, ни выходы. Так зачем же их использовать? Они помогают вам использовать плагин Java Gradle Plugin Development при разработке и публикации своих плагинов. Этот плагин проверяет есть ли свойства у ваших классов задачи, у которых отсутствуют аннотации инкрементальной сборки. Это защищает вас от того, что вы можете забыть добавить соответствующую аннотацию во время разработки.

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

API времени выполнения

Когда у вас нет доступа к исходным кодам пользовательского класса задачи, нет способа добавить аннотации, которые мы рассмотрели в прошлом разделе. К счастью, для таких случаев Gradle предоставляет API времени выполнения. Оно может быть использовано и для специализированных задач, которые вы увидите дальше.

Использование API для специализированных задач.

Это API предоставляет через пару хорошо названных именованных свойств, которые доступны в каждой задаче Gradle:

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

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

Пример 19.24. Специализированная задача

build.gradle

task processTemplatesAdHoc {
    inputs.property("engine", TemplateEngineType.FREEMARKER)
    inputs.files(fileTree("src/templates"))
    inputs.property("templateData.name", "docs")
    inputs.property("templateData.variables", [year: 2013])
    outputs.dir("$buildDir/genOutput2")

    doLast {
        // Process the templates here
    }
}
	  

Вывод команды gradle processTemplatesAdHoc

> gradle processTemplatesAdHoc
:processTemplatesAdHoc

BUILD SUCCESSFUL
	  

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

Все определения входов и выходов сделаны через методы объектов inputs и outputs, такие как property(), files(), и dir(). Gradle выполняет проверки на устаревание для значений аргументов, чтобы вычислить надо ли запускать задачу снова или нет. Каждый метод соответствует одной из аннотаций инкрементальной сборки, например, свойство inputs.property() соответствует @Input и outputs.dir() соответствует @OutputDirectory. Единственное отличие состоит в том, что методы file(), files(), dir() и dirs() не проверяют тип объекта файла по указанному пути (файл или папка) в отличие от аннотаций.

Одно заметное отличие между API времени выполнения и аннотациями - отсутствие метода, который прямо соответствуе @Nested. Вот почему в примере используется два объявления property() для данных шаблонов, по одному для каждого свойства TemplateData. Вам стоит использовать такую же технику, когда используете API времени выполнения с вложенными значениями.

Использование API времени выполнения для пользовательских типов задачи.

Другой тип примера связан с добавлением определений входных и выходных данных к экземпляру пользовательского класса задачи, у которого отсутствуют требуемые аннотации. Например, представим, что задача ProcessTemplates предоставлена нам плагинов и у нее отсутствуют аннотации инкрементальной сборки. Чтобы исправить этот недостаток, вы можете воспользоваться API времени выполнения:

Пример 19.25. Использование API времени выполнения с пользовательским типом задачи

build.gradle

task processTemplatesRuntime(type: ProcessTemplatesNoAnnotations) {
    templateEngine = TemplateEngineType.FREEMARKER
    sourceFiles = fileTree("src/templates")
    templateData = new TemplateData("test", [year: 2014])
    outputDir = file("$buildDir/genOutput3")

    inputs.property("engine",templateEngine)
    inputs.files(sourceFiles)
    inputs.property("templateData.name", templateData.name)
    inputs.property("templateData.variables", templateData.variables)
    outputs.dir(outputDir)
}
	  

Вывод команды gradle processTemplatesRuntime

> gradle processTemplatesRuntime
:processTemplatesRuntime

BUILD SUCCESSFUL
	  

Вывод команды gradle processTemplatesRuntime

> gradle processTemplatesRuntime
:processTemplatesRuntime UP-TO-DATE

BUILD SUCCESSFUL
	  

Как вы можете видеть, мы настраиваем свойства задачи и используем эти свойства как аргументы для API времени выполнения инкрементальной сборки. Использование его в таком виде немного похоже на использование doLast() и doFirst() для добавления дополнительных действий к задаче, за исключением того, что мы прикрепляем информацию о входных и выходных данных. Обратите внимание, что если тип задачи уже использует аннотации инкрементальной сборки, API времени выполнения добавит входы и выходы вместо их замены.

Тонкая настройка

Методы API времени выполнения позволяют только объявлять ваши входы и выходы. Однако, методы, ориентированные на файлы, возвращают построитель - типа TaskInputFilePropertyBuilder - который позволяет вам предоставлять дополнительную информацию об этих входах и выходах.

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

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

Пример 19.26. Использование skipWhenEmpty() посредством API времени выполнения

build.gradle

task processTemplatesRuntimeConf(type: ProcessTemplatesNoAnnotations) {
    // ...
    sourceFiles = fileTree("src/templates") {
        include "**/*.fm"
    }

    inputs.files(sourceFiles).skipWhenEmpty()
    // ...
}
	  

Вывод команды gradle clean processTemplatesRuntimeConf

> gradle clean processTemplatesRuntimeConf
:processTemplatesRuntimeConf NO-SOURCE

BUILD SUCCESSFUL
	  

Метод TaskInputs.files() возвращает построитель, у которого есть метод skipWhenEmpty(). Выполнение этого метода эквивалентно аннотированию свойства с @SkipWhenEmpty.

До Gradle 3.0 вы должны были использовать методы TaskInputs.source() и TaskInputs.sourceDir() для получения такого же поведения как при использовании skipWhenEmpty(). Использование эти методов сейчас не рекомендуется и они не должны использоваться с Gradle 3.0 и выше.

Теперь, когда вы видели и аннотации, и API времени выполнения, вы должны быть гадаете, какое API стоит использовать. Наша рекомендация - использовать аннотации, когда это возможно и иногда создание пользовательского класса задачи, чтобы воспользоваться аннотациями, стоит того. API времени выполнения больше подходит в тех ситуациях, когда вы не можете использовать аннотации.

Важны полезные побочные эффекты

Как только вы объявили формальные входы и выходы задачи, Gradle может вывести некоторые факты об этих свойствах. Например, если на вход одной задачи подается выход другой, значит, что первая задача зависит от второй, разве нет? Gradle знает это и может действовать основываясь на этом.

Дальше мы посмотрим на эту возможность и на некоторые другие, которые следуют из того, что Gradle знает кое-что об входах и выходах.

Выведенные зависимости задач

Рассмотрим задачу архивирования, которая упаковывает выходные данные задачи processTemplates. Автор сборки увидит, что задача архивирования очевидно требует, что processTemplates запустилась первой и может явно добавить dependsOn. Однако, если вы определите задачу архивирования так:

Пример 19.27. Выведенная зависимость задачи через выходные данные

build.gradle

task packageFiles(type: Zip) {
    from processTemplates.outputs
}
	  

Вывод команды gradle clean packageFiles

> gradle clean packageFiles
:processTemplates
:packageFiles

BUILD SUCCESSFUL
	  

Gradle автоматически сделает packageFiles зависящей от processTemplates. Он может это сделать, потому что знает, что входные данные packageFiles требуют выходные данные задачи processTemplates. Мы называем это - выведенная зависимость задачи.

Приведенный выше пример можно написать так

Пример 19.28. Выведенная зависимость задачи через аргумент

build.gradle

task packageFiles2(type: Zip) {
    from processTemplates
}
	  

Вывод команды gradle clean packageFiles2

> gradle clean packageFiles2
:processTemplates
:packageFiles2

BUILD SUCCESSFUL
	  

Такое возможно, потому что метод from() может принимать объект задачи в качестве аргумента. За кулисами, from() использует метод project.files() для оборачивания аргумента, который в свою очередь делает видимыми формальные выходы задачи как коллекцию файлов. Другими словами, это особый случай!

Проверка входных и выходных данных

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

  • @InputFile - проверяет, что у свойства есть значение и что путь соответствует файлу (не папке), который существует.
  • @InputDirectory - тоже самое, что и @InputFile, за исключением того, что путь должен соответствовать папке.
  • @OutputDirectory - проверяет, что путь не соответствует файлу и создает папку, если она не существует.

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

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

Непрерывная сборка

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

Больше вы можете узнать в Главе 9 Непрерывная сборка.

19.9.2. Продвинутые техники.

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

Добавление ваших собственных кэшируюших входы/выходы методов

Не приходилось ли вам задумываться как работает метод from() задачи Copy? Он не аннотирован @InputFiles и все еще любые файлы, переданные в него, рассматриваются как формальные входные данные задачи. Что происходит?

Реализация достаточно проста и вы можете использовать такую же технику для своих задач, чтобы улучшить их API. Пишите ваши методы так, чтобы они добавляли файлы напрямую в соответственно аннотированное свойство. В качестве примера, ниже показано как добавить метод sources() к пользовательскому классу ProcessTemplates, представленному ранее:

Пример 19.29. Объявление метода для добавления входных данных задачи

buildSrc/src/main/java/org/example/ProcessTemplates.java

public class ProcessTemplates extends DefaultTask {
    // ...
    private FileCollection sourceFiles = getProject().files();

    @SkipWhenEmpty
    @InputFiles
    @PathSensitive(PathSensitivity.NONE)
    public FileCollection getSourceFiles() {
        return this.sourceFiles;
    }

    public void sources(FileCollection sourceFiles) {
        this.sourceFiles = this.sourceFiles.plus(sourceFiles);
    }

    // ...
}
	  

build.gradle

task processTemplates(type: ProcessTemplates) {
    templateEngine = TemplateEngineType.FREEMARKER
    templateData = new TemplateData("test", [year: 2012])
    outputDir = file("$buildDir/genOutput")

    sources fileTree("src/templates")
}
	  

Вывод команды gradle processTemplates

> gradle processTemplates
:processTemplates

BUILD SUCCESSFUL
	  

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

Если мы хотим поддерживать задачи в качестве аргументов и рассматривать их выходные данные как входные, мы можем использовать метод project.files() приблизительно так:

Пример 19.30. Объявление метода для добавления задачи как входа

buildSrc/src/main/java/org/example/ProcessTemplates.java

// ...
public void sources(Task inputTask) {
    this.sourceFiles = this.sourceFiles.plus(getProject().files(inputTask));
}
// ...
	  

build.gradle

task copyTemplates(type: Copy) {
    into "$buildDir/tmp"
    from "src/templates"
}

task processTemplates2(type: ProcessTemplates) {
    // ...
    sources copyTemplates
}
	  

Вывод команды gradle processTemplates2

> gradle processTemplates2
:copyTemplates
:processTemplates2

BUILD SUCCESSFUL
	  

Такая техника может сделать вашу задачу более легкой для использования и в результате сборочные файлы более чистыми. В качестве дополнительной выгоды, наше использование getProject().files() означает, что наш пользовательский метод может установить выведенную зависимость задачи.

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

Связывание @OutputDirectory с @InputFiles

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

К сожалению, такой подход не сработает, когда вам надо, чтобы файлы в @OutputDirectory (типа File) задачи стали исходными файлами для @InputFiles (типа FileCollection) другой задачи. Так как у них разные типы, установка свойства не сработает.

В качестве примера, представьте, что вам надо использовать выходные данные задачи компиляции Java - посредством свойства destinationDir - в качестве входных в пользовательской задачи, которая работает с набором файлов, содержащих байт-код Java. У этой пользовательской задачи, которую мы назовем Instrument, есть свойство classFiles аннотированное @InputFiles. Изначально вы можете попробовать настроить задачу вот так:

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

build.gradle

apply plugin: "java"

task badInstrumentClasses(type: Instrument) {
    classFiles = fileTree(compileJava.destinationDir)
    destinationDir = file("$buildDir/instrumented")
}
	  

Вывод команды gradle clean badInstrumentClasses

> gradle clean badInstrumentClasses
:clean UP-TO-DATE
:badInstrumentClasses NO-SOURCE

BUILD SUCCESSFUL
	  

Ничего неправильно, на первый взгляд, в этом коде нет, но из вывода консоли вы можете видеть, что задача компиляции не была выполнена. В этом случае вам надо добавить явную зависимость между instrumentClasses и compileJava посредством dependsOn. Использование fileTree() означает, что Gradle сам не можете вывести зависимость задачи.

Одно из решений, использовать свойство TaskOutputs.files, как показано в следующем примере:

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

build.gradle

task instrumentClasses(type: Instrument) {
    classFiles = compileJava.outputs.files
    destinationDir = file("$buildDir/instrumented")
}
	  

Вывод команды gradle clean instrumentClasses

> gradle clean instrumentClasses
:clean UP-TO-DATE
:compileJava
:instrumentClasses

BUILD SUCCESSFUL
	  

В качестве альтернативы, вы можете сделать так, чтобы Gradle сам получил доступ к соответствующему свойству используя метод project.files() вместо project.fileTree():

Пример 19.33. Установка выведенной зависимости с помощью метода files()

build.gradle

task instrumentClasses2(type: Instrument) {
    classFiles = files(compileJava)
    destinationDir = file("$buildDir/instrumented")
}
	  

Вывод команды gradle clean instrumentClasses2

> gradle clean instrumentClasses2
:clean UP-TO-DATE
:compileJava
:instrumentClasses2

BUILD SUCCESSFUL
	  

Помните, что files() может принимать задачи в качестве аргументов, тогда как fileTree() не может.

Недостаток данного подхода состоит в том, что все файлы, полученные на выходе исходной задачи, становятся входными файлами целевой - в данном случае, instrumentClasses. Все работает хорошо до тех пор, пока у исходной задачи только одиночный основанный на файлах вывод, как у задачи JavaCompile. Но, если вам надо связать только одно выходное свойство среди нескольких, тогда необходимо явно сказать Gradle, какая задача генерирует входные файлы, используя метод builtBy:

Пример 19.34. Установка выведенной зависимости с помощью метода builtBy()

build.gradle

task instrumentClassesBuiltBy(type: Instrument) {
    classFiles = fileTree(compileJava.destinationDir) {
        builtBy compileJava
    }
    destinationDir = file("$buildDir/instrumented")
}
	  

Вывод команды gradle clean instrumentClassesBuiltBy

> gradle clean instrumentClassesBuiltBy
:clean UP-TO-DATE
:compileJava
:instrumentClassesBuiltBy

BUILD SUCCESSFUL
	  

Конечно, вы можете просто добавить явную зависимость на задачу посредством dependsOn, но приведенный выше подход предоставляет более семантическое значение, объясняя почему compileJava должна запускаться раньше.

Предоставление пользовательской логики устаревания

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

В таких случаях на сцену выходит метод upToDateWhen() у TaskOutputs. Он принимает на вход предикат, который используется для вычисления свежая ли задача или нет. Один из вариантов использования, совсем отключить проверку на устаревание у задачи, вот так:

Пример 19.35. Игнорирование проверок на устаревание

build.gradle

task alwaysInstrumentClasses(type: Instrument) {
    classFiles = files(compileJava)
    destinationDir = file("$buildDir/instrumented")
    outputs.upToDateWhen { false }
}
	  

Вывод команды gradle clean alwaysInstrumentClasses

> gradle clean alwaysInstrumentClasses
:compileJava
:alwaysInstrumentClasses

BUILD SUCCESSFUL
	  

Вывод команды gradle alwaysInstrumentClasses

> gradle alwaysInstrumentClasses
:compileJava UP-TO-DATE
:alwaysInstrumentClasses

BUILD SUCCESSFUL
	  

Замыкание { false } гарантирует, что copyResources всегда выполнит копирование, независимо от того были ли изменения во входных или выходных данных.

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

Распространенная ошибка использовать upToDateWhen() вместо Task.onlyIf(). Если вы хотите пропустить задачу на основании какого-либо условия, не связанного с входными или выходными данными задачи, тогда вам стоит использовать onlyIf(). Например, в случаях, когда вам нужно пропустить задачу при условии, что определенное свойство установлено или нет.

19.9.3. Как это работает?

До того, как задача выполнится в первый раз, Gradle делает снимок входов. Этот снимок содержит пути к входным файлам и хэши содержимого каждого файла. Затем Gradle выполняет задачу. Если задача успешно завершается, он делает снимок выходных данных. В этом снимке содержится набор выходных файлов и хэши содержимого каждого файла. Gradle сохраняет оба снимка для следующего выполнения задачи.

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

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

Gradle понимает, если файловое свойство (например то, в котором находится путь к классам Java) зависит от порядка его элементов. При сравнении снимка такого свойства, даже изменение в порядке файлов приведет к устареванию задачи.

Обратите внимание, что, если у задачи определена выходная папка, любые файлы добавленные в нее со времени последнего выполнения игнорируются и не служат причиной устаревания задачи. Это сделано для того, чтобы несвязанные задачи могли иметь общую папку без влияния друг на друга. Если по какой-либо причине вам требуется другое поведение, рассмотрите использование TaskOutputs.upToDateWhen(groovy.lang.Closure).

19.10. Правила задач.

Иногда вам требуется задача, чье поведение зависит от огромного или бесконечного числа значений параметров. Хороший и выразительный способ предоставить такие задачи - правила задач:

Пример 19.36. Правило задачи

build.gradle

tasks.addRule("Pattern: ping") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
}
	  

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

> gradle -q pingServer1
Pinging: Server1
	  

Параметр String используется в качестве описания для правила, которое отображается при выполнение команды gradle tasks.

Правила используются не только при вызове задач из командной строки. Вы можете создавать dependsOn связи на задачи построенные на правилах:

Пример 19.37. Зависимость на задачу построенные на правилах

build.gradle

tasks.addRule("Pattern: ping") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
}

task groupPing {
    dependsOn pingServer1, pingServer2
}
	  

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

> gradle -q groupPing
Pinging: Server1
Pinging: Server2
	  

Если вы запустите 'gradle -q tasks' вы не найдете задач с именами 'pingServer1' или 'pingServer2', но этот скрипт выполнит логику, которая основана на запросе на запуск таких задач.

19.11. Задачи-финализаторы.

Задачи-финализаторы - инкубационная функция.

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

Пример 19.38. Добавление финализатора задачи

build.gradle

task taskX {
    doLast {
        println 'taskX'
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}

taskX.finalizedBy taskY
	  

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

> gradle -q taskX
taskX
taskY
	  

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

Пример 19.39. Финализатор задача для задачи завершающейся с ошибкой

build.gradle

task taskX {
    doLast {
        println 'taskX'
        throw new RuntimeException()
    }
}
task taskY {
    doLast {
        println 'taskY'
    }
}

taskX.finalizedBy taskY
	  

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

> gradle -q taskX
taskX
taskY
	  

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

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

Для указания задачи-финализаторы, используйте метод Task.finalizedBy(java.lang.Object[]). Этот мтоде принимает экземпляр задачи, имя задачи или другие данные, принимаемые Task.dependsOn(java.lang.Object[]).

19.12. Заключение.

Если вы пришли с Ant, расширенные задачи Gradle, наподобие Copy, кажутся вам пересечением между целями и задачами Ant. Хотя задачи и цели Ant действительно различные сущности, Gradle объединяет эти идеи в единую сущность. Просты задачи Gradle похожи на цели Ant, но расширенные также включают аспекты задач Ant. Все задачи разделяют общее API и вы можете создавать зависимости между ними. Такие задачи проще настраивать в отличие от задач Ant. Они полностью используют систему типов, а также более выразительные и легкий в поддержке.