Pull to refresh

Gradle: Better Way To Build

Reading time 5 min
Views 112K
Ни один проект с использованием платформы 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 и многом-многом другом. Но об этом, быть может, в других статьях.
Tags:
Hubs:
+39
Comments 14
Comments Comments 14

Articles