• Part 1 (Introduction)
  • Part 2: (Building a Gradle Plugin)
  • Part 3: (Generating data classes)
  • Part 4: (Writing to files)
  • Part 5: (Handling Multi Platform)
  • Part 4: (Appending functions to the data classes)

With the mentality of sprint zero, we're going to spend the first two parts getting a work space set up. I think this is also the part most apt to trip up new comers. We're going to focus on making a build plugin, and integrating it into a CI/CD. This will be based off the prior simple data class.

We will have to modify several gradle kts files. With the end goal of.

  • A plugin module that is the gradle plugin
  • Publishing the plugin to maven local
  • Having an example project consuming the plugin from
  • A helper script to publish the module to maven local.

Settings.gradle.kts

This will largely crib from my prior guide on setting up Kotlin multi platform builds.  The key item here is how plugins are resolved. Much like mavenLocal for dependencies, we're adding mavenLocal for the plugins. You can publish the plugin to Gradle central, but this is assuming local internal one off type providers.

import java.net.URI

enableFeaturePreview("GRADLE_METADATA")
rootProject.name = "kotlin-frm"

pluginManagement {
  if (requested.id.id.startsWith("design.animus.kotlin.example.typeproviders")) {
                // Update Version Build Source if being changed.
                useVersion("0.1.0-SNAPSHOT")
            }

    ...
    repositories {
        maven { url = java.net.URI("https://dl.bintray.com/kotlin/kotlinx") }
        mavenCentral()
        mavenLocal()
        jcenter()
        gradlePluginPortal()
    }
}

The second item of note is the useVersion syntax. This will allow us to use the plugin we build in several modules, without having to increment the plugin version. This allows us limit the number of places we have to modify the version.

At the bottom of the settings.gradle.kts we will have two includes. One for the plugin, the other for an example project.

include(":plugin:")
include(":example:")

A problem will occur with the example project. The build will fail because the necessary type provider plugin is not present in maven local. We will need to comment and uncomment that module. While the plugin is being built.

Building the Plugin Build Script

We need to apply two plugins in the build block.

plugins {
    kotlin("jvm")
    ...
    id("com.gradle.plugin-publish") version "0.10.1"
    id("java-gradle-plugin")

}
gradlePlugin {
    plugins {
        create("TypeProvider") {
            id = "design.animus.kotlin.example.typeproviders"
            implementationClass = "design.animus.kotlin.example.typeproviders.TypeProvider"
        }
    }
}

pluginBundle {
    website = "http://example-typeprovider.com"
    vcsUrl = "https://gitlab.com/AnimusDesign/examples/TypeProvider"
    description = "Sample Type Provider."
    tags = listOf("kotlin", "database", "multiplatform")
}

The main portion of this configuration is to say.

  • What the plugin id will be.
  • What class to call when applying the plugin.

Building the Plugin

$repoRoot/plugin/main/design/animus/kotlin/example/typeprovider/TypeProvider.kt

open class TypeProviderConfig {
    var nameSpace: String = "design.animus.tutorials.typeprovider"
    var targetDirectory: String = "./"
}

class TypeProvider : Plugin<Project> {
    override fun apply(project: Project) {
      ...
    }
}

The first class we provide is a configuration class. This needs to be open as it will be mutated, we also set sane defaults.

  • nameSpace Sets the package name prefix where the code will live.
  • targetDirectory Denotes where the generated code will reside.

The following code snippets will reside in the override fun apply. I will be showing them line by line.

val extension = project.extensions.create<TypeProviderConfig>(
 "TypeProviderConfig",
  TypeProviderConfig::class.java
)

This will allow the plugin to provide a configuration class of TypeProviderConfig, this is covered below in the example module.

project.task("generateTypes").doLast {
            val file = FileSpec.builder(extension.nameSpace, "TypeProviderSample")
            val sampleClass = TypeSpec.Companion.classBuilder("User")
                    .addModifiers(KModifier.DATA)
                    .primaryConstructor(
                            FunSpec.constructorBuilder()
                                    .addParameter(
                                            ParameterSpec.builder("id", Int::class)
                                                    .build()
                                    )
                                    .build()
                    )
                    .addProperty(
                            PropertySpec.builder("id", Int::class)
                                    .initializer("id")
                                    .build()
                    )
                    .build()
            file.addType(sampleClass)
                    .build()
                    .writeTo(Paths.get(extension.targetDirectory))

}

Next we register the logic to build the sample data class. This will be moved out to another module later on. But for the time being this will just generate the sample class noted in the first portion.

This will make it callable via

./gradlew :example:generateTypes

You can rename the task to something else if preferable.

Publishing the Plugin

As noted one of the big problems you'll encounter is a dependency error. The example project depends on a plugin that is yet to be published.

$repoRoot/scripts/publish-local.sh

#!/usr/bin/env bash
if [[ $(grep -q -E '^include\(":example:"\)' settings.gradle.kts) ]]
then
  echo "Disabling example"
  sed -i s'/include(":example:")/\/\/include(":example:")/g' settings.gradle.kts;
fi

echo "Building plugin"
./gradlew :plugin:publishToMavenLocal
echo "Re-enabling example project"
sed -i 's/\/\/include(":example:")/include\(":example:"\)/g' settings.gradle.kts
echo "Attempting to build example"
./gradlew :example:build
echo "Running Plugin"
./gradlew :example:generateTypes

This script will check and ensure that the example project is disabled. If not it will disable, the re-enable it. Running the actual task to verify everything is configured properly.

Configuing the Example Project

Now that we have the gradle plugin configured we can consume and use it.

$repoRoot/example/build.gradle.kts

plugins {
    kotlin("jvm")
    id("org.jetbrains.kotlin.plugin.serialization")
    id("design.animus.kotlin.example.typeprovider")
}

We pull in the plugin we just created. But we don't need to specify a version because that is set in the settings.gradle.kts.

TypeProviderConfig {
    targetDirectory = "$projectDir/generated"
}

This references back to the configuration class we made. Here we set the output of the generated code to ./examplegenerated. Now we need to make this a main source root. Below is the configuration to add generated to the main source list.

kotlin {
    sourceSets {
        main {
            kotlin.setSrcDirs(
                    mutableListOf("main", "generated")
            )
        }
        test {
            kotlin.srcDir("test")
            resources.srcDir("test/resources")
        }
    }
}

$repoRoot/example/generated/design/animus/tutorials/typeprovider/TypeProviderSample.kt

This generates the following sample file. As noted the imports

package design.animus.tutorials.typeprovider

import kotlin.Int

data class User(
  val id: Int
)

Next we will show this in a CI/CD process.