Holidays are always a good moment to stop and reflect a little bit about the last few projects, and this is even more true for Christmas holidays as November and December are always busy months.
During the last couple of months in Tengio we have created a few open source library and demo apps, and as a reasult we have plenty of duplicated code in build scripts.
It is time to have a deeper look at the gradle build scripts and plugins that we use and extract the duplication where possible.
I set the following requirements for this task:
- Cost in terms of my time should not be more than 2 days
- The abstraction should not increase the learning curve for other developers in the team
- The changes should affect positively more than 1 project.
Duplication in gradle scripts
It is quite easy to identify the primary source of duplication in the gradle scripts of our projects. Just by reading through a few project build.gradle
files I have identified the following parts:
- checkstyle
- bintray
Checkstyle
I hope people reading this blog are familiar with checkstyle. If you are not please google it and try to use it. In the past, it was quite hard and cumbersome to use, but nowdays there are no excuses.
The best way to take advantage of checkstyle is to create a company wide code style rule set that is as close as possible to the current standard style guidelines of you project type. Once the rule set is defined you can enforce the rules in the static code analysis phase of the build scripts and at the same time help developers use the rules with some IDE integration.
To summarise if you want to use checkstyle, you have to:
- enforce it in your continuous integration
- integrate it in your IDE
Any friction between these two rules and you will have more issues using checkstyle than not using it.
In this case I’m looking at java/android base projects. For this type of projects you can implement checkstyle with the following steps:
- define the rules of your code style in a file
checkstyle.xml
. - add the checkstyle plugin to your gradle project.
- create a
codestyle.xml
file, and import the file in your IDE (at least this should work for android studio and IntelliJ).
Checkstyle implementation problem
As mentioned before, the codestyle implementation for a project consists of the following files:
- codestyle.xml
- checkstyle.xml
- Plugin repository definition and dependency
- For android project you also need the ridefinition of the checkstyle task so that it can take the proper source code to analyse.
All this is fine for a single project, but as soon as you start to have a few projects you end up with lots of duplications which are expensive to maintain.
Tengio Checkstyle Plugin
Ok so, we have one problem and we know the requirements for our solution. I think a gradle plugin is the best possible solution in this case. One of the main goals is the ability to share checkstyle.xml
file for all the projects.
First thing to know: is it actually possible to include and distribute the xml file into the jar and pass it to checkstyle?
A gradle plugin is essentialy a jar. So it is possible to include a file as a resource and read it. More complicated is passing it to checkstyle plugin. With limited time I can’t rewrite the checkstyle plugin. I need to have it as a dependency and set the proper configuration to make it work.
Luckily for me the checkstyle plugin has a new config
property that can take the checkstyle Checker configuration as a string. Finally, with this line of code it is possible to read a file in the resources of the jar as a string and assign it to the checkstyle plugin:
checkstyle.config = project.resources.text.fromString(getClass().getResourceAsStream("checkstyle.xml").text)
Ok now I have to learn how to create a gradle plugin, inject a checkstyle plugin and make sure it is configured correctly.
These are the things we need to do:
create a gradle plugin project
define a plugin id
define the plugin class
Checkstyle plugin dependency
apply the Checkstyle plugin
try the plugin
add support for android projects
publish the plugin
update existing projects to use the new plugin
1. Create a gradle plugin project
Note : I will discuss only the necessary parts in the blog, if you want to look at the full code the project is open source:
Simple, just create a build.gradle file with this in your root folder:
apply plugin: 'groovy'
dependencies {
compile gradleApi()
compile localGroovy()
}
With this in you build.gradle you have a basic groovy gradle plugin (you can also do it in java if you prefer).
2. Define a plugin id
Each gradle plugin needs to define a plugin id. The plugin id will also have to associate the id to a Class that implements the Plugin itself. To do that you have to create a file with name [package].[pluginId]
. The file need to be in src/main/resources/META-INF
directory. The file needs to contain the class association. Like in this case:
implementation-class=com.tengio.gradle.TengioCheckstylePlugin
3. Define the plugin class
The plugin class is a very simple class in itself that implements Plugin. You can use the apply method to do your own logic.
class TengioCheckstylePlugin implements Plugin<Project> {
void apply(Project project) {
....
}
}
4. Checkstyle plugin dependency
Dependency declaration with gradle plugins is easy. See here:
buildscript {
...
dependencies {
classpath 'com.puppycrawl.tools:checkstyle:7.3'
}
}
This needs to be placed in the build.gradle of the plugin we are creating.
5. Apply the Checkstyle plugin
Going back to the TengioCheckstylePlugin class, to configure and apply the checkstyle plugin to the targeted project you need to use the pluginManager and the apply method as in this script:
project.plugins.withType(CheckstylePlugin) {
project.checkstyle {
toolVersion = "7.3"
config = project.resources.text.fromString(getClass().getResourceAsStream("checkstyle.xml").text)
}
}
project.pluginManager.apply('checkstyle')
6. Try the plugin
To test the plugin with a demo project, I have created a simple java project with a couple of classes. There is one tricky part though. I need to publish the plugin somewhere so that my demo app can use it.
Maven to the rescue:
apply plugin: 'maven'
group = 'com.tengio.gradle'
version = '1.0-SNAPSHOT'
uploadArchives {
repositories {
mavenDeployer {
repository(url: mavenLocal().url)
}
}
}
By adding this to your build.gradle and with the task uploadArchives
you can deploy a version of your plugin locally. In the demo project you can easily pick up the plugin by adding usual plugin declaration:
buildscript {
repositories {
...
mavenLocal()
}
dependencies {
classpath 'com.tengio.gradle:tengio-checkstyle-plugin:1.0-SNAPSHOT'
}
}
apply plugin: 'com.tengio.gradle.tengio-checkstyle-plugin'
Ok now everything works on a normal java project.
7. Add support for android projects
When you apply the checkstyle plugin to a java project, gradle will automatically attach two tasks checkstyleMain and checkstyleTest as dependencies of the check task.
With android though this doesn’t happen. This is probably related to the different way android plugin manager defines the sources paths of the project. If you have used checkstyle on android project you already know this. Anyway we basically have to do that manually:
if(!hasTask(project, 'checkstyleMain')) {
Checkstyle c = project.tasks.create('checkstyleMain', Checkstyle)
c.source = 'src'
c.include '**/*.java'
c.exclude '**/gen/**'
c.classpath = project.files()
c.config = project.resources.text.fromString(getClass().getResourceAsStream("checkstyle.xml").text)
project.tasks.getByName('check').dependsOn 'checkstyleMain'
}
The interesting bit is the last line:
project.tasks.getByName('check').dependsOn 'checkstyleMain'
It is adding the newly defined task as a Dependency of ‘check’. Check is one of the goals executed during an android gradle build.
8. Publish the plugin
I’m used to publish Tengio’s open source project to bintray, and it is possible to publish a gradle plugin to bintray. However, I noticed that there is a specific gradle repository for plugins. So I gave it a go.
Go to this link and follow the steps, this pubblication process is actually very simple (one of the best I have used so far).
Once it is published you can use it like any other public plugin:
buildscript {
repositories {
...
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
...
classpath "com.tengio.gradle:tengio-checkstyle-plugin:1.0"
}
}
apply plugin: 'com.tengio.gradle.tengio-checkstyle-plugin'
9. Update existing projects to use the new plugin
Finally, the best part and is all about deleting stuff.
Bintray and maven
At Tengio we have a few open source projects. All open source projects should be as easy as possible to use. For this reason all our projects are published on Bintray.
Bintray requires the maven plugin to prepare some meta-information of the artifact. Also for Android projects, bintray needs some custom configuration. See this example:
apply plugin: 'com.jfrog.bintray'
apply plugin: 'com.github.dcendents.android-maven'
project.group = "com.tengio.android"
project.afterEvaluate {
project.version = android.defaultConfig.versionName
}
bintray {
user = ''
key = ''
if (project.hasProperty('BINTRAY_USER') && project.property('BINTRAY_KEY')) {
user = project.property('BINTRAY_USER')
key = project.property('BINTRAY_KEY')
}
configurations = ['archives']
pkg {
repo = 'maven'
userOrg = 'tengioltd'
licenses = ['Apache-2.0']
}
}
android.libraryVariants.all { variant ->
def name = variant.buildType.name
def task = project.tasks.create "jar${name.capitalize()}", Jar
task.dependsOn variant.javaCompile
task.from variant.javaCompile.destinationDir
artifacts.add('archives', task);
}
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
task javadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
failOnError false
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
}
NOTE: Note the trick that we use to avoid the duplication of the versionName:
project.afterEvaluate {
project.version = android.defaultConfig.versionName
}
Bintray and maven implementation problem
You can see the problem already. For all the Android projects we have to write the previous snippet of code.
Tengio Bintray Script
Digging stuff on how to use checkstyle took me quite a bit and having seen the difficulties, I’m afraid I can’t follow the same plugin solution I used for checkstyle.
Luckily for me there is another clever solution for gradle to reuse code: apply from: url
.
With this you can apply a script to an existing build. It is quite awesome. There is only one drawback though, the apply from:
is executed after the evaluation phase. This means that it is not possible to add dependencies from within the applyed script.
Still a pretty good result. We remove all the code making sure to leave the plugin dependencies:
buildscript {
repositories {
...
dependencies {
...
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
}
}
And we apply the script directly from the github repository:
apply from: 'https://raw.githubusercontent.com/Tengio/tengio-bintray-script/master/bintray.gradle'
Conclusions
Overall I’m happy with checkstyle plugin. Also the bintray imlementation is much better than before. I know the ideal solution would be a plugin for bintray too, but I leave it for another holiday.
To be honest all this was also an elaborate excuse to get to build some plugin with gradle. I always wanted to try it.