5 November 2010

Gradle: Tasks Are Code

JavaGradle
В предыдущем топике я постарался вкратце рассказать, что же такое 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 развязывает вам руки и позволяет самостоятельно решать, как вы хотите выполнить вашу задачу.

Продолжение следует.
Tags:gradlegroovybuild
Hubs: Java Gradle
+26
44.1k 97
Comments 48