Gradle: Better Way To Build. Gradle книга


Почему Gradle? / Хабр

Если вы еще не знаете, что такое Gradle, то о нём можно почитать в предыдущих двух обзорах:
  1. Gradle: Better Way To Build
  2. Gradle: Tasks Are Code
Не так давно c Maven на Gradle была переведена сборка Hibernate Core. На что информационное сообщество отреагировало неоднозначно. Я хочу представить вашему вниманию перевод статьи, посвященной миграции сборки Hibernate. В статье раскрываются причины принятия такого решения, описываются преимущества Gradle и проблемы с Maven2. Дальнейшее повествование ведётся от лица Steve Ebersole.
Почему Gradle?
Многие просили меня записать причины, по которым я хочу перенести сборку Hibernate c Maven на Gradle, и здесь я их перечислю. Если вы впервые сталкиваетесь с Gradle, есть смысл просмотреть обзор. Прежде всего, хочу подчеркнуть, что цель этой статьи не «бей – круши — Maven» и не прямое сравнение Gradle с Maven. Это просто способ поведать миру о проблемах и разочарованиях, с которыми я столкнулся за 2.5+ года использования Maven для сборки Hiberbnate. Во многих случаях причина в несоответствии соглашений, используемых в Maven, тому, как я представляю себе сборку Hibernate. Некоторые пункты списка, собранного Полом, актуальны только для Hibernate (на этот список тоже стоит взглянуть). Так же это способ объяснить, почему я предпочел Gradle другим новым инструментам сборки (Buildr, SBT, и т.д.). Кстати, есть еще wiki сравнение Gradle и Maven, но оно изрядно устарело во многих частях. Особенно в том, что касается Gradle.

Вот проблемы (скорее в хронологическом порядке, чем «по важности»), с которыми я столкнулся в Maven:

  1. Maven не поддерживает многомодульные сборки. Утверждается, что поддерживает, но это не совсем точно. Если быть точным, он поддерживает сбоку группы независимых проектов. В чем разница? Для примера, возьмем Hibernate. У нас есть набор модулей, которые мы собираем. Модули существуют исключительно с целью разделения зависимостей соответственно используемым функциям. Не используете ehcache для кэширования? Без проблем, не используйте hibernate-ehcache модуль и ehcache не окажется в ваших зависимостях. Мы не собираемся никогда раздельно поставлять эти модули. Тогда зачем нам нужно задавать версию проекта снова и снова (во всех модулях)? Вот пример того, как maven усложняет простое. А последствия ощущаются когда, к примеру, я хочу сделать релиз. Я должен вручную убедиться, что версии совпадают во множестве разных мест (и даже не начинайте про release plugin)
  2. Ну ладно, раз начал про release plugin. Он вообще бесполезен. Я мог заставить его более-менее прилично работать на небольших проектах, но с Hibernate он практически всегда «падал». Хуже того, приходится ждать по 30-40-60 минут, чтобы узнать, что он не сработает, потом исправлять то, что ему не понравилось (если удалось это выяснить), и запускать по новой. В конце концов, мы просто сдались. (Кстати, друг только что заметил, что Jason van Zyl, низверг release plugin в седьмой круг ада, и что он будет заменен на «что-то новенькое»)
  3. Hibernate-core — основной артефакт. Hibernate-Annotations зависит от него. Maven об этом знает. Ну и почему тогда я не могу просто зайти в каталог hibernate-annotations и запустить 'maven compile', чтобы hibernate-core автоматически собрался тоже? (подсказка: см. п. 1) В любом случае: `cd hibernate-core; mvn install; cd ../hibernate-annotations; mvn compile`- ни в какие ворота не лезет. Инстумент сборки должен сам это понимать, после того как я преодолел столько препятствий, объясняя ему природу этих зависимостей.
  4. Один артефакт на проект. Я знаю, что это хорошее правило. Но Хорошие Правила редко бывают Абсолютными Истинами. И к примеру это ограничение Maven вынудило нас вынести тесты для hibernate-core за пределы hibernate-core. Потому что нам был нужен hibernate-testing, который зависит от hibernate-core и задает общую среду исполнения тестов и используется в большинстве других модулей (так же, мы хотели сделать его доступным для пользователей). И теперь у нас постоянно происходят коммиты, ломающие тесты, потому, что разработчики (включая и меня) забывают запустить тесты из-за того, что они лежат в каком-то другом, совершенно отдельном модуле. Да, в основном это проблема внимания к деталям, но разве не в решении таких вопросов должен помогать инструмент сборки вместо того, чтобы их создавать? Ну и один каталог исходников и один каталог скомпилированных классов на проект. См. http//in.relation.to/Bloggers/SimultaneouslySupportingJDBC3AndJDBC4WithMaven
  5. Я, лично, терпеть не могу концепцию объединения «inherited pom data» и «module aggregation» в одном файле. Это ужасная мысль. Многие maven разработчики согласны со мной и помогли разработать схему, при которой Hibernate разносит «inherited pom data» в ./parent/pom.xml, а «module aggregation» в ./pom.xml. Отлично, maven это поддерживает… ну почти отлично: он почти поддерживает. Проблема в том, что многие plugin'ы это не поддерживают, и начинаются мелкие, заковыристые проблемы. Зачем тогда вообще разносить? Как я уже сказал, я не считаю объединение правильным. Но почему бы не переступить через «правильно» и не сделать по-другому, т.к. Maven работает по-другому? Проблема в настройках после первоначального checkout. Вы не можете собрать модуль, пока нет его родителя. Но если родитель — агрегатор, то единственный способ собрать модуль — установить весь проект (или знать ключи команд maven и отключить рекурсию)
  6. В самом начале, 2.5 года назад, мне пришлось столкнуться с нашей сборкой DocBook. Кристиан своей работой над сборкой DocBook с использованием Ant в Hibernate задал планку для многих open-source Java проектов. У меня были мысли, как это улучшить, используя концепцию управления зависимостями для скачивания нужных компонент по мере необходимости. Проблема в том, что для того, чтобы сделать что-либо похожее в Maven вам придется написать plugin. Нельзя «просто накидать скрипт, а потом, если он окажется полезным, я упакую его для других» (Я понимаю, что сейчас это в некоторой степени возможно при помощи GMaven и т.д., но тогда это было не так). Так что пришлось писать jDocBook plugin и еще несколько. И со всеми ними были проблемы. Это единственный раз, когда я прямо сравню Gradle и Maven, потому что Gradle «попал в яблочко». Писать plugin'ы в Gradle (я написал уже 2 довольно крупных) очень приятно. В некотором смысле, это непросто из-за отсутствия инструкций о том, как это делать правильно, но API и возможности, которые предоставлены, просто восхитительны (да и не то, чтобы у Maven была документация по «правильным plugin'ам»).
  7. Многие пользователи хотели бы, чтобы скрипты сборки были настроены на взаимодействие с JBoss maven репозиторием. Вполне логично: сборка должна работать «из коробки». Но в Maven так сделать нельзя потому, что pom файл, используемый для сборки, будет использован и для установки в репозиторий. А большинство репозиториев (в том числе JBoss и Maven Central) проверяют, что pom-файлы не содержат настроек репозитория. Так что, вместо настройки скриптов, нам приходится поддерживать wiki и docbook и отсылать пользователей к документации.
  8. Запуск тестов Hibernate поверх нескольких баз данных практически невозможен через IDE, в IntelliJ как минимум с использованием интеграции. Проблема в профилях и том, как интеграция взаимодействует с ними (в плане фильтрации ресурсов)

