Статьи

Gradle Goodness: Расширение DSL

У Gradle уже есть мощный DSL, но Gradle не был бы Gradle, если бы мы сами не смогли расширить DSL. Возможно, у нас есть собственные соглашения об именах в нашей компании, или у нас есть специальный проблемный домен, который мы хотим выразить в скрипте сборки Gradle. Мы можем использовать ExtensionContainer, доступные через  project.extensions, чтобы добавить новые концепции в наши сценарии сборки. На   вебинаре по стандартизации вашей корпоративной среды Люком Дейли (Luke Daley) показано несколько примеров того, как расширить DSL. Также в  samples папке дистрибутива Gradle приведены примеры того, как создать пользовательский DSL.

Давайте сначала создадим простое расширение DSL. Сначала мы определим новый класс  CommonDependencies с методами для определения зависимостей в проекте Java. Мы хотим использовать эти методы с описательными именами в наших скриптах сборки. Чтобы добавить класс, мы используем  create() метод ExtensionContainer. Первый аргумент — это имя, которое должно быть уникальным в сборке. Имя может использоваться вместе с блоком конфигурации в скрипте для вызова методов класса, который мы передаем в качестве второго аргумента. Наконец, мы можем передать аргументы конструктора для класса как последние аргументы  create() метода.

/**
 * Class for DSL extension. A default repository is added
 * to the project. The use<name>() methods add 
 * dependencies to the project.
 */
class CommonDependencies {
    /** Reference to project, so we can set dependencies/repositories */
    final Project project

    CommonDependencies(final Project project) {
        this.project = project

        // Set mavenCentral() repository for project.
        project.repositories {
            mavenCentral()
        }
    }

    /**
     * Define Spock for testCompile dependency 
     * @param version Version of Spock dependency with default 0.7-groovy-2.0
     */
    void useSpock(final String version = '0.7-groovy-2.0') {
        project.dependencies {
            testCompile "org.spockframework:spock-core:$version"
        }
    }

    /**
     * Define Spring for compile dependency 
     * @param version Version of Spring dependency with default 3.2.3.RELEASE
     */
    void useSpring(final String version = '3.2.3.RELEASE') {
        project.dependencies {
            compile "org.springframework:spring-core:$version"
        }
    }

}

// Add DSL extension 'commonDependencies' with class CommonDependencies 
// passing project as constructor argument.
project.extensions.create('commonDependencies', CommonDependencies, project)

apply plugin: 'java'

// Use new DSL extension. Notice we can use configuration closures just
// like we are used to with other Gradle DSL methods.
commonDependencies {
    useSpock()
    useSpring '3.1.4.RELEASE'
}

// We can still use the Java plugin dependencies configuration.
dependencies {
    compile 'joda-time:joda-time:2.1'
}

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

$ gradle dependencies
...
compile - Compile classpath for source set 'main'.
+--- org.springframework:spring-core:3.1.4.RELEASE
|    +--- org.springframework:spring-asm:3.1.4.RELEASE
|    \--- commons-logging:commons-logging:1.1.1
\--- joda-time:joda-time:2.1
...
testCompile - Compile classpath for source set 'test'.
+--- org.springframework:spring-core:3.1.4.RELEASE
|    +--- org.springframework:spring-asm:3.1.4.RELEASE
|    \--- commons-logging:commons-logging:1.1.1
+--- joda-time:joda-time:2.1
\--- org.spockframework:spock-core:0.7-groovy-2.0
+--- junit:junit-dep:4.10
|    \--- org.hamcrest:hamcrest-core:1.1 -> 1.3
+--- org.codehaus.groovy:groovy-all:2.0.5
\--- org.hamcrest:hamcrest-core:1.3

