Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
NotificationsYou must be signed in to change notification settings

qonto/gradle-workshop

Repository files navigation

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 theProblems API to report issues.

Our hands-on project involves building a Gradle plugin that generates a Kotlin file containingproject metadata such as project version, group and name. This practical experience will covercreating Gradle plugins, tasks, and extensions, as well as reporting issues and integrating withsource sets.

Start

The repository contains everything needed to start the workshop.

  • Thebuild-logic included build contains the Gradle plugin which is going to be created.
  • Theapplication module will be used as a simple application to run the generated code by theplugin.

To run the application, use the next CLI command:

./gradlew run

Step 1: Create the Qonto plugin ✅

Create the Gradle plugin by extending the `Plugin` interface.
  • Right-click on thebuild-logic module.
  • Create the directorysrc/main/kotlin/com/qonto/.
  • Create the fileQontoPlugin.kt in the directory.
  • Create the classQontoPlugin and extends thePlugin interface usingProject as its typeparameter.
packagecom.qontoimportorg.gradle.api.Pluginimportorg.gradle.api.ProjectclassQontoPlugin :Plugin<Project> {overridefunapply(target:Project) {        target.logger.quiet("Hello from QontoPlugin!")    }}
Register the plugin in the `build-logic` module with the `qonto` id.
  • Open thebuild.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 thelibs.versions.toml file inside thegradle directory.
  • Add the plugin to the bottom of theplugins 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 thebuild.gradle.kts file inside theapplication project.
  • Apply the plugin in theplugins 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"

Step 2: Create the QontoGenerateProjectDataTask task ✅

Create a task with the minimum amount of code.
  • Create the fileQontoGenerateProjectDataTask.kt in thecom.qonto package.
  • Create the classQontoGenerateProjectDataTask class and extends theDefaultTask class.
packagecom.qontoimportjavax.inject.Injectimportorg.gradle.api.DefaultTaskimportorg.gradle.api.Projectimportorg.gradle.api.logging.Loggerimportorg.gradle.api.tasks.TaskActionimportorg.gradle.api.tasks.TaskProviderimportorg.gradle.kotlin.dsl.registerimportorg.slf4j.LoggerFactoryopenclassQontoGenerateProjectDataTask@Injectconstructor(privateval logger:Logger):DefaultTask() {init {        group="qonto"        description="Generates the project data"    }    @TaskActionfunrun() {        logger.quiet("Generating project data...")    }companionobject {constvalNAME:String="generateProjectData"funregister(project:Project) {val generateProjectData:TaskProvider<QontoGenerateProjectDataTask>=                project.tasks.register<QontoGenerateProjectDataTask>(                    name=NAME,LoggerFactory.getLogger("qonto"),                )        }    }}
Register the task.
  • Call theregister method on the taskcompanion object within theapply block in the plugin.
