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.