Итак, пришло время начать поиски лучшего способа сборки Hibernate. Да, на горизонте маячил Maven3. Да, он добавляет некоторую «поддержку скриптов», но это скорее «рюшечка». Насколько мне известно, она просто позволяет по-другому настраивать все тоже самое. Поэтому я начал изучать Gradle среди прочих «DSL, build-by-convention» инструментов. (Кстати, «рюшечка» эта называется Polyglot Maven и, по словам друга, Jason van Zyl заявил, что она будет убрана и заменена на «что-то еще»).

  1. Первым и главным преимуществом стала возможность создавать скрипты для тех частей сбоки, которые слишком сложно описать в терминах «build by convention». Эта смесь мне понравилась: в вашем распоряжении лучшее от обоих миров.
  2. Столкнувшись с трудностями с доступом к информации из других модулей при сборке [с использованием Maven2 — прим. пер.], я понял, что возможность написать перебор модулей сборки с использованием замыканий во многих случаях просто «дар Божий». Так же как и способ настроить стандартные параметры для всех подпроектов в главном скрипте gradle — шедевр, намного превосходящий концепцию наследования в Maven.
  3. Общая гибкость в настройках сборки и каталогов так, как нужно мне, без необходимости следовать ограничениям системы сборки — это прекрасно
  4. Вся концепция процесса сборки в Gradle намного чище, imho. В Gradle можно не только задавать зависимости между модулями, но и невероятно гибко описывать зависимости от задач, модулей, каталогов и т.д. Например, в Gradle уже можно сказать, что один модуль зависит от скомпилированных классов, а не от результата (jar) сборки другого. Задумайтесь над этим. Очень и очень гибко. И полезно!
  5. У каждого проекта или модуля может быть несколько «Source set». «Source set» задает набор каталогов с исходниками, набор целевых каталогов, каталогов с ресурсами, и т.п… С помощью этого, к примеру, можно сделать вариант параллельной поддержки и тестирования JDBC3/JDBC4 такой, как я хотел (см in.relation.to/Bloggers/SimultaneouslySupportingJDBC3AndJDBC4WithMaven)
  6. Не знаю, к чему это отнести, но так приятно думать «Как же я буду это делать?» вместо «Какие варианты достичь желаемого оставит мне система сборки?»
  7. Инкрементальная сборка — это прекрасно. Как это? Она понимает, когда что-то изменилось, а когда — нет, и когда части сборки нужны на самом деле. Это встроено в plugin'ы. Задача определяет ввод и вывод, а Gradle использует эту информацию, чтобы определить, нужно ли запустить задачу, т.к. ее ввод изменился, и обновила ли задача свой вывод и поэтому нужно запустить зависящие от нее задачи. В итоге, последующие сборки становятся очень быстрыми
  8. Способность Gradle публиковать артефакты в maven-репозитории и генерировать точные POM'ы не только уникальна, но была и важным аргументом принятия решения о переходе. У меня могут быть претензии к сборке Hibernate при помощи Maven, но возможность создавать и использовать артефакты унифицированным образом крайне необходима. В общем случае, Gradle способен сгенерировать POM по той информации, которой располагает. А если нет, вы можете просто описать дополнительные параметры в замыкании для настройки POM. Это к вопросу о гибкости.
  9. Генерация проектов IDE. Это был еще один важный аргумент. Большинство разработчиков Hibernate используют Eclipse или IntelliJ. Лично я провожу большую часть времени в IDE: пишу код, запускаю тесты, оформляю документацию. Я не собирался идти путём усложнения работы в IDE. Уже довольно давно Gradle может создавать проекты под Eclipse. Недавно появилась возможность генерировать проекты и под IntelliJ. Для нее, говорят, разрабатывается и интеграция gradle, аналогичная maven

habr.com

Tasks Are Code / Хабр

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

Теперь я хотел бы рассказать о том, чем Gradle зацепил лично меня. Речь пойдёт о способах работы с задачами. Задача в Gradle — близкий аналог Ant Target. И, чтобы не путаться в терминах, под задачей (или task) далее по тексту всегда будет подразумеваться Gradle Task. Если речь будет идти о сущности из Ant, то это будет указано явно: Ant task.

Так вот, задачи в Gradle создаются при помощи специального dsl (domain specific language) на основе Groovy. И возможности, которые этот dsl предоставлет, на мой взгляд, почти безграничны в сравнении с ant или maven.

Начнем, пожалуй, с традиционного для программистов «Hello World». Пусть у нас есть пустой файл build.gradle. Пишем:

task hello << { println 'Hello world!' } Запускаем

>gradle -q hello Hello world! Bingo! Но не впечатляет. Давайте попробуем кое-что еще:

task upper << { String someString = 'mY_nAmE' println "Original: " + someString println "Upper case: " + someString.toUpperCase() 4.times { print "$it " } } Запускаем

