Пишем свой Gradle плагин для AnnotatedSql

    Вступление



    Привет, коллеги. Давно я не писал ничего на Хабр. Вот, решил исправить это досадное недоразумение.

    Не так давно я сменил место работы, и проект, над которым я теперь работаю, использует для сборки Gradle. Более того, проект достаточно развесистый и сложный, и Gradle скрипт в нем весьма непростой. Поэтому я решил, что надо подучить Gradle. Как один из шагов обучения я решил написать свой собственный плагин. Плагин посвящен замечательной библиотеке annotated-sql, созданной моим хорошим товарищем Геннадием hamsterksu. Я использую эту библиотеку в персональных проектах, поэтому мне нужен удобный способ прикреплять и конфигурировать ее к ним. Библиотека использует процессоры аннотаций, поэтому цель плагина — подружить эти процессоры и gradle сборку.

    Задача


    Перед тем, как приступить к плагину, давайте сначала определим, что мы хотим получить в итоге от плагина.
    Библиотека состоит из двух частей — jar с аннотациями, которые мы используем в нашем коде для описания нашей схемы БД и jar с процессорами, который не линкуется в наш код, но используется на этапе компиляции. Наш плагин должен определить, где находятся эти jar's (в идеале, он должен их скачать, но это только после того, как у кого-то из нас дойдут руки их захостить на maven central). После этого он должен настроить процесс компиляции java, чтобы использовать процессоры.

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

    Конфигурация


    Поскольку мы не хотим линковать наши процессоры к коду, мы определим отдельную конфигурацию зависимостей, назовем ее asqlapt.
    configurations {
        asqlapt
    }
    

    и сконфигурируем ее.
    dependencies {
        asqlapt fileTree(dir: 'libs.apt', include: '*.jar')
    }
    

    здесь мы говорим, что зависимости нашего asqlapt находятся в папке libs.apt нашего проекта.

    Модификация компиляции


    Следующий шаг — вклиниться в компилятор и указать ему на наш процессор. Давайте рассмотрим код, который делает это:
    android.applicationVariants.all { variant ->
    	def aptOutput = file("$buildDir/source/apt_generated/$variant.dirName")
    		
    	variant.javaCompile.doFirst {
    		aptOutput.mkdirs()
    		variant.javaCompile.options.compilerArgs += [
    			'-processorpath', configurations.asqlapt.getAsPath(), '-processor',
    			'com.annotatedsql.processor.provider.ProviderProcessor,com.annotatedsql.processor.sql.SQLProcessor',
    			'-s', aptOutput
    		]
    	}
    }
    

    Итак, для каждого варианта сборки андроид приложения мы выполняем следующие шаги:
    • Определяем папку, в которую поместятся наши сгенерированные классы.
    • Для каждого варианта сборки существует задача javaCompile. Мы можем вклиниться в исполнение этой задачи при помощи метода doFirst. Здесь мы добавляем аргументы компилятору, указываем на путь к процессорам и сами процессоры, а так же папку, куда поместить результат

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

    Создание плагина



    Как написано в документации, наш плагин дожен находиться в папочке buildSrc в корневом проекте. Создадим эту папочку и build.gradle следующего содержания:

    apply plugin: 'groovy'
    
    dependencies {
        compile gradleApi()
        compile localGroovy()
    }
    


    теперь объявим наш плагин в файле src/main/resources/META-INF/gradle-plugins/annotatedsql.properties следующим образом:

    implementation-class=com.evilduck.annotatedsql.AnnotatedSqlPlugin
    


    С рутиной покончено, теперь к коду. Код плагинов пишется на Groovy.

    Создадим наш класс:

    public class AnnotatedSqlPlugin implements Plugin<Project>  {
    
        private Project project
    
        public void apply(Project project) {
            this.project = project
        }
    
    }
    


    вот так выглядит заготовка плагина. Начнем выполнять действия в методе apply.

    Первое, что мы хотим — добавить конфигурации зависимостей:

    def setupDefaultAptConfigs() {
            project.configurations.create('apt').with {
                visible = false
                transitive = true
                description = 'The apt libraries to be used for annotated sql.'
            }
    
            project.configurations.create('annotatedsql').with {
                extendsFrom project.configurations.compile
                visible = false
                transitive = true
                description = 'The compile time libraries to be used for annotated sql.'
            }
    
            project.dependencies {
                apt project.fileTree(dir: "$project.projectDir/libs-apt", include: '*.jar')
                annotatedsql project.files("$project.projectDir/libs/sqlannotation-annotations.jar")
            }
    }
    


    этот метод создает две конфигурации — apt и annotatedsql. Первая — для процессоров, вторая — для API. Далее мы инициализируем эти конфигурации значениями по-умолчанию.

    Следующий шаг — настройка компилятора:

    def modifyJavaCompilerArguments() {
            project.android.applicationVariants.all { variant ->
                def aptOutput = project.file("$project.buildDir/source/$extension.aptOutputDir/$variant.dirName")
    
                variant.javaCompile.doFirst {
                    aptOutput.mkdirs()
    
                    variant.javaCompile.options.compilerArgs += [
                            '-processorpath', project.configurations.apt.getAsPath(), '-processor',
                            'com.annotatedsql.processor.provider.ProviderProcessor,com.annotatedsql.processor.sql.SQLProcessor',
                            '-s', aptOutput
                    ]
                }
            }
    }
    


    Тоже ничего нового. Но подождите, что такое extension. Gradle позволяет нам при создании плагинов создавать объекты — расширения. Это простые POGO объекты, хранящие конфигурацию плагина. Самое близкое нам, андроидщикам расширение — android. Да, это как раз тот android обьект, в котором вы конфигурируете свою сборку. Давайте посмотрим, как мы объявили наш extension:

    class AnnotatedSqlExtension  {
    
        public String aptOutputDir
    
    }
    


    и в методе apply плагина:

    extension = project.extensions.create("annotatedsql", AnnotatedSqlExtension)
    extension.with {
                aptOutputDir = "aptGenerated"
    }
    


    инициализируем расширение и прикрепляем его к проекту.

    Вот, собственно, и весь плагин.

    Полный код
    public class AnnotatedSqlPlugin implements Plugin<Project>  {
    
        private Project project
    
        private AnnotatedSqlExtension extension
    
        public void apply(Project project) {
            this.project = project
    
            project.apply plugin: 'android'
    
            extension = project.extensions.create("annotatedsql", AnnotatedSqlExtension)
            extension.with {
                aptOutputDir = "aptGenerated"
            }
    
            setupDefaultAptConfigs()
            modifyJavaCompilerArguments()
        }
    
        def setupDefaultAptConfigs() {
            project.configurations.create('apt').with {
                visible = false
                transitive = true
                description = 'The apt libraries to be used for annotated sql.'
            }
    
            project.configurations.create('annotatedsql').with {
                extendsFrom project.configurations.compile
                visible = false
                transitive = true
                description = 'The compile time libraries to be used for annotated sql.'
            }
    
            project.dependencies {
                apt project.fileTree(dir: "${project.projectDir}/libs-apt", include: '*.jar')
                annotatedsql project.files("${project.projectDir}/libs/sqlannotation-annotations.jar")
            }
        }
    
        def modifyJavaCompilerArguments() {
            project.android.applicationVariants.all { variant ->
                def aptOutput = project.file("$project.buildDir/source/$extension.aptOutputDir/$variant.dirName")
    
                variant.javaCompile.doFirst {
                    aptOutput.mkdirs()
    
                    variant.javaCompile.options.compilerArgs += [
                            '-processorpath', project.configurations.apt.getAsPath(), '-processor',
                            'com.annotatedsql.processor.provider.ProviderProcessor,com.annotatedsql.processor.sql.SQLProcessor',
                            '-s', aptOutput
                    ]
                }
            }
        }
    
    }
    



    Теперь, давайте посмотрим, как мы его используем:

    buildscript {
        repositories {
            mavenCentral()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:0.6.+'
        }
    }
    apply plugin: 'android'
    apply plugin: 'annotatedsql'
    
    repositories {
        mavenCentral()
    }
    
    android {
        // наши конфигурации андроид проекта
    }
    
    /*
     * Если хотим, мы можем изменить aptOutputDir
     */
    //annotatedsql {
    //    aptOutputDir = "customAptOutputDir"
    //}
    
    /*
     * Если нужно, можем изменить локацию библиотек
     */
    //dependencies {
    //    apt project.fileTree(dir: "${project.projectDir}/libszzz-apt", include: '*.jar')
    //    annotatedsql project.files("${project.projectDir}/libszzz/sqlannotation-annotations.jar")
    //}
    


    Как видно, если мы поместили библиотеки в нужное место, все, что нам нужно добавить в скрипт, это

    apply plugin: 'annotatedsql'
    


    Как я уже говорил, в идеале, если разместить jar'ы библиотеки в центральном репозитории, необходимость в ручном добавлении их в проект отпадет совсем. Gradle просто скачает их и положит сам в укромное место. К сожалению, пока репозиториев нет, и это не есть что-то, что я могу контролировать в плагине. Однако, если предположить, что библиотеки были загружены в репозиторий, всё, что нам нужно было бы сделать — это изменить локальные dependencies на удаленные. Что-то вроде:

    project.dependencies {
                apt 'com.hamsterksu.asql:something-version'
                annotatedsql 'com.hamstersku.asql:something-else-version'
    }
    


    В этом случае, все, что нам нужно было бы сделать, это добавить плагин в проект (плагин тоже может быть в репозитории, аналогично android плагину) и применить его:

    apply plugin: 'annotatedsql'
    


    Завершение


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

    Подробнее
    Реклама
    Комментарии 6
    • –4
      Код плагинов пишется на Groovy.

      И тут-же сразу приводится код на Java. Генильно.
      А вообще плагины можно писать на чем угодно, что поддерживает линковку с Java — это, собственно, сама Java, Groovy и Scala.
      • +1
        Это код на Groovy. В Java этот код бы не скомпилировался. Не нужно быть гением, чтобы заметить это. Еще попытки подколоть?
        • 0
          Удалено
          • 0
            Еще бы ссылки на источники информации :)
            • 0
              В качестве источника инфо пользовался официальной документацией Gradle, в частности, по созданию плагинов, http://www.gradle.org/docs/current/userguide/custom_plugins.html, а также скачал и поизучал исходные коды. Они доступны на этом же сайте, и там есть много готовых плагинов. Я в частности смотрел на Findbugs плагин :)
        • 0
          Понимаю, что оживляю уже старый топик, но не могли бы вы добавить в текст, что вы опубликовали плагин в maven central и его можно подключить вот так в build.gradle:
          // ...
          apply plugin: 'annotatedsql'
          // ...
          buildscript {
              repositories {
                  mavenCentral()
              }
              dependencies {
          // тут может быть еще что-то
                  classpath 'com.github.hamsterksu:android-annotatedsql-gradle-plugin:1.7.9'
              }
          }
          
          dependencies {
          // ...
          // подключаем, чтобы сама IDE адекватно показывала подстветку и узнавала классы
              compile 'com.github.hamsterksu:android-annotatedsql-api:1.7.8'
          // ...
          }
          
          

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

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