How to write a Kobalt plug-in.
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.
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 "actors" 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".
These "actors" are exactly what the kobalt-plugin.xml
file describes. This file informs Kobalt about
the various ways in which your plug-in participates in the build system by specifying 1) plug-ins, 2) contributors
or 3) interceptors.
kotlinProject
or dependencies
. These functions typically configure some data that your plug-in will later use to perform its functions.
The kobalt-plugin.xml
file (stored in META-INF
in the jar file of your plug-in) is mandatory and describes all the actors of your plug-in. This file contains a list of class names, each of which is expected to implement at least one of IPluginActor
's interfaces:
<plugin-actors> <class-name>com.beust.kobalt.plugin.java.JavaPlugin</class-name> <class-name>com.beust.kobalt.plugin.android.AndroidPlugin</class-name> <class-name>com.beust.kobalt.plugin.java.JavaBuildGenerator</class-name> <class-name>com.beust.kobalt.plugin.kotlin.KotlinBuildGenerator</class-name> </plugin-actors>
IPluginActors
can be split in several categories:
@Task
annotations.
All plug-in actors are interfaces that extend IPluginActor
. Plug-ins extend IPlugin
,
interceptors extend IInterceptor
and contributors extend IContributor
. When Kobalt parses your
kobalt-plugin.xml
, it instantiates all the classes found in the <plugin-actors>
tag
and then introspects them to find out which IPluginActor
interfaces that class implements.
If we look a the declarations of these classes, we can get an idea what they do
class JavaPlugin : ICompilerContributor, IDocContributor {
With this declaration, we know that the JavaPlugin
contributes a compiler and a doc generator.
class JavaBuildGenerator: IInitContributor {
This class is declaring that it wants to take part in the --init
selection process, discussed below.
Here is a list of actors (contributors and interceptors) that you can define in your plug-in.
Interface name | Type | Description |
IBuildConfigFieldContributor |
IInterceptor |
Plug-ins that want to add custom fields to the generated BuildConfig class.
|
IBuildDirectoryInterceptor |
IInterceptor |
Plug-ins that need to generate class files in a different directory than the default one should implement this interface. |
IClasspathContributor
|
IContributor |
Classpath contributors let you specify additional jar files or directories that will be used by
the "compile" task.
|
IClasspathInterceptor
|
IInterceptor |
Plug-ins that want to modify the classpath before Kobalt uses it should implement this interface. |
ICompilerContributor |
IContributor |
Plug-ins that know how to turn files into bytecodes should implement this interface. |
ICompilerFlagContributor |
IContributor
|
Plug-ins that need to add flags to the compiler. |
ICompilerInterceptor |
IInterceptor |
Plug-ins that implement this interface get a chance to alter the dependencies of a project (dependencies{} , dependenciesTest{} , ...) before Kobalt sees them.
|
IDocContributor |
IContributor |
Plug-ins that know how to generate documentation out of source files should implement this interface. |
IInitContributor |
IContributor |
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 implement this interface. 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...
|
IProjectContributor |
IContributor |
Some plug-ins 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. |
IRepoContributor |
IContributor |
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.
|
IRunnerContributor |
IContributor |
Plug-ins that can operate when the "run" task gets invoked should implement that interface.
|
ISourceDirectoryContributor |
IContributor
|
Plug-ins that add source directories. |
ISourceDirectoryInterceptor |
IInterceptor |
Plug-ins that want to add, remove or alter the source directories should implement this interface. |
ITestRunnerContributor |
IContributor |
Plug-ins that can operate when the "test" task gets invoked should implement that interface.
|
ITestSourceDirectoryContributor |
IContributor
|
Plug-ins that add test source directories. |
Several plug-ins might want to contribute to a specific task where only one participant should be allowed, such as running tests or generating documentation. Even the simple task of compiling should probably only ever be performed by no more than one plug-in for a given project. Therefore, when comes the time to compile a project, Kobalt needs to find which plug-in is the most suitable for that task and pick it. In order to do that, plug-ins that contribute to tasks that can only be performed by one candidate need to declare their affinity to that task for a given project.
Contributors that want to participate in a selection process need to implement the following interface:
interface IProjectAffinity { /** * @return an integer indicating the affinity of your actor for the given project. The actor that returns * the highest affinity gets selected. */ fun affinity(project: Project, context: KobaltContext) : Int }
For example, the JavaPlugin implements the ICompilerContributor
interface and then overrides
the affinity()
method to make sure it gets run for Java projects but ignored for others:
override fun affinity(project: Project, context: KobaltContext) = if (project.sourceSuffix == ".java") 1 else 0
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().apply { init() }
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. The code above defines a myConfig
function that accepts a closure as an argument. It creates an Info
object, calls the init()
function on it (which runs all the code inside that closure) and then return that Info
object.
Users can now specify the following in their build file:
// Build.ktort.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(init: Info.() -> Unit) = Info().apply { init() (Kobalt.findPlugin("my-plug-in") as MyPlugin).info = info this }
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 override the entire Info
field, but you could instead choose to call
a function, just set one boolean instead of the whole object, etc...
Tasks are provided by plug-ins and can be invoked from the command line, e.g. ./kobaltw assemble
. There are two kinds of tasks: static and dynamic.
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.
The @Task
annotation accepts the following attributes:
kobaltw
command.
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 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). Plug-ins declare dynamic tasks by implementing the ITaskContributor
intrface:
interface ITaskContributor { fun tasksFor(context: KobaltContext) : List<DynamicTask> }
For example:
override fun tasksFor(context: KobaltContext) = listOf( DynamicTask( name = "dynamicTask", description = "Description", alwaysRunAfter = listOf("compile"), closure = { project: Project -> println("Running dynamicTask") TaskResult() }))
DynamicTask
mirrors the @Task
attributes: name
, description
and
dependencies. The only addition is the closure
parameter, which specifics the code that will
run if your task gets invoked. That closure needs to follow the same constraints that a @Task
method
obeys: it takes a Project
parameter and returns a TaskResult
.
Once you have implemented ITaskContributor
, you can see your dynamic task in the list of tasks and run it directly:
$ ./kobaltw --tasks ===== kobalt-line-count ===== dynamicTask Description lineCount Count the lines $ ./kobaltw dynamicTask Running dynamictask
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
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)
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") }
Plug-ins that define properties should annotate them with the @ExportedPluginProperty
or
@ExportedProjectProperty
annotation:
companion object { @ExportedProjectProperty const val BUILD_DIR = "buildDir"