packagecom.qontoimportorg.gradle.api.Pluginimportorg.gradle.api.ProjectclassQontoPlugin :Plugin<Project> {overridefunapply(target:Project) {        target.logger.quiet("Hello from QontoPlugin!")QontoGenerateProjectDataTask.register(target)// Add this line    }}
Apply the base plugin.
  • Use thepluginManager to apply theBasePlugin plugin
packagecom.qontoimportorg.gradle.api.Pluginimportorg.gradle.api.Projectimportorg.gradle.api.plugins.BasePlugin// Add this lineimportorg.gradle.kotlin.dsl.apply// Add this lineclassQontoPlugin :Plugin<Project> {overridefunapply(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 thenamed method on thetasks to get theassemble task.
  • UsedependsOn to make theassemble task depend on thegenerateProjectData task.
packagecom.qontoimportjavax.inject.Injectimportorg.gradle.api.DefaultTaskimportorg.gradle.api.Projectimportorg.gradle.api.logging.Loggerimportorg.gradle.api.plugins.BasePlugin// Add this lineimportorg.gradle.api.tasks.TaskActionimportorg.gradle.api.tasks.TaskProviderimportorg.gradle.kotlin.dsl.registerimportorg.slf4j.LoggerFactoryopenclassQontoGenerateProjectDataTask@Injectconstructor(privateval logger:Logger):DefaultTask() {init {        group="qonto"        description="Generates the project data"    }    @TaskActionfunrun() {        logger.quiet("Generating project data...")    }companionobject {constvalNAME:String="generateProjectData"funregister(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)            }        }    }}

Step 3: Add inputs and outputs to the task ✅

Make the task cacheable.
  • Add the@CacheableTask annotation to theQontoGenerateProjectDataTask class.
packagecom.qonto// ...importorg.gradle.api.tasks.CacheableTask// Add this line// ...@CacheableTask// Add this lineopenclassQontoGenerateProjectDataTask@Injectconstructor(privateval 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 theconfigure method block from theTaskProvider.
  • Use theprovider lambda to do lazy evaluation of the provided properties.
packagecom.qontoimportjavax.inject.Injectimportorg.gradle.api.DefaultTaskimportorg.gradle.api.Projectimportorg.gradle.api.logging.Loggerimportorg.gradle.api.model.ObjectFactoryimportorg.gradle.api.plugins.BasePluginimportorg.gradle.api.provider.Propertyimportorg.gradle.api.tasks.CacheableTaskimportorg.gradle.api.tasks.Inputimportorg.gradle.api.tasks.TaskActionimportorg.gradle.api.tasks.TaskProviderimportorg.gradle.kotlin.dsl.propertyimportorg.gradle.kotlin.dsl.registerimportorg.slf4j.LoggerFactory@CacheableTaskopenclassQontoGenerateProjectDataTask@Injectconstructor(privateval logger:Logger,privateval objects:ObjectFactory,):DefaultTask() {    @Inputval projectGroup:Property<String>= objects.property()    @Inputval projectName:Property<String>= objects.property()    @Inputval projectVersion:Property<String>= objects.property()init {        group="qonto"        description="Generates the project data"    }    @TaskActionfunrun() {        logger.quiet("Generating project data...")        logger.quiet("Project group:${projectGroup.get()}")        logger.quiet("Project name:${projectName.get()}")        logger.quiet("Project version:${projectVersion.get()}")    }companionobject {constvalNAME:String="generateProjectData"funregister(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.
packagecom.qontoimportjavax.inject.Injectimportorg.gradle.api.DefaultTaskimportorg.gradle.api.Projectimportorg.gradle.api.file.DirectoryPropertyimportorg.gradle.api.file.ProjectLayoutimportorg.gradle.api.file.RegularFilePropertyimportorg.gradle.api.logging.Loggerimportorg.gradle.api.model.ObjectFactoryimportorg.gradle.api.plugins.BasePluginimportorg.gradle.api.provider.Propertyimportorg.gradle.api.tasks.CacheableTaskimportorg.gradle.api.tasks.Inputimportorg.gradle.api.tasks.Internalimportorg.gradle.api.tasks.OutputDirectoryimportorg.gradle.api.tasks.TaskActionimportorg.gradle.api.tasks.TaskProviderimportorg.gradle.kotlin.dsl.propertyimportorg.gradle.kotlin.dsl.registerimportorg.slf4j.LoggerFactory@CacheableTaskopenclassQontoGenerateProjectDataTask@Injectconstructor(privateval logger:Logger,    objects:ObjectFactory,    layout:ProjectLayout,):DefaultTask() {    @Inputval projectGroup:Property<String>= objects.property()    @Inputval projectName:Property<String>= objects.property()    @Inputval projectVersion:Property<String>= objects.property()    @OutputDirectoryval outputDir:DirectoryProperty=        objects            .directoryProperty()            .convention(layout.buildDirectory.dir("generated/kotlin/com/qonto"))    @Internalval outputFile:RegularFileProperty=        objects            .fileProperty()            .convention { outputDir.file("Project.kt").get().asFile }init {        group="qonto"        description="Generates the project data"    }    @TaskActionfunrun() {        logger.quiet("Generating project data...")        logger.quiet("Project group:${projectGroup.get()}")        logger.quiet("Project name:${projectName.get()}")        logger.quiet("Project version:${projectVersion.get()}")    }companionobject {constvalNAME:String="generateProjectData"funregister(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)            }        }    }}

Step 4: Change the task implementation to codegen a file and wire it with the Kotlin source set ✅

Change the task implementation to generate a file by using the inputs and outputs.
  • Use theoutputFile andoutputDir properties to generate a file with the project data.
packagecom.qonto// ...@CacheableTaskopenclassQontoGenerateProjectDataTask@Injectconstructor(privateval logger:Logger,    objects:ObjectFactory,    layout:ProjectLayout,):DefaultTask() {// ...    @TaskActionfunrun() {// ...        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).
  • UsepluginManager to react to theorg.jetbrains.kotlin.jvm plugin being applied.
  • Use theconfigure method on theKotlinProjectExtension to add the generated directory to themain Kotlin source set.
  • Run./gradlew assemble or./gradlew run to see the issue.
packagecom.qontoimportorg.gradle.api.Pluginimportorg.gradle.api.Projectimportorg.gradle.api.plugins.BasePluginimportorg.gradle.kotlin.dsl.applyimportorg.gradle.kotlin.dsl.configureimportorg.jetbrains.kotlin.gradle.dsl.KotlinProjectExtensionclassQontoPlugin :Plugin<Project> {overridefunapply(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 thenamed method on thesourceSets to get themain source set.
  • Use thekotlin.srcDirs method to add the task outputs to the source set.
  • Run./gradlew assemble or./gradlew run to see the task being executed.
  • Modify themain function to print the generated project data.
packagecom.qonto// ...@CacheableTaskopenclassQontoGenerateProjectDataTask@Injectconstructor(privateval logger:Logger,    objects:ObjectFactory,    layout:ProjectLayout,):DefaultTask() {// ...companionobject {constvalNAME:String="generateProjectData"funregister(project:Project) {// ..            project.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {                project.configure<KotlinProjectExtension> {                    sourceSets.named("main") {                        kotlin.srcDirs(generateProjectData)                    }                }            }        }    }}
packagecom.qontoimportorg.gradle.api.Pluginimportorg.gradle.api.Projectimportorg.gradle.api.plugins.BasePluginimportorg.gradle.kotlin.dsl.applyclassQontoPlugin :Plugin<Project> {overridefunapply(target:Project) {        target.pluginManager.apply(BasePlugin::class)        target.logger.quiet("Hello from QontoPlugin!")QontoGenerateProjectDataTask.register(target)    }}
packagecom.qonto.applicationfunmain() {println("""            Project data:            Group:${com.qonto.Project.group}            Name:${com.qonto.Project.name}            Version:${com.qonto.Project.version}""".trimIndent()    )}

Step 5: Create the QontoExtension to allow the user to specify default values ✅

Create the QontoExtension.
  • Create the fileQontoExtension.kt in thecom.qonto package.
  • Create the classQontoExtension and add theprojectDescription property.
packagecom.qontoimportjavax.inject.Injectimportorg.gradle.api.Projectimportorg.gradle.api.model.ObjectFactoryimportorg.gradle.api.provider.Propertyimportorg.gradle.kotlin.dsl.createimportorg.gradle.kotlin.dsl.propertyopenclassQontoExtension@Injectconstructor(    objects:ObjectFactory,) {val projectDescription:Property<String>=        objects.property<String>().convention("Gradle workshop")companionobject {constvalNAME="qonto"funregister(project:Project):QontoExtension= project.extensions.create(NAME)    }}
Change the task implementation and wire its configuration with the extension.
  • Add theprojectDescription property as input in theQontoGenerateProjectDataTask.
  • Use theqontoExtension to wire theprojectDescription property of the task in thePluginQonto.
  • Modify thebuild.gradle.kts file in theapplication module to use theqonto extension.
  • Modify themain function to print the generated project data with theprojectDescription.
  • Run./gradlew run to see the task being executed.
packagecom.qontoimportjavax.inject.Injectimportorg.gradle.api.DefaultTaskimportorg.gradle.api.Projectimportorg.gradle.api.file.DirectoryPropertyimportorg.gradle.api.file.ProjectLayoutimportorg.gradle.api.file.RegularFilePropertyimportorg.gradle.api.logging.Loggerimportorg.gradle.api.model.ObjectFactoryimportorg.gradle.api.plugins.BasePluginimportorg.gradle.api.provider.Propertyimportorg.gradle.api.tasks.CacheableTaskimportorg.gradle.api.tasks.Inputimportorg.gradle.api.tasks.Internalimportorg.gradle.api.tasks.OutputDirectoryimportorg.gradle.api.tasks.TaskActionimportorg.gradle.api.tasks.TaskProviderimportorg.gradle.kotlin.dsl.configureimportorg.gradle.kotlin.dsl.propertyimportorg.gradle.kotlin.dsl.registerimportorg.jetbrains.kotlin.gradle.dsl.KotlinProjectExtensionimportorg.slf4j.LoggerFactory@CacheableTaskopenclassQontoGenerateProjectDataTask@Injectconstructor(privateval logger:Logger,    objects:ObjectFactory,    layout:ProjectLayout,):DefaultTask() {    @Inputval projectGroup:Property<String>= objects.property()    @Inputval projectName:Property<String>= objects.property()    @Inputval projectVersion:Property<String>= objects.property()    @Inputval projectDescription:Property<String>= objects.property<String>()    @OutputDirectoryval outputDir:DirectoryProperty=        objects            .directoryProperty()            .convention(layout.buildDirectory.dir("generated/kotlin/com/qonto"))    @Internalval outputFile:RegularFileProperty=        objects            .fileProperty()            .convention { outputDir.file("Project.kt").get().asFile }init {        group="qonto"        description="Generates the project data"    }    @TaskActionfunrun() {        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(),            )        }    }companionobject {constvalNAME:String="generateProjectData"funregister(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)                    }                }            }        }    }}
packagecom.qontoimportorg.gradle.api.Pluginimportorg.gradle.api.Projectimportorg.gradle.api.plugins.BasePluginimportorg.gradle.kotlin.dsl.applyclassQontoPlugin :Plugin<Project> {overridefunapply(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}
packagecom.qonto.applicationfunmain() {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()    )}

