The workshop will guide you through the process of creating Gradle tasks, focusing on inputs, outputs, and making tasks cacheable.
You'll also learn:
- How to create a Gradle project extension to extend plugin functionality.
- How to create a Gradle task that codegen a Kotlin file.
- Wire the generated code with the Kotlin source sets.
- If time, how to use the
ProblemsAPI to report issues.
Our hands-on project involves building a Gradle plugin that generates a Kotlin file containing project metadata such as project version, group and name. This practical experience will cover creating Gradle plugins, tasks, and extensions, as well as reporting issues and integrating with source sets.
The repository contains everything needed to start the workshop.
- The
build-logicincluded build contains the Gradle plugin which is going to be created. - The
applicationmodule 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 runCreate the Gradle plugin by extending the `Plugin` interface.
- Right-click on the
build-logicmodule. - Create the directory
src/main/kotlin/com/qonto/. - Create the file
QontoPlugin.ktin the directory. - Create the class
QontoPluginand extends thePlugininterface usingProjectas 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.ktsfile inbuild-logicmodule. - 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.tomlfile inside thegradledirectory. - Add the plugin to the bottom of the
pluginssection 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 lineApply the plugin in the `application` project.
- Open the
build.gradle.ktsfile inside theapplicationproject. - Apply the plugin in the
pluginsblock.
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.ktin thecom.qontopackage. - Create the class
QontoGenerateProjectDataTaskclass and extends theDefaultTaskclass.
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
registermethod on the taskcompanion objectwithin theapplyblock 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
pluginManagerto apply theBasePluginplugin
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
namedmethod on thetasksto get theassembletask. - Use
dependsOnto make theassembletask depend on thegenerateProjectDatatask.
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
@CacheableTaskannotation to theQontoGenerateProjectDataTaskclass.
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
@Inputannotation to mark the properties as inputs in theQontoGenerateProjectDataTask. - Wire them within the
configuremethod block from theTaskProvider. - Use the
providerlambda 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
@OutputDirectoryannotation to mark theoutputDirproperty as an output in theQontoGenerateProjectDataTask. - Use the
@Internalannotation to mark theoutputFileproperty 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
outputFileandoutputDirproperties 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
pluginManagerto react to theorg.jetbrains.kotlin.jvmplugin being applied. - Use the
configuremethod on theKotlinProjectExtensionto add the generated directory to the main Kotlin source set. - Run
./gradlew assembleor./gradlew runto 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
namedmethod on thesourceSetsto get themainsource set. - Use the
kotlin.srcDirsmethod to add the task outputs to the source set. - Run
./gradlew assembleor./gradlew runto see the task being executed. - Modify the
mainfunction 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.ktin thecom.qontopackage. - Create the class
QontoExtensionand add theprojectDescriptionproperty.
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
projectDescriptionproperty as input in theQontoGenerateProjectDataTask. - Use the
qontoExtensionto wire theprojectDescriptionproperty of the task in thePluginQonto. - Modify the
build.gradle.ktsfile in theapplicationmodule to use theqontoextension. - Modify the
mainfunction to print the generated project data with theprojectDescription. - Run
./gradlew runto 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
@Optionannotation to theprojectDescriptionproperty 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
--projectDescriptionoption to see the new value.
./gradlew run generateProjectData --projectDescription="New project description!"- Check the output to see the new project description.
Gradle documentation about the `Problems` API
Gradle has a Problems API that allows you to report problems. The docs can be found:
It is very simple, the Problems interface is injected in any place you want to do a report, it can
be a plugin, a task, etc. Then you can use the reporting or throwing methods to report a
problem.
Update the task `QontoGenerateProjectDataTask` to report an invalid version
@CacheableTask
abstract class QontoGenerateProjectDataTask
@Inject constructor(
private val logger: Logger,
objects: ObjectFactory,
layout: ProjectLayout,
) : DefaultTask() {
// Inject via constructor fails in Gradle 8.12, move to constructor when it is fixed
@get:Inject
abstract val problems: Problems
// ...
@TaskAction
fun run() {
if (!projectVersion.get().matches(VersionRegex)) {
val problemGroup: ProblemGroup = ProblemGroup.create("qonto", "qonto")
val problemId: ProblemId =
ProblemId.create("invalid-version", "invalid-version", problemGroup)
val exception = IllegalStateException("The project version is invalid")
problems.reporter.throwing(exception, problemId) {
contextualLabel("The project version '${projectVersion.get()}' is invalid")
severity(Severity.ERROR)
solution("Provide a valid version (example: 'project.version = 1.0.0')")
}
}
// ...
}
companion object {
// ...
private val VersionRegex = Regex(
"""^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?${'$'}""",
)
}
}After calling the task, if the project::version assigned in the build.gradle.kts file is not
valid, the build will fail and the error will be added to the problems report file, which can be
found in gradle-workshop/build/reports/problems/problems-reports.html.
The file is in the build root directory as it will summarize all the problems in the whole
project, that includes all Gradle projects.