The repository contains everything needed to start the workshop.
- The
build-logic
included build contains the Gradle plugin which is going to be created. - The
application
module will be used as a simple application to run the generated code by the plugin.
To run the application, use the next CLI command:
./gradlew run
Create the Gradle plugin by extending the `Plugin` interface.
- Right-click on the
build-logic
module. - Create the directory
src/main/kotlin/com/qonto/
. - Create the file
QontoPlugin.kt
in the directory. - Create the class
QontoPlugin
and extends thePlugin
interface usingProject
as its type parameter.
package com.qonto
import org.gradle.api.Plugin
import org.gradle.api.Project
class QontoPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.logger.quiet("Hello from QontoPlugin!")
}
}
Register the plugin in the `build-logic` module with the `qonto` id.
- Open the
build.gradle.kts
file inbuild-logic
module. - Add the following code to the file below the plugins block.
plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
register("QontoPlugin") {
id = "qonto"
implementationClass = "com.qonto.QontoPlugin"
}
}
}
Add it to the version catalog.
- Open the
libs.versions.toml
file inside thegradle
directory. - Add the plugin to the bottom of the
plugins
section and sync the Gradle project.
[versions]
kotlin = "2.0.21"
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
qonto = { id = "qonto" } # Add this line
Apply the plugin in the `application` project.
- Open the
build.gradle.kts
file inside theapplication
project. - Apply the plugin in the
plugins
block.
plugins {
application
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.qonto) // Add this line
}
application {
mainClass = "com.qonto.application.MainKt"
}
group = "com.qonto"
version = "1.0.0"
Create a task with the minimum amount of code.
- Create the file
QontoGenerateProjectDataTask.kt
in thecom.qonto
package. - Create the class
QontoGenerateProjectDataTask
class and extends theDefaultTask
class.
package com.qonto
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.logging.Logger
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.register
import org.slf4j.LoggerFactory
open class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger
) : DefaultTask() {
init {
group = "qonto"
description = "Generates the project data"
}
@TaskAction
fun run() {
logger.quiet("Generating project data...")
}
companion object {
const val NAME: String = "generateProjectData"
fun register(project: Project) {
val generateProjectData: TaskProvider<QontoGenerateProjectDataTask> =
project.tasks.register<QontoGenerateProjectDataTask>(
name = NAME,
LoggerFactory.getLogger("qonto"),
)
}
}
}
Register the task.
- Call the
register
method on the taskcompanion object
within theapply
block in the plugin.
package com.qonto
import org.gradle.api.Plugin
import org.gradle.api.Project
class QontoPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.logger.quiet("Hello from QontoPlugin!")
QontoGenerateProjectDataTask.register(target) // Add this line
}
}
Apply the base plugin.
- Use the
pluginManager
to apply theBasePlugin
plugin
package com.qonto
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.BasePlugin // Add this line
import org.gradle.kotlin.dsl.apply // Add this line
class QontoPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply(BasePlugin::class) // Add this line
target.logger.quiet("Hello from QontoPlugin!")
QontoGenerateProjectDataTask.register(target)
}
}
Wire the task with the `assemble` task.
- Use the
named
method on thetasks
to get theassemble
task. - Use
dependsOn
to make theassemble
task depend on thegenerateProjectData
task.
package com.qonto
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.logging.Logger
import org.gradle.api.plugins.BasePlugin // Add this line
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.register
import org.slf4j.LoggerFactory
open class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger
) : DefaultTask() {
init {
group = "qonto"
description = "Generates the project data"
}
@TaskAction
fun run() {
logger.quiet("Generating project data...")
}
companion object {
const val NAME: String = "generateProjectData"
fun register(project: Project) {
val generateProjectData: TaskProvider<QontoGenerateProjectDataTask> =
project.tasks.register<QontoGenerateProjectDataTask>(
name = NAME,
LoggerFactory.getLogger("qonto"),
)
// Add these lines
project.tasks.named(BasePlugin.ASSEMBLE_TASK_NAME).configure {
dependsOn(generateProjectData)
}
}
}
}
Make the task cacheable.
- Add the
@CacheableTask
annotation to theQontoGenerateProjectDataTask
class.
package com.qonto
// ...
import org.gradle.api.tasks.CacheableTask // Add this line
// ...
@CacheableTask // Add this line
open class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger
) : DefaultTask() {
// ...
}
Add inputs to the task and configure them.
- Use the
@Input
annotation to mark the properties as inputs in theQontoGenerateProjectDataTask
. - Wire them within the
configure
method block from theTaskProvider
. - Use the
provider
lambda to do lazy evaluation of the provided properties.
package com.qonto
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.logging.Logger
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.register
import org.slf4j.LoggerFactory
@CacheableTask
open class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger,
private val objects: ObjectFactory,
) : DefaultTask() {
@Input
val projectGroup: Property<String> = objects.property()
@Input
val projectName: Property<String> = objects.property()
@Input
val projectVersion: Property<String> = objects.property()
init {
group = "qonto"
description = "Generates the project data"
}
@TaskAction
fun run() {
logger.quiet("Generating project data...")
logger.quiet("Project group: ${projectGroup.get()}")
logger.quiet("Project name: ${projectName.get()}")
logger.quiet("Project version: ${projectVersion.get()}")
}
companion object {
const val NAME: String = "generateProjectData"
fun register(project: Project) {
val generateProjectData: TaskProvider<QontoGenerateProjectDataTask> =
project.tasks.register<QontoGenerateProjectDataTask>(
name = NAME,
LoggerFactory.getLogger("qonto"),
)
generateProjectData.configure {
projectGroup.set(project.provider { "${project.group}" })
projectName.set(project.provider { project.name })
projectVersion.set(project.provider { "${project.version}" })
}
project.tasks.named(BasePlugin.ASSEMBLE_TASK_NAME).configure {
dependsOn(generateProjectData)
}
}
}
}
Add outputs to the task and configure them.
- Use the
@OutputDirectory
annotation to mark theoutputDir
property as an output in theQontoGenerateProjectDataTask
. - Use the
@Internal
annotation to mark theoutputFile
property as an internal property in theQontoGenerateProjectDataTask
.
package com.qonto
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.ProjectLayout
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.Logger
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.register
import org.slf4j.LoggerFactory
@CacheableTask
open class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger,
objects: ObjectFactory,
layout: ProjectLayout,
) : DefaultTask() {
@Input
val projectGroup: Property<String> = objects.property()
@Input
val projectName: Property<String> = objects.property()
@Input
val projectVersion: Property<String> = objects.property()
@OutputDirectory
val outputDir: DirectoryProperty =
objects
.directoryProperty()
.convention(layout.buildDirectory.dir("generated/kotlin/com/qonto"))
@Internal
val outputFile: RegularFileProperty =
objects
.fileProperty()
.convention { outputDir.file("Project.kt").get().asFile }
init {
group = "qonto"
description = "Generates the project data"
}
@TaskAction
fun run() {
logger.quiet("Generating project data...")
logger.quiet("Project group: ${projectGroup.get()}")
logger.quiet("Project name: ${projectName.get()}")
logger.quiet("Project version: ${projectVersion.get()}")
}
companion object {
const val NAME: String = "generateProjectData"
fun register(project: Project) {
val generateProjectData: TaskProvider<QontoGenerateProjectDataTask> =
project.tasks.register<QontoGenerateProjectDataTask>(
name = NAME,
LoggerFactory.getLogger("qonto"),
)
generateProjectData.configure {
projectGroup.set(project.provider { "${project.group}" })
projectName.set(project.provider { project.name })
projectVersion.set(project.provider { "${project.version}" })
}
project.tasks.named(BasePlugin.ASSEMBLE_TASK_NAME).configure {
dependsOn(generateProjectData)
}
}
}
}
Change the task implementation to generate a file by using the inputs and outputs.
- Use the
outputFile
andoutputDir
properties to generate a file with the project data.
package com.qonto
// ...
@CacheableTask
open class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger,
objects: ObjectFactory,
layout: ProjectLayout,
) : DefaultTask() {
// ...
@TaskAction
fun run() {
// ...
outputDir.get().asFile.mkdirs()
outputFile.get().asFile.apply {
createNewFile()
writeText(
"""
package com.qonto
data object Project {
const val group: String = "${projectGroup.get()}"
const val name: String = "${projectName.get()}"
const val version: String = "${projectVersion.get()}"
}
""".trimIndent(),
)
}
}
// ...
}
Add the generated directory to the main Kotlin source set (WRONG WAY).
- Use
pluginManager
to react to theorg.jetbrains.kotlin.jvm
plugin being applied. - Use the
configure
method on theKotlinProjectExtension
to add the generated directory to the main Kotlin source set. - Run
./gradlew assemble
or./gradlew run
to see the issue.
package com.qonto
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.BasePlugin
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
class QontoPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply(BasePlugin::class)
target.logger.quiet("Hello from QontoPlugin!")
QontoGenerateProjectDataTask.register(target)
target.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
target.configure<KotlinProjectExtension> {
sourceSets.named("main") {
kotlin.srcDirs(target.layout.buildDirectory.dir("generated/kotlin"))
}
}
}
}
}
Fix the issue above by wiring the task directly with the Kotlin source set.
- Use the
named
method on thesourceSets
to get themain
source set. - Use the
kotlin.srcDirs
method to add the task outputs to the source set. - Run
./gradlew assemble
or./gradlew run
to see the task being executed. - Modify the
main
function to print the generated project data.
package com.qonto
// ...
@CacheableTask
open class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger,
objects: ObjectFactory,
layout: ProjectLayout,
) : DefaultTask() {
// ...
companion object {
const val NAME: String = "generateProjectData"
fun register(project: Project) {
// ..
project.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
project.configure<KotlinProjectExtension> {
sourceSets.named("main") {
kotlin.srcDirs(generateProjectData)
}
}
}
}
}
}
package com.qonto
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.BasePlugin
import org.gradle.kotlin.dsl.apply
class QontoPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply(BasePlugin::class)
target.logger.quiet("Hello from QontoPlugin!")
QontoGenerateProjectDataTask.register(target)
}
}
package com.qonto.application
fun main() {
println(
"""
Project data:
Group: ${com.qonto.Project.group}
Name: ${com.qonto.Project.name}
Version: ${com.qonto.Project.version}
""".trimIndent()
)
}
Create the QontoExtension.
- Create the file
QontoExtension.kt
in thecom.qonto
package. - Create the class
QontoExtension
and add theprojectDescription
property.
package com.qonto
import javax.inject.Inject
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.property
open class QontoExtension
@Inject constructor(
objects: ObjectFactory,
) {
val projectDescription: Property<String> =
objects.property<String>().convention("Gradle workshop")
companion object {
const val NAME = "qonto"
fun register(project: Project): QontoExtension = project.extensions.create(NAME)
}
}
Change the task implementation and wire its configuration with the extension.
- Add the
projectDescription
property as input in theQontoGenerateProjectDataTask
. - Use the
qontoExtension
to wire theprojectDescription
property of the task in thePluginQonto
. - Modify the
build.gradle.kts
file in theapplication
module to use theqonto
extension. - Modify the
main
function to print the generated project data with theprojectDescription
. - Run
./gradlew run
to see the task being executed.
package com.qonto
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.ProjectLayout
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.Logger
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.register
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.slf4j.LoggerFactory
@CacheableTask
open class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger,
objects: ObjectFactory,
layout: ProjectLayout,
) : DefaultTask() {
@Input
val projectGroup: Property<String> = objects.property()
@Input
val projectName: Property<String> = objects.property()
@Input
val projectVersion: Property<String> = objects.property()
@Input
val projectDescription: Property<String> = objects.property<String>()
@OutputDirectory
val outputDir: DirectoryProperty =
objects
.directoryProperty()
.convention(layout.buildDirectory.dir("generated/kotlin/com/qonto"))
@Internal
val outputFile: RegularFileProperty =
objects
.fileProperty()
.convention { outputDir.file("Project.kt").get().asFile }
init {
group = "qonto"
description = "Generates the project data"
}
@TaskAction
fun run() {
logger.quiet("Generating project data...")
logger.quiet("Project group: ${projectGroup.get()}")
logger.quiet("Project name: ${projectName.get()}")
logger.quiet("Project version: ${projectVersion.get()}")
logger.quiet("Project description: ${projectDescription.get()}")
outputDir.get().asFile.mkdirs()
outputFile.get().asFile.apply {
createNewFile()
writeText(
"""
package com.qonto
data object Project {
const val group: String = "${projectGroup.get()}"
const val name: String = "${projectName.get()}"
const val version: String = "${projectVersion.get()}"
const val description: String = "${projectDescription.get()}"
}
""".trimIndent(),
)
}
}
companion object {
const val NAME: String = "generateProjectData"
fun register(project: Project, qontoExtension: QontoExtension) {
val generateProjectData: TaskProvider<QontoGenerateProjectDataTask> =
project.tasks.register<QontoGenerateProjectDataTask>(
name = NAME,
LoggerFactory.getLogger("qonto"),
)
generateProjectData.configure {
projectGroup.set(project.provider { "${project.group}" })
projectName.set(project.provider { project.name })
projectVersion.set(project.provider { "${project.version}" })
projectDescription.set(qontoExtension.projectDescription)
}
project.tasks.named(BasePlugin.ASSEMBLE_TASK_NAME).configure {
dependsOn(generateProjectData)
}
project.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
project.configure<KotlinProjectExtension> {
sourceSets.named("main") {
kotlin.srcDirs(generateProjectData)
}
}
}
}
}
}
package com.qonto
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.BasePlugin
import org.gradle.kotlin.dsl.apply
class QontoPlugin : Plugin<Project> {
override fun apply(target: Project) {
val qontoExtension: QontoExtension = QontoExtension.register(target)
target.pluginManager.apply(BasePlugin::class)
target.logger.quiet("Hello from QontoPlugin!")
QontoGenerateProjectDataTask.register(target, qontoExtension)
}
}
plugins {
application
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.qonto)
}
application {
mainClass = "com.qonto.application.MainKt"
}
group = "com.qonto"
version = "1.0.0"
qonto {
projectDescription = "The Qonto Gradle Workshop!"
// projectDescription.set("Qonto Workshop!") same as above due to the new Kotlin Compiler plugin
}
package com.qonto.application
fun main() {
println(
"""
Project data:
Group: ${com.qonto.Project.group}
Name: ${com.qonto.Project.name}
Version: ${com.qonto.Project.version}
Additional lines: ${com.qonto.Project.description}
""".trimIndent()
)
}
Change the task's input to be an option.
- Add the
@Option
annotation to theprojectDescription
property in theQontoGenerateProjectDataTask
.
package com.qonto
// ...
import org.gradle.api.tasks.options.Option
// ...
@CacheableTask
open class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger,
objects: ObjectFactory,
layout: ProjectLayout,
) : DefaultTask() {
// ...
@Input
@Option(option = "projectDescription", description = "The project description")
val projectDescription: Property<String> = objects.property<String>()
// ...
}
Run the task via CLI by passing the option with a different value.
- Run the task with the
--projectDescription
option to see the new value.
./gradlew run generateProjectData --projectDescription="New project description!"
- Check the output to see the new project description.