Plug-in development

How to write a Kobalt plug-in.

Introduction

Kobalt plug-ins are usually made of several parts:

If you are curious to get a quick feel for what a Kobalt plug-in looks like, I suggest you go read how to write and publish a plug-in in ten minutes and then you can come back here and keep reading.

kobalt-plugin.xml

The kobalt-plugin.xml file (stored in META-INF in the jar file of your plug-in) is mandatory and describes all the components of your plug-in. At a minimum, this file will contain the name of your plug-in and the main plug-in class:

<kobalt-plugin>
    <name>kobalt</name>
    <plugins>
        <class-name>com.beust.kobalt.plugin.android.AndroidPlugin</class-name>
    </plugins>
</kobalt-plugin>

This file can also contain Contributors, which are the main mechanism that Kobalt plug-ins use to interact with each other.

Contributors

Plug-ins often produce files and data that other plug-ins need to use in order for a build to succeed. For example, the Android plug-in needs to generate a file called R.java and then make this file available at compile time by the Java or Kotlin (or any other language) plug-in. Since plug-ins have no idea about what other plug-ins are currently enabled and running, they can't directly talk to each other so instead of calling into Kobalt, Kobalt calls into them. This is done by declaring various contributors that Kobalt will invoke whenever it needs the information that your plug-in produced. This is a design pattern often referred to as the "Hollywood Principle": "Don't call us, we'll call you".

In order to make things more concrete, let's take a look at Kobalt's own kobalt-plugin.xml and go over it line by line.

plugins (IPlugin)

    <plugins>
        <class-name>com.beust.kobalt.plugin.android.AndroidPlugin</class-name>
        <class-name>com.beust.kobalt.plugin.application.ApplicationPlugin<class-name>

Kobalt defines a few plug-ins in its core so you never need to download them.

Classpath contributors (IClasspathContributor)

    <classpath-contributors>
        <class-name>com.beust.kobalt.plugin.android.AndroidPlugin</class-name>
        <class-name>com.beust.kobalt.plugin.kotlin.KotlinPlugin</class-name>

Classpath contributors let you specify additional jar files or directories that will be used by the compile task. In the above example, the KotlinPlugin adds the Kotlin runtime to the classpath and Android adds various Android resources (e.g. aar files) to it as well.

Project contributors (IProjectContributor)

    <project-contributors>
        <class-name>com.beust.kobalt.plugin.java.JavaPlugin</class-name>
        <class-name>com.beust.kobalt.plugin.kotlin.KotlinPlugin</class-name>

Some plug-ings produce projects (Java, Kotlin) while others don't (Packaging, Application, etc...). The ones that do need to register themselves as project contributors. This is how Kobalt collects all the projects defined after a build file was parsed.

Init contributors (IInitContributor)

    <init-contributors>
        <class-name>com.beust.kobalt.plugin.java.JavaBuildGenerator</class-name>
        <class-name>com.beust.kobalt.plugin.kotlin.KotlinBuildGenerator</class-name>

Kobalt supports the --init command line parameter, which generates a default build file based on the files found in the current directory. Any plug-in that wants to be part of this process need to specify Init Contributors. In this case, both the Java and Kotlin plug-ins define such a contributor but future plug-ins might use this contributor to generate their own build file: Android, Ceylon, Spring, etc...

You can take a look at the IInitContributor interface to find out more details but in a nutshell, each Init Contributor is asked how many files in the current directory their plug-in handles and the contributor with the highest number of files is then asked to generate the build file.

Repo contributors (IRepoContributor)

    <repo-contributors>
        <class-name>com.beust.kobalt.plugin.android.AndroidPlugin</class-name>

Some plug-ins might want to add their own repository to the list of repositories that Kobalt already supports. This is the case of the Android plug-in which, once the ANDROID_HOME environment variable has been defined, will automatically add the repository inside the Android distribution so that support libraries and other artifacts can be found.

Compiler flag contributors (ICompilerFlagContributor)

Plug-ins can add flags to the compiler by implementing this interface.

Directives

Directives are functions that users of your plug-in can use in their build file in order to configure your plug-in. These can be any kind of Kotlin function but in the interest of preserving a clean syntax in the build file, it's recommended to use the type safe builder pattern, as described here.

Imagine that you want to offer a boolean parameter publish to users of your plug-in, you start by creating a class to hold that parameter:

class Info(val publish: Boolean)

Next, you create a directive that returns such a class and which also allows to configure it via the type safe builder pattern:

@Directive
public fun myConfig(init: Info.() -> Unit) : Info {
    val info = Info()
    info.init()
    return info
}

The @Directive annotation is not enforced but you should always use it in order to help future tools (e.g. an IDEA plug-in) identify Kobalt directives so they can be treated differently from regular Kotlin functions.

Users can now specify the following in their build file:

// Build.kt
import.com.example.plugin.myConfig

myConfig {
    publish = true
}

If you need access to the project being built, just declare an additional parameter of type Project to your directive and have the user pass that project:

@Directive
public fun myConfig(project: Project, init: Info.() -> Unit) : Info {
// ...
myConfig(project) {
    publish = true
}

The last piece of this puzzle is how you give this data back to your plug-in so it can act on it. In order to do this, you simply look up the name of your plug-in in the Plugins registry and invoke whatever function you need to run:

@Directive
public fun myConfig(project: Project, init: Info.() -> Unit) : Info {
    val info = Info()
    info.init()
    (Kobalt.findPlugin("my-plug-in") as MyPlugin).info = info
    return info
}

Obviously, you can choose any kind of API to communicate between the directive and its plug-in. In the code above, I chose to directly overrid the entire Info field, but you could instead choose to call a function, just set one boolean instead of the whole object, etc...

Tasks

Tasks are provided by plug-ins and can be invoked from the command line, e.g. ./gradlew assemble. There are two kinds of tasks: static and dynamic.

Static tasks

Static tasks are functions declared directly in your plug-in class and annotated with the @Task annotation. Here is an example:

@Task(name = "lineCount", description = "Count the lines", runBefore = arrayOf("compile"))
fun lineCount(project: Project): TaskResult {
    // ...
    return TaskResult()
}

A Kobalt task needs to accept a Project in parameter and return a TaskResult, which indicates whether this task completed successfully.

Request for feedback

Having the Project passed in both the apply() function and in each task feels redundant, although it avoids the trouble from having to store that project in a field of the plug-in, making it essentially stateless.

The @Task annotation accepts the following attributes:

name
The name of the task, which will be used to invoke it from the command line.
description
The description of this command, which will be displayed if the user invokes the usage for the gradlew command.
runBefore
A list of all the tasks that this task should run prior to.
runAfter
A list of all the tasks that should run before this task does.
alwaysRunAfter
A list of all the tasks that will always be run after this task if it's invoked.

The difference between runAfter and alwaysRunAfter is subtle but important. runAfter is just a declaration of dependency. It's basically the reverse of runBefore but it's useful in case you are not the author of the task you want to run before (if you were, you would just use the runBefore annotation on it). Since you can't say "a runBefore b" because you don't own task "a", you say "b runAfter a".

For example, compileTest is declared as a runAfter for the task compile. This means that it doesn't make sense to run compileTest unless compile has run first. However, if a user invokes the task compile, they probably don't want to invoke compileTest, so a dependency is exactly what we need here: invoking compileTest will trigger compile but not the other way around.

However, there are times where you want to define a task that will always run after a given task. For example, you could have a signJarFile task that should always be invoked if someone builds a jar file. You don't expect users to invoke that target explicitly, but whenever they invoke the assemble target, you want your signJarFile target to be invoked. When you want such a task to always be invoked even if the user didn't explicitly request it, you should use alwaysRunAfter. Note that there is no alwaysRunBefore annotation since runBefore achieves the same functionality.

Here are a few different scenarios to illustrate how the three attributes work for the task exampleTask:

Result of the command ./kobaltw --dryRun compile

Configuration for exampleTask Result
runBefore = "compile"
kobalt-line-count:clean
kobalt-line-count:exampleTask
kobalt-line-count:compile
runAfter = "compile"
kobalt-line-count:clean
kobalt-line-count:compile
alwaysRunAfter = "compile"
kobalt-line-count:clean
kobalt-line-count:compile
kobalt-line-count:exampleTask

Dynamic tasks

Dynamic tasks are useful when you want your plug-in to generate one or several tasks that depend on some other runtime information (therefore, you can't declare a method and put a @Task annotation on it). Here is the simplest dynamic task you can create in your plug-in class:

override fun apply(project: Project, context: KobaltContext) {
    println("*** Adding dynamic task")
    addTask(project, "dynamicTask") {
        println("Dynamic task")
        TaskResult()
    }
}

Like a regular task method, the closure you pass to addTask() has to return a TaskResult object to indicate whether it succeeded or failed. You can then see your dynamic task in the list of tasks and run it directly:

$ ./kobaltw --tasks
  ===== kobalt-line-count =====
    dynamicTask
    lineCount           Count the lines
$ ./kobaltw dynamicTask
Dynamic task

The addTask() method lets you specify any attribute you can specify on the @Task annotation: description, runBefore, etc... For example, here is how we would specify that this task should always run after compile:

addTask(project, "dynamicTask", alwaysRunAfter = listOf("compile")) {
    println("Dynamic task")
    TaskResult()
}

Let's test it:

$ ./kobaltw --dryRun compile
kobalt-line-count:clean
kobalt-line-count:compile
kobalt-line-count:exampleTask

Properties

Properties are the mechanism that plug-ins can use to export values and also read values that other plug-ins have exported. There are two kinds of properties that plug-ins can manipulate:

Project properties

Project instances have a property called projectProperties that is an instance of the ProjectProperties class. Plugins can put and get values on this object in order to store project specific properties.

fun taskAssemble(project: Project) : TaskResult {
    project.projectProperties.put(PACKAGES, packages)
          

Plug-in properties

The PluginProperties instance can be found on the KobaltContext object that your plug-in receives in its apply() method. Once you have an instance of this class, you can read or write variables into it:

override fun apply(project: Project, context: KobaltContext) {
    // Export a property for other plug-ins to use
    context.pluginProperties.put(PLUGIN_NAME, "somePluginProperty", "someValue")

    // Read a property from another plug-in
    val sourceDir = context.pluginProperties.get("pluginName", "somePluginProperty")
}

Documenting properties

Plug-ins that define properties should annotate them with the @ExportedPluginProperty or @ExportedProjectPropertyannotation:

    companion object {
        @ExportedProjectProperty
        const val BUILD_DIR = "buildDir"