>gradle -q upper Original: mY_nAmE Upper case: MY_NAME 0 1 2 3 То есть внутри определения задачи может находиться произвольный код на Groovy. И сами задачи — полноценный объект Groovy. А это значит, что у них есть свойства и методы, которые позволяют ими управлять. Например, добавляя новые действия.

Давайте посмотрим на более интересный пример.

Пусть у нас есть небольшой java проект. Вот его build.gradle:

apply plugin: 'java' version = '1.0' repositories { mavenCentral() } dependencies { compile group: 'commons-collections', name: 'commons-collections', version: '3.2' testCompile group: 'junit', name: 'junit', version: '4.7' } и структура каталогов /projectAlpha /src /test /main /java /my /own /code /spring /db /plugin /auth

Ничего сложного: версия, репозиторий Maven Central, две зависимости для компиляции, несколько packages. При запуске команды

>gradle build

В каталоге projectAlpha/build/libs будет собран архив projectAlpha-1.0.jar. Все в полном соответствии с соглашениями. Maven сделал бы все точно так же.

Но с течением времени требования, как известно, меняются. Изменим требования в примере. Пусть нам понадобилось собрать Spring-related код в отдельные архивы, собрать отдельный jar с классами unit-тестов, и еще один jar с исходниками. В Gradle это решается следующим образом:

task sourcesJar(type: Jar) { appendix = 'sources' from sourceSets.main.allJava } task testJar(type: Jar) { appendix = 'test' from sourceSets.test.classes } jar { exclude 'my/spring/**' } task springDbJar(type: Jar) { appendix = 'spring-db' from sourceSets.main.classes include 'my/spring/db/**' } task springAuthJar(type: Jar) { appendix = 'spring-auth' from sourceSets.main.classes include 'my/spring/auth/**' } task springPluginJar(type: Jar) { appendix = 'spring-plugin' from sourceSets.main.classes include 'my/spring/plugin/**' } Запускаем>gradle assemble

И видим:projectAlpha>dir /b build\libs projectAlpha-1.0.jar projectAlpha-sources-1.0.jar projectAlpha-spring-auth-1.0.jar projectAlpha-spring-db-1.0.jar projectAlpha-spring-plugin-1.0.jar projectAlpha-test-1.0.jar

Что произошло:

  • Мы определили две новых задачи с типом Jar: sourcesJar и testJar. Для описания содержимого архива используются уже знакомые вам source Sets. Еще задается атрибут appendix, который, как нетрудно догадаться, будет включен в имя архива после версии.
  • Мы изменили заданную по умолчанию задачу jar (она определяется в plugin'е) таким образом, чтобы в основной архив не попадали классы из определённых packages.
  • Мы определили еще 3 задачи для сборки трёх отдельных архивов с модулями для Spring.При вызове задачи assemble система сборки самостоятельно выбрала все задачи, формирующие архивы (Zip, Jar..), и выполнила их. Предварительно обработав зависимости от source sets и скомпилировав нужные классы, как и в предыдущей статье.
Интересно, а как это сделать в Maven?

Но жизнь не стоит на месте, и наши требования продолжают меняться. В одно прекрасное утро, Spring Foundation потребовали добавлять в манифест каждого jar, который имеет отношение к Spring и публикуется на Хабре, атрибут demo со значением habr.ru. Звучит странно, но нам все равно нужно их реализовывать. Добавим:

tasks.withType(Jar).matching { task -> task.archiveName.contains('spring') }.allObjects { task -> task.manifest { attributes demo: 'habr.ru' } } Запустим:

projectAlpha>gradle assemble :compileJava UP-TO-DATE :processResources UP-TO-DATE :classes UP-TO-DATE :jar UP-TO-DATE :sourcesJar UP-TO-DATE :springAuthJar :springDbJar :springPluginJar :compileTestJava UP-TO-DATE :processTestResources UP-TO-DATE :testClasses UP-TO-DATE :testJar UP-TO-DATE :assemble

Обратите внимание на то, что многие задачи были отмечены UP-TO-DATE. Это еще одна изюминка Gradle — инкрементальная сборка. Но о ней в другой раз. Теперь если не полениться и посмотреть на содержимое манифестов архивов, то в относящихся к Spring можно обнаружить нужную строчку

Manifest-Version: 1.0 demo: habr.ru Bingo!

Но требования Spring Foundation продолжают меняться. И теперь уже нужно рядом с каждым jar положить его контрольную сумму :) Лицензионная чистота — дело нешуточное, и мы вынуждены подчиниться. К сожалению, в Gradle нет встроенной поддержки операции вычисления MD5. Зато она есть в Ant. Ну так давайте ее и используем. Изменим последний фрагмент следующим образом:

def allSpringJars = tasks.withType(Jar).matching { task -> task.archiveName.contains('spring') } allSpringJars.allObjects { task -> configure(task) { manifest { attributes demo: 'habr.ru' } doLast { ant.checksum(file: archivePath, todir: archivePath.parentFile) } } } task springJars(dependsOn: allSpringJars)

И соберем на этот раз только злосчастные spring-related архивы:

projectAlpha>gradle clean springJars :clean :compileJava :processResources UP-TO-DATE :classes :springAuthJar :springDbJar :springPluginJar :springJars

BUILD SUCCESSFUL

Total time: 5.015 secs

Посмотрим, что получилось.c:\Work\Gradle\tasksAreCode\projectAlpha>dir /b build\libs projectAlpha-spring-auth-1.0.jar projectAlpha-spring-auth-1.0.jar.MD5 projectAlpha-spring-db-1.0.jar projectAlpha-spring-db-1.0.jar.MD5 projectAlpha-spring-plugin-1.0.jar projectAlpha-spring-plugin-1.0.jar.MD5

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

В этой статье нам удалось с помощью небольшого объема Groovy-кода подстроиться под изменения требований и выполнить несколько задач, с которыми трудно было бы справиться средствами Ant или Maven. Использование гибкого языка программирования вместо xml развязывает вам руки и позволяет самостоятельно решать, как вы хотите выполнить вашу задачу.

Продолжение следует.

habr.com

Android: Gradle

Статья проплачена кошками - всемирно известными производителями котят.

Если статья вам понравилась, то можете поддержать проект.

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

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

Запустите любой ваш проект в Android Studio для дальнейших опытов. Откройте файл build.gradle, который относится к модулю app.

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

task hello_kitty