Step 6: Change one task's input to be an option ✅

Change the task's input to be an option.
  • Add the@Option annotation to theprojectDescription property in theQontoGenerateProjectDataTask.
packagecom.qonto// ...importorg.gradle.api.tasks.options.Option// ...@CacheableTaskopenclassQontoGenerateProjectDataTask@Injectconstructor(privateval 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.

Step 7: Add version validation report withProblems API ✅

Gradle documentation about the `Problems` API

Gradle has aProblems API that allows you to report problems. The docs can be found:

It is very simple, theProblems interface is injected in any place you want to do a report, it canbe a plugin, a task, etc. Then you can use thereporting orthrowing methods to report aproblem.

Update the task `QontoGenerateProjectDataTask` to report an invalid version
@CacheableTaskabstractclassQontoGenerateProjectDataTask@Injectconstructor(privateval logger:Logger,    objects:ObjectFactory,    layout:ProjectLayout,):DefaultTask() {// Inject via constructor fails in Gradle 8.12, move to constructor when it is fixed    @get:Injectabstractval problems:Problems// ...    @TaskActionfunrun() {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')")            }        }// ...    }companionobject {// ...privatevalVersionRegex=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 theproject::version assigned in thebuild.gradle.kts file is notvalid, the build will fail and the error will be added to the problems report file, which can befound ingradle-workshop/build/reports/problems/problems-reports.html.

The file is in thebuild root directory as it will summarize all the problems in the wholeproject, that includes all Gradle projects.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp