Category: Blog, Android, Development

How to Create a Gradle Plugin in Kotlin

creating gradle plugin in kotlin

Are you going to create a Gradle plugin in Kotlin? Check out these useful tips!

Background

There are many tutorials about writing custom Gradle plugins on the Internet. For example this official one. However, most of them use Groovy and plugins consisting of “Hello world” only. In this post, we will solve a real problem with a custom Gradle plugin written in Kotlin!

The problem

There is no built-it support for code coverage in the Gradle TestKit. Some snippets have been proposed on Gradle’s discussion forum. OK, there is a solution, so what is the point in doing something more?

Buildscript snippets may be good at the beginning of a single project. Nevertheless, they are less maintainable when required across multiple projects. Gradle offers several more plugin packaging options. Instead of copy-pasting code snippets, you can create a standalone Gradle plugin, deploy it to the repository and apply (like java or com.android.application plugins). android-apt – one of the famous plugins for Android projects evolved from snippets.

The solution

In order for JaCoCo to work in a project under TestKit, we need to tell its JVM to set Java agent using -javaagent: argument. For the purpose of our task, it is enough to know that the agent is required to instrument programs (so JaCoCo can collect its data), see java.lang.Instrument Javadoc for more info about agents.

The easiest way to pass arguments to JVM spawned by TestKit is to specify it in org.gradle.jvmargs property in the gradle.properties file used by the project under test. So far, everything sounds pretty simple. Let’s make a plugin for that!

Vocabulary

Before we begin coding, we have to get familiar with the basic theory. Pretty much nobody likes theory, so there will only include the absolute minimum. Let’s meet core Gradle element names.

  • Project – main Gradle API class, entry point to nearly all operations, corresponds to IntelliJ/Android Studio module
  • Task – a piece of work with actions, inputs and outputs, all having a descriptive name e.g. build
  • Configuration – file collections containing artifacts e.g. implementation or testImplementation
  • Dependency – a particular artifact written using dependency notation e.g. org.assertj:assertj-core:3.6.2

Kotlin

Kotlin met Gradle on May 2016 but gradle-script kotlin is still in pre-release at the time of writing. Hopefully, we aren’t gonna need it. We will use Kotlin in source sets but not in the buildscript itself.

Let’s start coding. First, we need to create a Gradle project and add base dependencies. We’ll also add test-only dependencies so we can even use TDD. The Initial buildscript looks like this:

There are several kinds of dependencies here:

Properties file generation

Let’s start from the most obvious part. We need to generate the gradle.properties file with appropriate content. Namely, it should contain this: org.gradle.jvmargs:-javaagent:$jacocoRuntimePath=destfile=$jacocoDestFile. Here, $jacocoRuntimePath is a path to JaCoCo runtime JAR and $jacocoDestFile is an execution data file location (which will be populated during unit tests with coverage).

Execution data file

The built-in JaCoCo plugin writes to the jacoco/test.exec file (path relative to project build directory) in append mode by default. So, we can just use the same file and all execution data will be located in a single file, requiring no further configuration changes (e.g. HTML reports will include entire data out of the box). The complete snippet for execution data file looks like this:val jacocoDestFile = "${project.buildDir}/jacoco/test.exec".

JaCoCo runtime

Getting $jacocoRuntimePath value requires a little bit more work. JaCoCo runtime JAR is distributed via public repositories – e.g. jCenter or Maven Central – just like most of the libraries you use in dependencies stanza. This means we can use the same mechanism which Gradle uses to resolve dependencies. We just need to create a configuration (let’s call it jacocoRuntime) and add JaCoCo runtime to its dependencies. Gradle will resolve the URL of the JAR, download and cache it for us!

Creating own configuration and its dependencies will be covered in the next sections. For now, assume that they are ready to use and let’s look at how to use them. Here is a configuration: val jacocoRuntimeConfiguration: Configuration = project.configurations.getByName("jacocoRuntime").
Note that the getByName method is defined in Java class, so Kotlin compiler doesn’t know whether it can return null or not. In this particular case, it cannot, so we explicitly declare its type as non-nullable – : Configuration (there would be : Configuration? if it was nullable).