После любого изменения файла синхронизуруйтесь.

Мы объявили задачу по одному только имени. На правой стороне Android Studio имеется вертикальная вкладка Gradle, которую можно развернуть. Она содержит список задач (task), которая выполняет Gradle при работе с текущим проектом. Вы можете выделить любую из этих задач и запустить её двойным щелчком. Можно выделить несколько задач.

Найдите свою задачу, она будет находиться по пути :app | Tasks | other. Задачи сортируются по алфавиту. Запустите задачу двойным щелчком.

Когда выполняется какая-то задача Gradle, то ход её выполнения можно увидеть в окне Gradle Console. Открыть её можно через вкладку Gradle Console в нижней правой части студии. После выполнения задачи вы увидите что-то типа такого:

Executing tasks: [hello_kitty] Configuration on demand is an incubating feature. :app:hello_kitty UP-TO-DATE BUILD SUCCESSFUL Total time: 1.569 secs

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

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

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

task hello_kitty << { println 'Hello Kitty' }

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

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

task hello_kitty hello_kitty << { print 'Hello ' } hello_kitty << { println 'Kitty' }

Сначала мы объявили задачу, затем добавили первую операцию с методом print, следом добавили вторую операцию с методом println. Результат будет таким же.

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

Расширим предыдущий пример, добавив блок конфигурации:

task hello_kitty hello_kitty << { println 'Соединяемся с базой данных' } hello_kitty << { println 'Обновляем данные' } hello_kitty { println 'Настраиваем базу данных' }

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

Настраиваем базу данных :app:hello_kitty Соединяемся с базой данных Обновляем данные

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

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

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

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

task hello_kitty hello_kitty << { println 'Соединяемся с базой данных' } hello_kitty << { println 'Обновляем данные' } hello_kitty { print 'Настраиваем ' } hello_kitty { println 'базу данных' }

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

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

Методы DefaultTask

dependsOn(task)

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

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, мы пишем код следующим образом:

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.

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

Результат выполнения

:setupDatabaseTests create schema load test data BUILD SUCCESSFUL

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

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

task setupDatabaseTests << { println 'load test data' } setupDatabaseTests { doFirst { println 'create schema' } }

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

task setupDatabaseTests << { println 'load test data' } setupDatabaseTests.doFirst { println 'create database schema' } setupDatabaseTests.doFirst { println 'drop database schema' }

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

:setupDatabaseTests drop database schema create database schema load test data BUILD SUCCESSFUL

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

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

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

// Исходное определение задачи (может быть недоступно для редактирования) task setupDatabaseTests << { println 'load test data' } // Наши изменения задачи (в файле, где мы можем вносить изменения) setupDatabaseTests { doFirst { println 'create database schema' } doFirst { println 'drop database schema' } }

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

doLast(closure)

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

task setupDatabaseTests << { println 'create database schema' } setupDatabaseTests.doLast { println 'load test data' }

Повторные вызовы 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, в котором определено только одно выражение, является функцией, возвращающей значение этого выражения.

Пример, в котором используется метод onlyIf

task createSchema << { println 'create database schema' } task loadTestData(dependsOn: createSchema) << { println 'load test data' } loadTestData.onlyIf { System.properties['load.data'] == 'true' }

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

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 в вашей задаче для отражения результатов выполнения созданного вами кода сборки:

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

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

enabled

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

Отключение задачи

task templates << { println 'process email templates' } task sendEmails(dependsOn: templates) << { println 'send emails' } sendEmails.enabled = false

path

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

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

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

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

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

Двоеточие впереди указывает на то, что задача определена на верхнем уровне файла билда. Расположение задач на верхнем уровне, однако, не является обязательным. 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 не будут выводиться в консольном окне.

Задача демонстрирует эффект применения всех уровней логирования. Несколько более сложный код 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 ' ' } }

logging

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

description

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

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

Два способа объявления поведения задачи и задания описания

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 в дальнейшем преобразует в артифакт установки. Список файлов звисит от параметров билда, но для соответствия специфическим требованиям установленного приложения, в артифакте должен храниться манифест, перечисляющий файлы. Здесь очень удобно использовать динамическое свойство:

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

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

Типы задач

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

Рассмотрим несколько важных типов с примерами использования.

Copy

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

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

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

Jar

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

Пример использования задачи 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.

Помните, вы всегда можете найти документацию, где описаны стандартные возможности Gradle, такие как задача Jar.

JavaExec

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

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

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

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

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

Пользовательская задача для выполнения запросов в базе данных 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 [email protected] 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:

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