Мы также можем использовать плагин для расширения Gradle DSL. В коде плагина мы используем тот же  project.extensions.create() метод, чтобы он был более прозрачным для пользователя. Нам нужно только применить плагин к проекту, и мы можем использовать дополнительные методы DSL в сценарии сборки. Давайте создадим простой плагин, который расширит DSL концепцией книги и глав. Следующий скрипт сборки показывает, что мы можем сделать после применения плагина:

apply plugin: 'book'

book {
    title 'Groovy Goodness Notebook'
    chapter project(':chapter1')
    chapter project(':chapter2')
}

Для этого мы сначала создаем следующую структуру каталогов с файлами:

+ sample
+ buildSrc
+ src/main/groovy/com/mrhaki/gradle
+ Book.groovy
+ BookPlugin.groovy
+ src/main/resources/META-INF/gradle-plugins
+ book.properties
+ book
+ build.gradle
+ chapter1/src/html
+ index.html
+ chapter2/src/html
+ index.html
+ settings.gradle

Book Класс будет добавлен в качестве расширения DSL. У класса есть метод для установки  title свойства и метод для добавления глав, которые являются объектами проекта Gradle.

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/Book.groovy
package com.mrhaki.gradle

import org.gradle.api.*

class Book {
    String title
    List<Project> chapters = []

    void title(final String title) {
        this.title = title
    } 

    void chapter(final Project chapter) {
        chapters << chapter
    }
}

Далее мы создаем  BookPlugin класс. Плагин добавит  Book класс как расширение DSL. Но мы также создаем задачу,  aggregate которая будет посещать каждую определенную главу и затем копировать содержимое из  scr/html папки в проекте главы в  aggregate папку в папке сборки. Наконец, мы добавляем  dist задачу, которая будет просто архивировать содержимое агрегированных файлов.

// File: buildSrc/src/main/groovy/com/mrhaki/gradle/BookPlugin.groovy
package com.mrhaki.gradle

import org.gradle.api.*
import org.gradle.api.tasks.*
import org.gradle.api.tasks.bundling.Zip

class BookPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.configure(project) {
            apply plugin: 'base'

            def book = project.extensions.create 'book', Book

            afterEvaluate {
                // Create task in afterEvaluate, so chapter projects
                // are resolved, otherwise chapters is empty.
                tasks.create(name: 'aggregate') {

                    // Skip task if no chapters are defined.
                    onlyIf { !book.chapters.empty }

                    // Copy content in src/html of 'book' directory.
                    copy {
                        from file('src/html')
                        into file("${buildDir}/aggregate")
                    }

                    // Copy content in src/html of chapter directories.
                    book.chapters.each { chapterProject ->
                        copy {
                            from chapterProject.file('src/html')
                            into file("${buildDir}/aggregate/${chapterProject.name}")
                        }
                    }
                }
            }

            tasks.create(name: 'dist', dependsOn: 'aggregate', type: Zip) {
                from file("${buildDir}/aggregate")
            }
        }        
    }
}

Мы создаем файл,  book.properties чтобы сообщить Gradle о нашем новом плагине:

# File: buildSrc/src/main/resources/META-INF/gradle-plugins/book.properties
implementation-class=com.mrhaki.gradle.BookPlugin

Наш плагин закончен, поэтому мы можем добавить проект книги и некоторые проекты глав. В  settings.gradle файле мы определяем включение для этих каталогов:

// File: settings.gradle
include 'chapter1'
include 'chapter2'
include 'book'

В каталогах глав мы можем добавить некоторые примеры содержимого в  src/html каталогах. И в  book папке мы создаем следующий build.gradle файл:

// File: book/build.gradle
apply plugin: 'book'

book {
    title 'Groovy Goodness Notebook'
    chapter project(':chapter1')
    chapter project(':chapter2')
}

Теперь из  book папки мы можем запустить  aggregate и  dist задачи. Конечный результат — все файлы из src/html папки раздела  находятся в build/aggregate папке. И в  build/distributions папке у нас есть файл,  book.zip содержащий файлы.

Код написан с Gradle 1.6.