Finally, we can retrieve the path of the JaCoCo runtime JAR with jacocoRuntimeConfiguration.asPath. Such a configuration is generally a file collection but this particular one is fully controlled by us and is guaranteed to contain exactly one element, so the asPath method will always return a path to a single file.

Saving output properties file

OK, we have content for a properties file but where it should be saved? Well, we can choose the arbitrary location inside the build directory (because it is a product of a task) which doesn’t clash with other files. We’ll later add that path to the runtime classpath, so our generated properties file will be available as a resource.

In Kotlin, text files can be created with one, handy extension method – File#writeText. There’s no need to deal with OutputStreams, buffering, charset (UTF-8 is always default), etc. A file will be created if it does not exist and overwritten if something already exists. The latter matches our needs since – e.g. if JaCoCo version changes (as well as its runtime JAR) – we just need a property with a new path instead of appending the new entry to the old one.

However, there is an important thing that needs to be noticed. The file itself will be created if it does not exist but its parent directory has to exist beforehand. It won’t be created automatically. We need to ensure this parent directory existence ourselves. This can be done as an extension method:

First, we try to create the parent directory with all necessary ancestors. File#mkdirs returns false if this target already exists. This is also a success for us, so we check for that case. Finally, if the directory has not been created and it does not exist (e.g. there is no space left) we throw an exception. Note that throw is an expression in Kotlin which is also very handy.

This extension method is very simple but there are, however, several important details. Firstly, we start with File#mkdirs and then optionally check if the file is a directory. This is slightly better than the opposite order.

Imagine a case when multiple threads are creating the same directory structure at the same time (they do not need to execute code from this plugin). If they would check the directory existence first, there could be a race condition where more than one thread is calling File#mkdirs at the same time and all calls (except one) fail, yet this is not a failure for us, as we still have the desired result (the created directory). If we use such an order, like in the snippet above, then only the final result matters, even if there were concurrent creations. This is a rather rare edge case but can be supported with no costs.

Secondly, we use File#isDirectory instead of File#exists since the parent of properties file has to be a directory, not a regular or special file (like device or pipe).

Thirdly, throwing an exception, if the parent can not be created, it is not necessary because the file writing will fail with an IOException (some of its subclasses preferably). However, the latter exception could not be so descriptive e.g. FileNotFoundException: /foo/bar (Not a directory).

Input and output

Business logic is not only part of the Gradle task. It is also important to properly declare task inputs and outputs. This prevents doing work that is already done. Gradle can check if any input or output has changed from a previous invocation and, if it hasn’t, the task won’t be executed and will be marked as UP-TO-DATE in Gradle’s console output. In this particular case, it does not make sense to overwrite the existing properties file with the same content.

Our task has one output – a generated properties file. The simplest way to declare this is to annotate it with @OutputFile like this:

Project#testKitDir is a custom extension function.

There is also one input, which is the jacocoRuntime configuration. It is modified whenever the JaCoCo version changes. The @Input annotation can be used to annotate this kind of input – not to be confused with @InputFile! Runtime JAR is indeed a file but input here is a configuration (which can be resolved to a file).

The finishing touch

Apart from the aforementioned logic, there are few minor technical issues related to our task. Firstly, on Windows, javaagent path needs to be adjusted; namely, we need to replace backslashes (\) with forward slashes (/). Secondly, the class has to be open. We won’t create subclasses explicitly but Gradle creates wrappers around task classes. Here is a complete task implementation:

Note that we inherit from DefaultTask and the function performing actual work (action) is annotated with @TaskAction. We also set group and description, they will be displayed in various places (e.g. Gradle projects view in Android Studio/IntelliJ) visible to users of our plugin.

Retrieving JaCoCo runtime JAR path

Let’s explain the configuration mentioned before. Configuration is just a file collection but elements are added to it in a special way. You probably noticed that the dependencies stanza entries look like this: testImplementation 'junit:junit:4.12'. testImplementation here is a configuration, added by the Kotlin Gradle plugin in this particular case.

Before we can add anything to a collection, we need to create it: configurations.maybeCreate(jacocoRuntime). This maybe prefix ensures the configuration will be created if it does not already exist and nothing will happen if it exists. Then, we can add an artifact to it:

Where does jacocoVersion come from? The easiest way is to use the value from jacoco extension, used by built-in plugin:

If the extension does not exist in the project, we fall-back to a default version (which is 0.7.8 in Gradle 3.5).

Sharing generated gradle.properties with project under test

One of the ways to make generated gradle.properties available in a project under test is to add that file to the test runtime classpath. Like this:

It will become a resource which can later be read using ClassLoader#getResourceAsStream() or similar methods (see javadoc for more details).

Note that the proper test runtime configuration name depends on the Gradle version. Starting from 3.4, it is called testRuntimeOnly and the old testRuntime is deprecated. Gradle 3.4 was only released on Feb 20, 2017, so we want to support users of older versions as well, choosing not to use the deprecated name if possible. To achieve this, we extract the configuration name to a variable which will be populated depending on the current Gradle version at runtime.

Assembling all those elements together

To connect a task and needed configurations, we can use a plugin. Task generating properties file can be added as a dependency to a built-in test task. It will be invoked before unit tests, so the generated file will be guaranteed to be available during tests.

We need to just implement Plugin<Project> which has one abstract apply method. As the name suggests, it will be called when a plugin is applied to the project, which happens exactly once for this given project. This application happens when the plugin is specified in plugins stanza or applied literally through apply plugin: for example.

The last important thing to notice is that all objects provided by other plugins, such as configurations, tasks and extensions, may not be available when the plugin is applied (e.g. because the plugin providing them has not been applied yet). We have to wait until they are created. For configuration and tasks collections, we can use the all method, which iterates over all elements: both those currently present and added in the future. There is no such mechanism for extensions but we can use afterEvaluate for this purpose.

A complete snippet looks like this:

Note that there is a with(project) block which will allow us to omit beginning each statement with project.. It’s a part of Kotlin’s standard library. Additionally, we set a description of the configuration (can be read by users) and made it invisible from outside of the project when the plugin is applied.

Testing Gradle plugins

Code not using Gradle API can be tested like any other (non-plugin) project. Common test libraries like JUnit or AssertJ can be used in all test sources. Additionally, Gradle provides two mechanisms specific for plugins: ProjectBuilder and TestKit.

ProjectBuilder

This class can provide a dummy project which can be used for tests not dealing with the project lifecycle. There is no API for evaluating projects or invoking tasks. ProjectBuilder tests for this plugin can be found on the GitHub repository.

TestKit

Gradle TestKit is designed for the functional tests which consist of executing the Gradle process in a separate JVM. This environment cannot be accessed programmatically. You cannot access the task graph to check if it contains a given task but you can, for example, try to invoke a particular task and examine (console) the build output and workspace. Examples of these kind of tests can also be found on the GitHub repository.

Publishing plugins to the Gradle plugin portal

You can publish your plugins to a Gradle plugin portal. Doing so simplifies plugin adoption by users. You need to register an account on a portal and retrieve your personal API key. Additionally publish plugin has to be applied and the plugin descriptor has to be added to the build.gradle file. The descriptor of this plugin looks like this:

SNAKE_CASE variables come from gradle.properties, which can be found here.

Usage

After a plugin is published, everyone can find instructions regarding how to apply it on the Gradle plugin portal. Every plugin has its own page with detailed instructions. For example here is the page of a plugin from this blog post.

Wrap up

As you can see, creating a Gradle plugin is very easy, especially in Kotlin. Plugins published to Gradle Plugin Portal represent a convenient way to reuse a buildscript code in multiple projects.

About the author

Karol Wrótniak

Karol Wrótniak

Mobile Developer

Flutter & Android Developer with 12 years of experience. A warhorse with impressive experience and skills in native and Flutter app development. Karol is probably the most active contributor to open source libraries you've ever met. He develops Gradle plugins and Bitrise steps, and he is engaged in many projects, in particular those related to testing.

Karol has been engaged as a speaker in many events and meetups like DevFest, 4Developers Wrocław, JDD Conference, Linux Academy, and more. He is an active member of Google Developers Group Wrocław, Flutter Wrocław, and Bitrise User Group.