diff --git a/README.md b/README.md index e665cfb..437ceaa 100644 --- a/README.md +++ b/README.md @@ -1 +1,977 @@ # Gradle Workshop + +## Start + +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: + +```shell +./gradlew run +``` + +## Step 1: Create the Qonto plugin ✅ + +
+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 the `Plugin` interface using `Project` as its type + parameter. + +```kotlin +package com.qonto + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class QontoPlugin : Plugin { + + 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 in `build-logic` module. +- Add the following code to the file below the plugins block. + +```kotlin +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 the `gradle` directory. +- Add the plugin to the bottom of the `plugins` section and sync the Gradle project. + +```toml +[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 the `application` project. +- Apply the plugin in the `plugins` block. + +```kotlin +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 file `QontoGenerateProjectDataTask.kt` in the `com.qonto` package. +- Create the class `QontoGenerateProjectDataTask` class and extends the `DefaultTask` class. + +```kotlin +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 = + project.tasks.register( + name = NAME, + LoggerFactory.getLogger("qonto"), + ) + } + } +} +``` + +
+ +
+Register the task. + +- Call the `register` method on the task `companion object` within the `apply` block in the plugin. + +```kotlin +package com.qonto + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class QontoPlugin : Plugin { + + 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 the `BasePlugin` plugin + +```kotlin +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 { + + 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 the `tasks` to get the `assemble` task. +- Use `dependsOn` to make the `assemble` task depend on the `generateProjectData` task. + +```kotlin +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 = + project.tasks.register( + 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 the `QontoGenerateProjectDataTask` class. + +```kotlin +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 the + `QontoGenerateProjectDataTask`. +- Wire them within the `configure` method block from the `TaskProvider`. +- Use the `provider` lambda to do lazy evaluation of the provided properties. + +```kotlin +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 = objects.property() + + @Input + val projectName: Property = objects.property() + + @Input + val projectVersion: Property = 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 = + project.tasks.register( + 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 the `outputDir` property as an output in the + `QontoGenerateProjectDataTask`. +- Use the `@Internal` annotation to mark the `outputFile` property as an internal property in the + `QontoGenerateProjectDataTask`. + +```kotlin +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 = objects.property() + + @Input + val projectName: Property = objects.property() + + @Input + val projectVersion: Property = 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 = + project.tasks.register( + 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 the `outputFile` and `outputDir` properties to generate a file with the project data. + +```kotlin +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 the `org.jetbrains.kotlin.jvm` plugin being applied. +- Use the `configure` method on the `KotlinProjectExtension` to add the generated directory to the + main Kotlin source set. +- Run `./gradlew assemble` or `./gradlew run` to see the issue. + +```kotlin +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 { + + 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 { + 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 the `sourceSets` to get the `main` 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. + +```kotlin +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 { + sourceSets.named("main") { + kotlin.srcDirs(generateProjectData) + } + } + } + } + } +} +``` + +```kotlin +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 { + + override fun apply(target: Project) { + target.pluginManager.apply(BasePlugin::class) + target.logger.quiet("Hello from QontoPlugin!") + + QontoGenerateProjectDataTask.register(target) + } +} +``` + +```kotlin +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() + ) +} + +``` + +
+ +## Step 5: Create the QontoExtension to allow the user to specify default values ✅ + +
+Create the QontoExtension. + +- Create the file `QontoExtension.kt` in the `com.qonto` package. +- Create the class `QontoExtension` and add the `projectDescription` property. + +```kotlin +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 = + objects.property().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 the `QontoGenerateProjectDataTask`. +- Use the `qontoExtension` to wire the `projectDescription` property of the task in the + `PluginQonto`. +- Modify the `build.gradle.kts` file in the `application` module to use the `qonto` extension. +- Modify the `main` function to print the generated project data with the `projectDescription`. +- Run `./gradlew run` to see the task being executed. + +```kotlin +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 = objects.property() + + @Input + val projectName: Property = objects.property() + + @Input + val projectVersion: Property = objects.property() + + @Input + val projectDescription: Property = 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()}") + 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 = + project.tasks.register( + 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 { + sourceSets.named("main") { + kotlin.srcDirs(generateProjectData) + } + } + } + } + } +} +``` + +```kotlin +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 { + + 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) + } +} +``` + +```kotlin +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 +} +``` + +```kotlin +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() + ) +} +``` + +
+ +## 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 the `projectDescription` property in the + `QontoGenerateProjectDataTask`. + +```kotlin +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 = objects.property() + + // ... +} + +``` + +
+ +
+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. + +```shell +./gradlew run generateProjectData --projectDescription="New project description!" +``` + +- Check the output to see the new project description. + +
+ +## Step 7: Add version validation report with `Problems` API ✅ + +
+Gradle documentation about the `Problems` API + +Gradle has a `Problems` API that allows you to report problems. The docs can be found: + +- [Reporting problems](https://docs.gradle.org/current/userguide/reporting_problems.html#sec:reporting_problems) +- [Reporting and receiving problems via the Problems API Sample](https://docs.gradle.org/current/samples/sample_problems_api_usage.html) + +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 + +```kotlin + +@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)) { + problems.reporter.throwing { + id("invalid-version", "The project version is invalid") + contextualLabel("The project version '${projectVersion.get()}' is invalid") + severity(Severity.ERROR) + withException(IllegalStateException("The project version is invalid")) + 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. + +
diff --git a/application/build.gradle.kts b/application/build.gradle.kts index 8a7b0ae..0b9816e 100644 --- a/application/build.gradle.kts +++ b/application/build.gradle.kts @@ -1,6 +1,7 @@ plugins { application alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.qonto) } application { @@ -9,3 +10,8 @@ application { 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 +} diff --git a/application/src/main/kotlin/com/qonto/application/main.kt b/application/src/main/kotlin/com/qonto/application/main.kt index b8809fb..f2b0739 100644 --- a/application/src/main/kotlin/com/qonto/application/main.kt +++ b/application/src/main/kotlin/com/qonto/application/main.kt @@ -1,5 +1,13 @@ package com.qonto.application fun main() { - println("Hello, world!") + 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() + ) } diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 12f9e44..e49668d 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -2,6 +2,15 @@ plugins { `kotlin-dsl` } +gradlePlugin { + plugins { + register("QontoPlugin") { + id = "qonto" + implementationClass = "com.qonto.QontoPlugin" + } + } +} + dependencies { implementation(libs.plugins.kotlin.jvm.artifact) } diff --git a/build-logic/src/main/kotlin/com/qonto/QontoExtension.kt b/build-logic/src/main/kotlin/com/qonto/QontoExtension.kt new file mode 100644 index 0000000..16a9bc9 --- /dev/null +++ b/build-logic/src/main/kotlin/com/qonto/QontoExtension.kt @@ -0,0 +1,24 @@ +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 = + objects.property().convention("Gradle workshop") + + companion object { + + const val NAME = "qonto" + + fun register(project: Project): QontoExtension = project.extensions.create(NAME) + } +} diff --git a/build-logic/src/main/kotlin/com/qonto/QontoGenerateProjectDataTask.kt b/build-logic/src/main/kotlin/com/qonto/QontoGenerateProjectDataTask.kt new file mode 100644 index 0000000..d9a641d --- /dev/null +++ b/build-logic/src/main/kotlin/com/qonto/QontoGenerateProjectDataTask.kt @@ -0,0 +1,143 @@ +@file:Suppress("UnstableApiUsage") + +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.problems.Problems +import org.gradle.api.problems.Severity +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.api.tasks.options.Option +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 +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 + + @Input + val projectGroup: Property = objects.property() + + @Input + val projectName: Property = objects.property() + + @Input + val projectVersion: Property = objects.property() + + @Input + @Option(option = "projectDescription", description = "The project description") + val projectDescription: Property = 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() { + if (!projectVersion.get().matches(VersionRegex)) { + problems.reporter.throwing { + id("invalid-version", "The project version is invalid") + contextualLabel("The project version '${projectVersion.get()}' is invalid") + severity(Severity.ERROR) + withException(IllegalStateException("The project version is invalid")) + solution("Provide a valid version (example: 'project.version = 1.0.0')") + } + } + + 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" + + 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-]+)*))?${'$'}""", + ) + + fun register(project: Project, qontoExtension: QontoExtension) { + val generateProjectData: TaskProvider = + project.tasks.register( + 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 { + sourceSets.named("main") { + kotlin.srcDirs(generateProjectData) + } + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/com/qonto/QontoPlugin.kt b/build-logic/src/main/kotlin/com/qonto/QontoPlugin.kt new file mode 100644 index 0000000..d664075 --- /dev/null +++ b/build-logic/src/main/kotlin/com/qonto/QontoPlugin.kt @@ -0,0 +1,17 @@ +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 { + + 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) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1618ac6..a5a82a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] -kotlin = "2.0.21" +kotlin = "2.1.10" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +qonto = { id = "qonto" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9..9bbc975 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94113f2..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6..faf9300 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -206,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line.