task createDatabase(type: MySqlTask) { sql = 'CREATE DATABASE IF NOT EXISTS example' } task createUser(type: MySqlTask, dependsOn: createDatabase) { sql = "GRANT ALL PRIVILEGES ON example.* TO [email protected] 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))' }

Пример. Определение пользовательской задачи в директории 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 — отдельная тема, которую мы затрагивать не будем.

Структура проекта 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 задачи являются фундаментальными единицами процесса сборки. Про них можно рассказать намного больше, чем может охватить введение.

Использованные материалы

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

Реклама

developer.alexanderklimov.ru

управляя зависимостями / Блог компании Redmadrobot / Хабр

Управление зависимостями – одна из наиболее важных функций в арсенале систем сборки. С приходом Gradle в качестве основной системы сборки Android-проектов в части управления зависимостями произошёл существенный сдвиг, закончилась эпоха ручного копирования JAR-файлов и долгих танцев с бубном вокруг сбоящих конфигураций проекта.

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

Репозиторий
Как известно, Gradle не имеет собственных репозиториев и в качестве источника зависимостей использует Maven- и Ivy-репозитории. При этом интерфейс для работы с репозиториями не отличается на базовом уровне, более развёрнуто об отличиях параметров вы можете узнать по ссылкам IvyArtifactRepository и MavenArtifactRepository. Стоит отметить, что в качестве url могут использоваться ‘http’, ‘https’ или ‘file’ протоколы. Порядок, в котором записаны репозитории, влияет на порядок поиска зависимости в репозиториях.// build.gradle repositories { maven { url "http://example.com" } ivy { url "http://example.com" } }
Объявление зависимостей
// build.gradle apply plugin: 'java' repositories { mavenCentral() } dependencies { compile group: 'com.googlecode.jsontoken', name: 'jsontoken', version: '1.1' testCompile group: 'junit', name: 'junit', version: '4.+' }

В приведённом выше примере вы видите сценарий сборки, в котором подключены две зависимости для различных конфигураций (compile и testCompile) компиляции проекта. JsonToken будет подключаться во время компиляции проекта и компиляции тестов проекта, jUnit только во время компиляции тестов проекта. Детальнее о конфигурациях компиляции — по ссылке.

Также можно увидеть, что jUnit-зависимость мы подключаем как динамическую(+), т.е. будет использоваться самая последняя из доступных версия 4.+, и нам не нужно будет следить за минорными обновлениями (рекомендую не использовать эту возможность в compile-типе компиляции приложения, т.к. могут появиться неожиданные, возможно, сложно локализуемые проблемы).

На примере с jUnit-зависимостью рассмотрим стандартный механизм Gradle по поиску необходимой зависимости:

1. Зависимость compile ("org.junit:junit:4.+") 2. Получение версии модуля group: "org.junit" name: "junit" version: "4.+" 3. Получение списка возможных версий модуля [junit:4.1] … [junit:4.12] 4. Выбор одной версии зависимости [junit:4.12] 5. Получение версии зависимости [junit:4.12] dependencies { … } artifacts { … } 6. Присоединение артефактов зависимости к проекту junit-4.12.jar junit-4.12-source.jar junit-4.12-javadoc.zip
Кэш
В Gradle реализована система кэширования, которая по умолчанию хранит зависимости в течение 24 часов, но это поведение можно переопределить.// build.gradle configurations.all { resolutionStrategy.cacheChangingModulesFor 4, 'hours' resolutionStrategy.cacheDynamicVersionsFor 10, 'minutes' }

После того, как время, установленное для хранения данных в кэше, вышло, система при запуске задач сначала проверит возможность обновления динамических (dynamic) и изменяемых (changing) зависимостей и при необходимости их обновит.

Gradle старается не загружать те файлы, которые были загруженны ранее, и использует для этого систему проверок, даже если URL/источники файлов будут отличаться. Gradle всегда проверяет кэш (URL, версия и имя модуля, кэш других версий Gradle, Maven-кэш), заголовки HTTP-запроса (Date, Content-Length, ETag) и SHA1-хэш, если он доступен. Если совпадений не найдено, то система загрузит файл.

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

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

Более детально про кэширование зависимостей можно прочитать в Gradle User Guide.

Виды зависимостей
Существует несколько видов зависимостей в Gradle. Наиболее часто используемыми являются:

– Внешние зависимости проекта — зависимости, загружаемые из внешних репозиториев;

// build.gradle dependencies { compile "com.android.support:appcompat-v7:23.1.1" }

– Проектные зависимости — зависимость от модуля (подпроекта) в рамках одного проекта;

// build.gradle dependencies { compile project(':subproject') }

– Файловые зависимости — зависимости, подключаемые как файлы (jar/aar архивы).

build.gradle repositories { flatDir { dirs 'aarlibs' // инициализируем папку, хранящую aar-архивы как репозиторий } } dependencies { compile(name:'android_library', ext:'aar') // подключаем aar-зависимость compile files('libs/a.jar', 'libs/b.jar') compile fileTree(dir: 'libs', include: '*.jar') }

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

Дерево зависимостей
Каждая внешняя или проектная зависимость может содержать собственные зависимости, которые необходимо учесть и загрузить. Таким образом, при выполнении компиляции происходит загрузка зависимостей для выбранной конфигурации и строится дерево зависимостей, человеческое представление которого можно увидеть, выполнив Gradle task ‘dependencies’ в Android Studio или команду gradle %module_name%:dependencies в консоли, находясь в корневой папке проекта. В ответ вы получите список деревьев зависимостей для каждой из доступных конфигураций.

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

Возьмем специально подготовленные исходники репозитория, расположенного на github и попробуем получить дерево зависимостей для конкретной конфигурации (в данный момент проект находится в состоянии 0, т.е. в качестве build.gradle используется build.gradle.0):

Проанализировав дерево зависимостей, можно увидеть, что модуль app использует в качестве зависимостей две внешних зависимости (appcompat и guava), а также две проектных зависимости (first и second), которые в свою очередь используют библиотеку jsontoken версий 1.0 и 1.1 как внешнюю зависимость. Совершенно очевидно, что проект не может содержать две версии одной библиотеки в Classpath, да и нет в этом необходимости. На этом этапе Gradle включает модуль разрешения конфликтов.

Разрешение конфликтов
Gradle DSL содержит компонент, используемый для разрешения конфликтов зависимостей. Если посмотреть на зависимости библиотеки jsontoken на приведённом выше дереве зависимостей, то мы увидим их только раз. Для модуля second зависимости библиотеки jsontoken не указаны, а вывод самой зависимости содержит дополнительно ‘–> 1.1’, что говорит о том, что версия библиотеки 1.0 не используется, а автоматически была заменена на версию 1.1 с помощью Gradle-модуля разрешения конфликтов.

Для объяснения каким образом была разрешена конфликтная ситуация, также можно воспользоваться Gradle-таском dependencyInsight, например:

Стоит обратить внимание, что версия 1.1 выбирается в результате conflict resolution, также возможен выбор в результате других правил (например: selected by force или selected by rule). В статье будут приведены примеры использования правил, влияющих на стратегию разрешения зависимостей, и выполнив таск dependencyInsight вы сможете увидеть причину выбора конкретной версии библиотеки на каждом из приведённых ниже этапов. Для этого при переходе на каждый этап вы можете самостоятельно выполнить таск dependencyInsight.

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

// build.gradle // … configurations.compile.resolutionStrategy { failOnVersionConflict() }

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

У задачи есть четыре варианта решения:

Первый вариант – удалить строки, переопределяющие стратегию разрешения конфликтов.

Второй вариант – добавить в стратегию разрешения конфликтов правило обязательного использования библиотеки jsonToken, с указанием конкретной версии (состояние 2):

// build.gradle // … configurations.compile.resolutionStrategy { force 'com.googlecode.jsontoken:jsontoken:1.1' failOnVersionConflict() }

При применении этого варианта решения дерево зависимостей будет выглядеть следующим образом:

Третий вариант — добавить библиотеку jsonToken явно в качестве зависимости для проекта app и присвоить зависимости параметр force, который явно укажет, какую из версий библиотеки стоит использовать. (состояние 3)

// build.gradle // … dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile project(":first") compile project(":second") compile ('com.googlecode.jsontoken:jsontoken:1.1') { force = true } }

А дерево зависимостей станет выглядеть следующим образом:

Четвёртый вариант – исключить у одной из проектных зависимостей jsontoken из собственных зависимостей с помощью параметра exclude. (состояние 4)

// build.gradle dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile project(":first") compile(project(":second")) { exclude group: "com.googlecode.jsontoken", module: 'jsontoken' } }

И дерево зависимостей станет выглядеть следующим образом:

Стоит отметить, что exclude не обязательно передавать оба параметра одновременно, можно использовать только один.

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

Причину ошибки можно понять из вывода сообщений выполнения задачи сборки — класс GwtCompatible с идентичным именем пакета содержится в нескольких зависимостях. И это действительно так, дело в том, что проект app в качестве зависимости использует библиотеку guava, а библиотека jsontoken использует в зависимостях устаревшую Google Collections. Google Collections входит в Guava, и их совместное использование в одном проекте невозможно.

Добиться успешной сборки проекта можно тремя вариантами:

Первый — удалить guava из зависимостей модуля app. Если используется только та часть Guava, которая содержится в Google Collections, то предложенное решение будет неплохим.

Второй — исключить Google Collections из модуля first. Добиться этого мы можем используя описанное ранее исключение или правила конфигураций. Рассмотрим оба варианта, сначала используя исключения (состояние 5)

// build.gradle dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile(project(":first")) { exclude module: 'google-collections' } compile(project(":second")) { exclude group: "com.googlecode.jsontoken", module: 'jsontoken' } }

Пример использования правил конфигураций (состояние 6):

//build.gradle configurations.all { exclude group: 'com.google.collections', module: 'google-collections' } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile project(":first") compile(project(":second")) { exclude group: "com.googlecode.jsontoken", module: 'jsontoken' } }

Дерево зависимостей для обеих реализаций исключения Google Collections будет идентично.

Третий вариант — использовать функционал подмены модулей (состояние 7):

// build.gradle dependencies { modules { module('com.google.collections:google-collections') { replacedBy('com.google.guava:guava') } } compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.google.guava:guava:+' compile project(":first") compile(project(":second")) { exclude group: "com.googlecode.jsontoken", module: 'jsontoken' } }

Дерево зависимостей будет выглядеть следующим образом:

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

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

После каждого из вариантов мы достигнем успеха в виде собранного и запущенного приложения.

Но давайте рассмотрим другую ситуацию (состояние 8), у нас одна единственная сильно урезанная (для уменьшения размеров скриншотов) динамическая зависимость wiremock. Мы её используем сугубо в целях обучения, представьте вместо неё библиотеку, которую поставляет ваш коллега, он может выпустить новую версию в любой момент, и вам непременно необходимо использовать самую последнюю версию:

// build.gradle configurations.all { exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.json', module: 'json' exclude group: 'org.eclipse.jetty' exclude group: 'com.fasterxml.jackson.core' exclude group: 'com.jayway.jsonpath' } dependencies { compile 'com.github.tomakehurst:wiremock:+' }

Дерево зависимостей выглядит следующим образом:

Как вы можете увидеть, Gradle загружает последнюю доступную версию wiremock, которая является beta. Ситуация нормальная для debug сборок, но если мы собираемся предоставить сборку пользователям, то нам определённо необходимо использовать release-версию, чтобы быть уверенными в качестве приложения. Но при этом в связи с постоянной необходимостью использовать последнюю версию и частыми релизами нет возможности отказаться от динамического указания версии wiremock. Решением этой задачи будет написание собственных правил стратегии выбора версий зависимости:

// build.gradle //… configurations.all { //… resolutionStrategy { componentSelection { all { selection -> if (selection.candidate.version.contains('alpha') || selection.candidate.version.contains('beta')) { selection.reject("rejecting non-final") } } } } }

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

Но при тестировании было обнаружено, что в версии 1.58 присутствует критичный баг, и сборка не может быть выпущена в таком состоянии. Решить эту задачу можно, написав ещё одно правило выбора версии зависимости:

// build.gradle //… configurations.all { //… resolutionStrategy { componentSelection { // … withModule('com.github.tomakehurst:wiremock') { selection -> if (selection.candidate.version == "1.58") { selection.reject("known bad version") } } } } }

После чего версия wiremock 1.58 будет также отброшена, и начнёт использоваться версия 1.57, а дерево зависимостей будет выглядеть следующим образом:

Заключение
Несмотря на то, что статья получилась достаточно объемной, тема Dependency Management в Gradle содержит много не озвученной в рамках этой статьи информации. Глубже погрузиться в этот мир лучше всего получится с помощью официального User Guide в паре с документацией по Gradle DSL, в изучение которых придется инвестировать немало времени.

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

Читайте также: Gradle: 5 полезностей для разработчика

habr.com

Урок 2. Структура Android-проекта. Система сборки Gradle.

Facebook

Twitter

Вконтакте

Google+

Gradle

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

Не будем вдаваться во внутреннее устройство этой системы, рассмотрим для начала основы работы.

Создайте новый проект по аналогии с первым уроком. В дереве проекта найдите два файла с раширением .gradle:

 

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

На данном этапе нам не очень интересен первый файл, давайте откроем второй, отвечающий за наш модуль:

apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0.3" defaultConfig { applicationId "ru.android_school.lesson2" minSdkVersion 16 targetSdkVersion 23 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:24.0.0-alpha1' }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

apply plugin: 'com.android.application'

 

android {

    compileSdkVersion 23

    buildToolsVersion "23.0.3"

 

    defaultConfig {

        applicationId "ru.android_school.lesson2"

        minSdkVersion 16

        targetSdkVersion 23

        versionCode 1

        versionName "1.0"

    }

    buildTypes {

        release {

            minifyEnabled false

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }

    }

}

 

dependencies {

    compile fileTree(dir: 'libs', include: ['*.jar'])

    testCompile 'junit:junit:4.12'

    compile 'com.android.support:appcompat-v7:24.0.0-alpha1'

}

Рассмотрим файл построчно:

apply plugin: 'com.android.application'

apply plugin: 'com.android.application'

Эта строчка говорит, что модуль является приложением. Если бы модуль был библиотекой, следовало бы написать ‘com.android.library’.

Далее идет блок android.

Версия SDK, под которую собирается приложение. Обратите внимание, это не минимальная поддерживаемая версия! Этот параметр в большинстве случаев должен быть равен последней доступной версии SDK.

buildToolsVersion "23.0.3"

buildToolsVersion "23.0.3"

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

applicationId "ru.android_school.lesson2"

applicationId "ru.android_school.lesson2"

Имя пакета нашего приложения. Должно быть уникальным во всей экосистеме. Если кто-то ранее использовал это имя пакета, то вы не сможете загрузить это приложение в Google Play, или установить на устройство, на котором уже установлено приложение с таким же именем пакета.

Минимальная поддерживаемая версия Android. На устройства, у которых версия SDK ниже, чем указанная, установить приложение не получится.

Версия SDK, под которую собрано приложение. В большинстве случаев — последняя доступная версия SDK.

Целочисленная версия приложения. При выгрузке обновления, например, в Google Play, должна обязательно инкрементироваться.

Имя версии. Может быть любой строкой, ни на что не влияет (создана для отображения пользователям).

Блок buildTypes пока что опустим, он нам сейчас не интересен.

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

Это — одна из главных фишек Gradle. Раньше, в основном, приходилось скачивать Jar-файлы и добавлять их в проект. В больших проектах обновление этих самых библиотек становилось большой проблемой. Конечно же, тогда существовал и Maven, но им, к сожалению, мало кто пользовался. В Gradle же (внутри которого, кстати, лежит тот же самый maven) все намного проще. Мы просто перечисляем на отдельных строчках зависимости с версиями — и все! При обновлении библиотеки нужно всего лишь поменять строчку с версией:

compile 'com.android.support:appcompat-v7:23.2.1'

compile 'com.android.support:appcompat-v7:23.2.1'

Мы так же не лишены и возможности использовать Jar-файлы, нужно просто положить их в директорию libs/:

compile fileTree(dir: 'libs', include: ['*.jar'])

compile fileTree(dir: 'libs', include: ['*.jar'])

Для тестов есть отдельная директива testCompile:

testCompile 'junit:junit:4.12'

testCompile 'junit:junit:4.12'

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

Как видите, с Gradle все достаточно просто. Давайте теперь разберем структуру файлов в проекте.

Структура Android-проекта

Если посмотреть на дефолтную панель в левой части Android Studio, вы увидите следующую картину:

 

В manifests сгруппированы манифесты наших модулей. AndroidManifest — одна из ключевых частей приложения. Манифест описывает, из каких «частей» состоит приложение. Он представляет собой XML-файл, в котором описаны разрешения приложения (о них мы поговорим в следующих уроках), «экраны», сервисы, метаданные и т. д. Пример AndroidManifest.xml из пустого проекта:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ru.android_school.lesson2"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="ru.android_school.lesson2">

 

    <application

        android:allowBackup="true"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:supportsRtl="true"

        android:theme="@style/AppTheme">

 

        <activity android:name=".MainActivity">

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

 

                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

 

    </application>

 

</manifest>

Далее в дереве мы видим исходники на Java. Они разделены на две части — непосредственно код, и код для тестов.

Следом идут ресурсы (директория res) — в них находятся изображения, текст, аудиофайлы, и прочие вещи, которые мы будем использовать в приложении.

На самом деле, структура Android-проекта выглядит несколько иначе, просто Android Studio специально группирует файлы так, чтобы нам с ними было удобно работать.

Чтобы увидеть реальное дерево файлов, нужно выбрать вкладку «Project»:

 

И тогда мы увидим такую картину:

 

Немного иначе выглядит, не так ли? :)

По факту, ничего сложного в такой структуре нет, да и она нам, в принципе, особо не понадобится, так что рекомендую переключиться обратно во вкладку «Android».

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

Новые уроки добавляются каждый день! Чтобы ничего не пропустить, подпишитесь на нашу группу ВКонтакте, или на рассылку по e-mail (обещаем, спама не будет. честно.)

Facebook

Twitter

Вконтакте

Google+

android-school.ru

Better Way To Build / Хабр

Ни один проект с использованием платформы Java (и не только) не обходится без инструментов сборки (если только это не «Hello, world!»). Рано или поздно, но собирать дистрибутив руками надоедает. Да и компилировать из консоли было бы неплохо, если в проекте используется несколько разных IDE. А перед сборкой дистрибутива было бы здорово проставить номер версии в его имени. И unit тесты прогнать — не зря же Kent Beck книжки пишет. А там и Continues Integration на горизонте маячит. И здорово было бы научить CI сервер это все делать самостоятельно. Одним словом, есть уйма задач.

Раз есть задачи, то есть и решения. Думаю, большинство разработчиков хоть раз, но сталкивались с Ant. Очень многие используют Maven. Есть другие, не такие распространённые инструменты: GAnt, Buildr, и др. Каждый из них обладает набором своих плюсов и минусов, но сегодня я хочу представить вам кое-что новенькое. Gradle.

Gradle пытается объединить в себе все плюсы Ant, Maven и Ivy. И представить то, что получилось, с помощью Groovy. Теперь вместо того, чтобы скрещивать Batch-скрипты, java и xml-файлы конфигурации, можно просто написать несколько строчек кода на диалекте Groovy и радоваться жизни. Диалект специально разработан для описания сборки, тестирования, развертывания, экспорта и любых других действий над проектом, которые только могут прийти вам в голову.

Т.к. Gradle работает в запущеной JVM, он успешно использует библиотеки задач Ant, средства управления зависимостями Apache Ivy и другие существующие инструменты (TestNG, JUnit, Surefire, Emma, и т.п.). В принципе, несложно интегрировать в сборку любой инструмент, работающий в jvm. В придачу ко всему, диалект Groovy, используемый в Gradle, дает вам полную свободу действий. Совсем полную. Хотите условные выражения? Пожалуйста! Хотите циклы? Милости просим! Читать и писать файлы? Работать с сетью? Генерировать на лету собственные задачи? Все что угодно! И на нормальном, человеческом языке программирования, а не жутковатыми xml-конструкциями.

Интересная возможность: соответствующим образом настроенный Gradle-проект можно собрать на машине пользователя, на которой Gradle не установлен. Все, что требуется, — положить в дерево исходников 4 файла (которые Gradle сгенерирует для вас): 2 исполняемых для Win/*nix, 1 файл настроек и маленький jar. Всего на ~20Kb. После этого проект можно собрать на любой машине, где есть доступ к Сети. Скрипт сам позаботится о скачивании правильной версии Gradle, о настройке и запуске сборки.

Миграция на Gradle очень проста. Например, сборку maven2 можно преобразовать в сборку Gradle автоматически (с сохранением настроенных зависимостей, артефактов, версий и подпроектов). И миграция уже началась. Сейчас этот инструмент используют проекты: Grails, Spring Security, Hibernate Core и даже GAnt (честно, GAnt собирается при помощи Gradle!).

Похвалили, теперь нужно продемонстрировать в действии.

Для начала создадим шаблонный java проект, чтобы продемонстрировать использование 'build-by-convention'. А затем попытаемся его немного видоизменить, добавив в структуру файлов набор автоматизированных интеграционных тестов, чтобы показать, насколько большую свободу в использовании 'convention' предоставляет Gradle. В примере преднамеренно не упоминаются файлы исходников, т.к. не в них смысл.

Пусть у нас есть структура проекта (вы видели ее уже тысячу раз):

/project /src /main /java /resources /test /java /resources

Создаем в каталоге project пустой файл build.gradle. Записываем туда одну строчку:

apply plugin:'java'

Запускаем команду gradle build и получаем:

>gradle build :compileJava :processResources :classes :jar :assemble :compileTestJava :processTestResources :testClasses :test :check :build

BUILD SUCCESSFUL

Total time: 4.116 secs

В консоли видим выполнение последовательности задач (Gradle Tasks), которые являются близким аналогом Ant Targets. В каталоге /project/build можно найти скомпилированные классы (в т.ч., аккуратно упакованные в jar), отчеты по выполнению тестов и другие результаты сборки.

Все это пока что ничем не отличается от того, к чему привыкли многочисленные участники проектов с использованием Maven. Те же каталоги, такой же pom.xml (только называется build.gradle). Но не спешите расчехлять тухлые помидоры и позвольте продемонстрировать одну из интересных возможностей Gradle.

Добавим интеграционные тесты. Создадим для них отдельную ветку каталогов:

/project /src /main /test /integTest /java /resources

и добавим в build.gradle следующий код:

sourceSets { integTest }

В терминах Gradle source set — набор файлов и ресурсов, которые должны компилироваться и запускаться вместе. Приведенный выше фрагмент определяет новый source set с именем integTest. По умолчанию, исходники и ресурсы будут браться из /project/src/<имя source set>/java и /project/src/<имя source set>/resources соответственно. Java Plugin, который мы подключили в начале, задает два стандартных source set: main и test.

Будут автоматически сформированы три новых task'a: компиляция (compileIntegTestJava), обработка ресурсов (processIntegTestResource) и объединяющая их integTestClasses. Попробуем запустить:

>gradle integTestClasses :compileIntegTestJava :processIntegTestResources :integTestClasses

BUILD SUCCESSFUL

Total time: 1.675 secs

Ценой двух строчек мы получили 3 новых task'a и добавили в сборку проекта новый каталог. Но как только дело дойдет до написания этих тестов, мы обнаружим, что нам нужны все зависимости основного проекта, да еще и скомпилированные классы в придачу. Не вопрос, пишем:

configurations { integTestCompile { extendsFrom compile } } sourceSets { integTest{ compileClasspath = sourceSets.main.classes + configurations.integTestCompile } }

Блок Congfigurations описывает конфигурации зависимостей. Каждая конфигурация может объединять maven артефакты, наборы локальных файлов и др. Новые конфигурации могут наследоваться от существующих. В данном случае, мы наследуем конфигурацию зависимостей для компиляции интеграционных тестов от конфигурации compile. Эта конфигурация — стандартная (заданная plugin) для компиляции main

Строка sourceSets.main.classes + configurations.integTestCompile обозначает объединение наборов файлов. main.classes — каталог, где будут находиться *.class файлы, полученные при сборке main.

Наверное, при сборке нам пригодится JUnit. И неплохо было бы еще и запустить эти тесты. Опишем еще одну конфигурацию зависимостей.

configurations { integTestCompile { extendsFrom compile } integTestRuntime { extendsFrom integTestCompile, runtime } } repositories { mavenCentral() } dependencies { integTestCompile "junit:junit:4.8.1" } sourceSets { integTest{ compileClasspath = sourceSets.main.classes + configurations.integTestCompile runtimeClasspath = classes + sourceSets.main.classes + configurations.integTestRuntime } } task integrationTest(type: Test) { testClassesDir = sourceSets.integTest.classesDir classpath = sourceSets.integTest.runtimeClasspath }

Блок Repositories подключит нам maven central (и другие репозитории на наш выбор). Блок dependencies добавит зависимость от артефакта к нашей конфигурации. runtimeClasspath = classes + sourceSets.main.classes + configurations.integTestRuntime объединит файлы из integTest.classes, main.classes и integTestRuntime.

Задачу для запуска тестов всё-таки пришлось написать. Впрочем, это было несложно: достаточно указать, откуда брать тесты и с каким classpath их запускать, т.е. «что». «Как» Gradle определит самостоятельно.

Теперь мы готовы:

>gradle clean integrationTest :clean :compileJava :processResources :classes :compileIntegTestJava :processIntegTestResources UP-TO-DATE :integTestClasses :integrationTest

BUILD SUCCESSFUL

Total time: 4.195 secs

Обратите внимание, что Gradle обработал зависимость integTest.compileClasspath от main.classes и собрал source set main прежде, чем собирать интеграционные тесты!

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

О чем еще не сказано ни слова: о работе с task-ами, об инкрементальной сборке, о работе с подпроектами, о работе с Maven репозиториями, об интеграции с Ant, о стандартных plugin-ах, о init-scripts и многом-многом другом. Но об этом, быть может, в других статьях.

habr.com