Introduction
Hello colleagues. For a long time I did not write anything on Habr. Here, I decided to correct this unfortunate misunderstanding.
Not so long ago, I changed jobs, and the project I am working on now uses to build Gradle. Moreover, the project is quite spreading and complicated, and the Gradle script in it is very complicated. So I decided to learn some Gradle. As one of the steps of learning, I decided to write my own plugin. The plugin is dedicated to the wonderful
annotated-sql library, created by my good friend Gennady
hamsterksu . I use this library in personal projects, so I need a convenient way to attach and configure it to them. The library uses annotation processors, so the purpose of the plugin is to make friends with these processors and gradle the assembly.
Task
Before proceeding with the plugin, let's first determine what we want to end up with the plugin.
The library consists of two parts - jar with annotations, which we use in our code to describe our database schema and jar with processors, which is not linked into our code, but used at the compilation stage. Our plugin should determine where these jar's are (ideally, it should download them, but this is only after some of us reach their hands at the maven central). After that, it must configure the java compilation process to use processors.
')
Let's first consider what changes we would make to our script without a plugin to make the library work.
Configuration
Since we do not want to link our processors to the code, we will define a separate dependency configuration, let's call it asqlapt.
configurations { asqlapt }
and configure it.
dependencies { asqlapt fileTree(dir: 'libs.apt', include: '*.jar') }
here we say that our asqlapt dependencies are in the libs.apt folder of our project.
Compile Modification
The next step is to hook into the compiler and point it to our processor. Let's take a look at the code that does this:
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 ] } }
So, for each version of the assembly of the android application, we perform the following steps:
- We define the folder in which our generated classes will fit.
- For each build option, there is a javaCompile task. We can hook into this task using the doFirst method. Here we add arguments to the compiler, point to the path to the processors and the processors themselves, as well as the folder where to place the result.
That is, in principle, all that we need. But we want to make it like a plugin, right?
Creating a plugin
As written in the
documentation , our plugin should be located in the buildSrc daddy in the root project. Create this daddy and build.gradle as follows:
apply plugin: 'groovy' dependencies { compile gradleApi() compile localGroovy() }
Now we will declare our plugin in the src / main / resources / META-INF / gradle-plugins / annotatedsql.properties file as follows:
implementation-class=com.evilduck.annotatedsql.AnnotatedSqlPlugin
With the routine over, now to the code. Plugin code is written in Groovy.
Let's create our class:
public class AnnotatedSqlPlugin implements Plugin<Project> { private Project project public void apply(Project project) { this.project = project } }
This is how the plugin looks like. Begin to perform actions in the apply method.
The first thing we want is to add dependency configurations:
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") } }
This method creates two configurations - apt and annotatedsql. The first is for processors, the second is for APIs. Next, we initialize these configurations with default values.
The next step is to set up the compiler:
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 ] } } }
Also nothing new. But wait, what is an extension. Gradle allows us when creating plugins to create objects - extensions. These are simple POGO objects that store the configuration of the plugin. The closest to us, androids expansion - android. Yes, this is exactly the android object in which you configure your build. Let's see how we declared our extension:
class AnnotatedSqlExtension { public String aptOutputDir }
and in the apply method of the plugin:
extension = project.extensions.create("annotatedsql", AnnotatedSqlExtension) extension.with { aptOutputDir = "aptGenerated" }
initialize the extension and attach it to the project.
Here, in fact, the whole plugin.
Full code 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 ] } } } }
Now, let's see how we use it:
buildscript { repositories { mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:0.6.+' } } apply plugin: 'android' apply plugin: 'annotatedsql' repositories { mavenCentral() } android {
As you can see, if we put the libraries in the right place, all we need to add to the script is
apply plugin: 'annotatedsql'
As I have already said, ideally, if you place the jar's of the library in the central repository, the need to manually add them to the project will disappear altogether. Gradle simply downloads them and puts it in a secluded place. Unfortunately, there are no repositories yet, and this is not something that I can control in the plugin. However, if we assume that the libraries have been loaded into the repository, all we would need to do is change the local dependencies to remote ones. Sort of:
project.dependencies { apt 'com.hamsterksu.asql:something-version' annotatedsql 'com.hamstersku.asql:something-else-version' }
In this case, all we would have to do is add the plugin to the project (the plugin can also be in the repository, similar to the android plugin) and apply it:
apply plugin: 'annotatedsql'
Completion
Finally I want to say that this is the tip of the iceberg of the Gradle features. Nevertheless, I hope that this will help someone to begin to understand the creation of plug-ins for Gradle, and Gradle as a whole. Personally, the more I learn, the more I fall in love with this build system.
On this, I hasten to say goodbye. Thank you all for your attention!