Plug-in development

How to set up your environment to write a Kobalt plug-in.

Introduction

Kobalt plug-ins are usually made of two parts.

We'll cover these two items shortly but first of all, let's go over a quick example that will show you the whole process of writing a plug-in from scratch and publishing it on JCenter in ten minutes.

Writing and publishing a plug-in in ten minutes

In this example, we'll write a Kobalt plug-in that simply counts the number of source files and lines in your project. Starting from scratch, we'll have this plug-in published to JCenter and ready to use in ten minutes.

Let's start by creating our project:

$ mkdir linecount
$ mkdir -p src/main/kotlin/com/beust/plugin/linecount
$ touch src/main/kotlin/com/beust/plugin/linecount/Main.kt
$ $KOBALT_HOME/kobaltw --init

I create an empty Main.kt in the example above so that calling ./kobaltw --init will detect the project as a Kotlin one. This way, the Build.kt file generated is already configured for Kotlin. Since we will be publishing this project to a Maven repository, we need to make sure that its group, artifactId and version are correct. The only thing that the generator can't guess is the group, so let's go ahead and fix it:

val project = kotlinProject {
    name = "kobalt-line-count"
    group = "com.beust.kobalt"
    artifactId = name
    version = "0.1"
    ...

Next, we want the manifest of our jar file to point to our main Kobalt plug-in class:

val packProject = packaging(project) {
    jar {
        manifest {
            attributes("Kobalt-Plugin-Class", "com.beust.kobalt.plugin.linecount.Main")
        }
    }
}

Now we're ready to code.

Let's start by writing the simplest plug-in we can:

package com.beust.kobalt.plugin.linecount

import com.beust.kobalt.api.*

public class Main : BasePlugin() {
    override val name = "kobalt-line-count"

    override fun apply(project: Project, context: KobaltContext) {
        println("*** Applying plugin ${name} with project ${project}")
    }
}

Before we can upload it, we need to create the package in bintray, as explained here. Once this is done, we are ready to do our first upload:

$ ./kobaltw uploadJcenter
...
========== kobalt-line-count:uploadJcenter
  kobalt-line-count: Found 2 artifacts to upload
  All artifacts successfully uploaded
  ############# Time to Build: 3590 ms

If you go to the maven section of your bintray account, you will now see that the new package has two unpublished files. Your new plug-in won't be visible by clients until you publish those files, so let's update our build file to automatically publish files from now on:

val jc = jcenter(project) {
    publish = true
}

And now, let's implement our logic, which is pretty simple:

// Main.kt
@Task(name = "lineCount", description = "Count the lines", runBefore = arrayOf("compile"))
fun lineCount(project: Project): TaskResult {
    var fileCount = 0
    var lineCount : Long = 0
    val matcher = FileSystems.getDefault().getPathMatcher("glob:**.kt")
    project.sourceDirectories.forEach {
        Files.walkFileTree(Paths.get(it), object: SimpleFileVisitor() {
            override public fun visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult {
                if (matcher.matches(path)) {
                    fileCount++
                    lineCount += Files.lines(path).count()
                }
                return FileVisitResult.CONTINUE
            }
        })
    }
    log(1, "Found ${lineCount} lines in ${fileCount} files")
    return TaskResult()
}

We create a task called "lineCount" in which we look for all files ending in ".kt" in all the source directories of the project. Finally, we display a count of files and lines at the end by using KobaltLogger.log(), which is obtained by extending the trait KobaltLogger:

public class Main : BasePlugin(), KobaltLogger {

Let's bump our version to 0.2 (since version 0.1 is already uploaded and JCenter won't allow us to overwrite it) and upload our new plug-in:

$ ./kobaltw uploadJcenter
...
kobalt-line-count: Compilation succeeded
========== kobalt-line-count:assemble
Created /Users/beust/kotlin/kobalt-line-count/kobaltBuild/libs/kobalt-line-count-0.2.jar
========== kobalt-line-count:generatePom
 Wrote /Users/beust/kotlin/kobalt-line-count/kobaltBuild/libs/kobalt-line-count-0.2.pom
========== kobalt-line-count:uploadJcenter
kobalt-line-count: Found 2 artifacts to upload
All artifacts successfully uploaded

Time to Build: 5907 ms

Finally, let's use our plug-in from another project. Since we didn't link this project to JCenter, it's uploaded in the user's maven repository, so we will have to add this maven repository to the build file where we want to use the plug-in. Adjust this line to point to your own maven repo:

val repos = repos("https://dl.bintray.com/cbeust/maven/")
val plugins = plugins("com.beust.kobalt:kobalt-line-count:0.2")

Now let's launch a build:

$ ./kobaltw assemble
...
========== kobalt:lineCount
Found 4972 lines in 65 files
========== kobalt:compile
...

Note that our plugin ran before the compile task, as we requested in the @Task annotation. We can also verify that it's activated and we can invoke the task directly instead of having it run as part of the build:

$ ./kobaltw --tasks
  ===== kobalt-line-count =====
    lineCount		Count the lines
$ ./kobaltw lineCount
Found 4972 lines in 65 files

And that's it! You can now iterate on your plug-in and upload it with additional ./kobaltw uploadJcenter. This plug-in is available on github.

Debugging

The simplest way to run your plug-in in your IDE is to create a main function in the main class of your plug-in as follows:

fun main(argv: Array<String>) {
    com.beust.kobalt.main(argv)
}

public class Main : BasePlugin(), KobaltLogger {
// ...

Now you can simply create a launch configuration for your main class, which will invoke Kobalt.

The next step is to have Kobalt invoke your plug-in, so you will have to modify your build file to call it. As long as you haven't deployed your plug-in to JCenter, you might want to use the file() directive to declare your dependency, so that Kobalt will use the jar file on your file system:

val p = plugins(
    file(homeDir("kotlin/kobalt-line-count/kobaltBuild/libs/kobalt-line-count-0.8.jar"))
)

You can now set a breakpoint in your plug-in and launch the configuration you created above.

Initialization

When your plug-in is activated, Kobalt will invoke its apply() function:

override fun apply(project: Project, context: KobaltContext) {
}

project is the project that your plug-in is currently being initialized for (keep in mind there can be multiple projects in a build) and the context gives you some information about other external data you might find useful, such as the command line that was passed to Kobalt.

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