From 3789a02fdd7ce2d6d27b1382427425e2a8ab2d53 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Fri, 3 Jan 2025 10:10:32 +0100 Subject: [PATCH] add code to generate plugin markers --- README.md | 16 +- .../gratatouille/implementation-runtime.kt | 12 +- .../librarian.properties | 1 + .../gradle/GenerateDescriptorTask.kt | 36 ++ .../gradle/GratatouilleExtension.kt | 95 +++++ .../gradle/MavenPublicationInternal.kt | 13 + .../main/kotlin/gratatouille/gradle/utils.kt | 16 +- gratatouille-processor/build.gradle.kts | 1 + .../gratatouille/processor/GTaskAction.kt | 328 +++++++++--------- .../kotlin/gratatouille/processor/Task.kt | 19 +- libs.versions.toml | 1 + test-plugin/test-plugin/build.gradle.kts | 6 + 12 files changed, 366 insertions(+), 178 deletions(-) create mode 100644 gratatouille-gradle-plugin/librarian.properties create mode 100644 gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/GenerateDescriptorTask.kt create mode 100644 gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/GratatouilleExtension.kt create mode 100644 gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/MavenPublicationInternal.kt diff --git a/README.md b/README.md index bad8548..9bd4033 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Gratatouille 🐘🤝🐭👉🧑‍🍳 -Gratatouille is an opinionated framework to build Gradle plugins. Write pure Kotlin functions and the Gratatouille KSP processor generates tasks, workers, and wiring code for you. +Gratatouille is an opinionated framework to build Gradle plugins. Write Kotlin functions and the Gratatouille KSP processor generates tasks, workers, and wiring code for you. When used in classloader isolation mode, Gratatouille enforces a clear separation between your plugin logic (**implementation**) and your plugin wiring (**api**) making your plugin immune to [classloader issues](https://github.com/square/kotlinpoet/issues/1730#issuecomment-1819118527) 🛡️ **Key Features**: -* [Pure functions](#pure-functions) +* [Functional programming style](#functions) * [Kotlinx serialization support](#built-in-kotlinxserialization-support) * [Comprehensive input/output types](#supported-input-and-output-types) * [Non overlapping task outputs](#non-overlapping-task-outputs-by-default) @@ -186,11 +186,11 @@ No need to implement `DefaultTask`, no risk of forgetting `@Cacheable`, etc... G ## Features -### Pure functions +### Functional programming style -Your task code is a side-effect-free function, making it easier to [parallelize](#parallel-task-execution-by-default) and reason about. +Your code is modeled as functions taking inputs and generating outputs. -Nullable parameters are generated as optional task properties. Calls to `Provider.get()` or `Provider.orNull` are automated. +No need for stateful properties or classes. Nullable parameters are generated as optional task properties. Calls to `Provider.get()` or `Provider.orNull` are automated. ### Built-in kotlinx.serialization support @@ -378,9 +378,3 @@ gradlePlugin { ``` In your plugin code, use `Project.register${TaskAction}Task()` to register the task - -## Limitations - -### Logging - -Because your task actions are called from a worker and possibly from a completely separate classloader, there is no way to use `logger`. A future version may transport logs over sockets. diff --git a/gratatouille-core/src/main/kotlin/gratatouille/implementation-runtime.kt b/gratatouille-core/src/main/kotlin/gratatouille/implementation-runtime.kt index 6c8bfe5..ce2b252 100644 --- a/gratatouille-core/src/main/kotlin/gratatouille/implementation-runtime.kt +++ b/gratatouille-core/src/main/kotlin/gratatouille/implementation-runtime.kt @@ -10,7 +10,17 @@ annotation class GratatouilleInternal annotation class GInternal annotation class GManuallyWired -annotation class GTaskAction(val name: String = "", val group: String = "", val description: String = "") + +/** + * @param pure whether the annotated function is [pure](https://en.wikipedia.org/wiki/Pure_function), i.e. its outputs only depends + * on the inputs. + * + * Impure functions generate non-cacheable tasks that are never [up-to-date](https://docs.gradle.org/current/userguide/incremental_build.html). + * + * In this context, a function can be considered pure even if it has side effects as long as those side effects do not impact other tasks and do not contribute the build outputs. + * For an example, writing logs to stdout may not make a function impure but publishing to a Maven Repository should. + */ +annotation class GTaskAction(val name: String = "", val group: String = "", val description: String = "", val pure: Boolean = true) typealias GOutputFile = File typealias GOutputDirectory = File diff --git a/gratatouille-gradle-plugin/librarian.properties b/gratatouille-gradle-plugin/librarian.properties new file mode 100644 index 0000000..897916f --- /dev/null +++ b/gratatouille-gradle-plugin/librarian.properties @@ -0,0 +1 @@ +version.packageName=gratatouille.gradle \ No newline at end of file diff --git a/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/GenerateDescriptorTask.kt b/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/GenerateDescriptorTask.kt new file mode 100644 index 0000000..378db34 --- /dev/null +++ b/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/GenerateDescriptorTask.kt @@ -0,0 +1,36 @@ +package gratatouille.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import java.util.* + +@CacheableTask +internal abstract class GenerateDescriptorTask: DefaultTask() { + @get:Input + abstract val id: Property + + @get:Input + abstract val implementationClass: Property + + @get:OutputDirectory + abstract val output: DirectoryProperty + + @TaskAction + fun taskAction() { + output.get().asFile.resolve("META-INF/gradle-plugins").apply { + mkdirs() + val properties = Properties().apply { + this.put("implementation-class", implementationClass.get()) + } + + resolve(id.get() + ".properties").outputStream().use { + properties.store(it, "Gradle plugin descriptor (auto-generated)") + } + } + } +} diff --git a/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/GratatouilleExtension.kt b/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/GratatouilleExtension.kt new file mode 100644 index 0000000..cd54215 --- /dev/null +++ b/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/GratatouilleExtension.kt @@ -0,0 +1,95 @@ +package gratatouille.gradle + +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.file.CopySpec +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.Copy + +class DescriptorConnection(val directory: Provider) + +class PluginSpec(private val id: String, private val project: Project) { + /** + * Registers a `generate${pluginId}Descriptor` task that generates a [plugin descriptor](https://docs.gradle.org/current/userguide/java_gradle_plugin.html#sec:gradle_plugin_dev_usage) for the plugin. + * That plugin descriptor is copied into the `.jar` file during the `processResources` task. + * This allows to load a plugin by id. + * + * @param implementationClass the fully qualified class name for the plugin implementation. Example: `com.example.ExamplePlugin` + */ + fun implementationClass(implementationClass: String) { + implementationClass(implementationClass) { connection -> + // From https://github.com/gradle/gradle/blob/master/platforms/extensibility/plugin-development/src/main/java/org/gradle/plugin/devel/plugins/JavaGradlePluginPlugin.java#L253 + project.tasks.named("processResources", Copy::class.java) { + val copyPluginDescriptors: CopySpec = it.rootSpec.addChild() + copyPluginDescriptors.into("META-INF/gradle-plugins") + copyPluginDescriptors.from(connection.directory) + } + } + } + + /** + * Registers a `generate${pluginId}Descriptor` task that generates a [plugin descriptor](https://docs.gradle.org/current/userguide/java_gradle_plugin.html#sec:gradle_plugin_dev_usage) for the plugin. + * Use [DescriptorConnection] to wire the descriptor to other tasks + * + * @param implementationClass the fully qualified class name for the plugin implementation. Example: `com.example.ExamplePlugin` + */ + fun implementationClass(implementationClass: String, action: Action) { + val task = project.tasks.register("generate${id.displayName()}Descriptor", GenerateDescriptorTask::class.java) { + it.id.set(id) + it.implementationClass.set(implementationClass) + + it.output.set(project.layout.buildDirectory.dir("gratatouille/descriptor/$id")) + } + + val connection = DescriptorConnection(task.flatMap { it.output }) + action.execute(connection) + } + + /** + * Creates a new `${pluginId}PluginMarkerMaven` publication allowing to locate the implementation from the plugin id. + * + * @param mainPublication the publication to redirect to. + */ + fun marker(mainPublication: MavenPublication) { + project.withRequiredPlugins("maven-publish") { + project.extensions.getByType(PublishingExtension::class.java).apply { + // https://github.com/gradle/gradle/blob/master/platforms/extensibility/plugin-development/src/main/java/org/gradle/plugin/devel/plugins/MavenPluginPublishPlugin.java#L88 + publications.create(id.displayName() + "PluginMarkerMaven", MavenPublication::class.java) { markerPublication -> + markerPublication.trySetAlias() + markerPublication.artifactId = id + markerPublication.artifactId = "$id.gradle.plugin"; + markerPublication.groupId = id; + + val groupProvider = project.provider { mainPublication.groupId } + val artifactIdProvider = project.provider { mainPublication.artifactId } + val versionProvider = project.provider { mainPublication.version } + markerPublication.pom.withXml { xmlProvider -> + val root = xmlProvider.asElement() + val document = root.ownerDocument + val dependencies = root.appendChild(document.createElement("dependencies")) + val dependency = dependencies.appendChild(document.createElement("dependency")) + val groupId = dependency.appendChild(document.createElement("groupId")) + groupId.textContent = groupProvider.get() + val artifactId = dependency.appendChild(document.createElement("artifactId")) + artifactId.textContent = artifactIdProvider.get() + val version = dependency.appendChild(document.createElement("version")) + version.textContent = versionProvider.get() + } + } + } + } + } +} + +private fun String.displayName() = this.split(".").joinToString(separator = "") { it.replaceFirstChar { it.uppercase() } } + +abstract class GratatouilleExtension(private val project: Project) { + fun plugin(id: String, action: Action) { + val spec = PluginSpec(id, project) + action.execute(spec) + } +} + diff --git a/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/MavenPublicationInternal.kt b/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/MavenPublicationInternal.kt new file mode 100644 index 0000000..d822f0d --- /dev/null +++ b/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/MavenPublicationInternal.kt @@ -0,0 +1,13 @@ +package gratatouille.gradle + +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal + +fun MavenPublication.trySetAlias() { + try { + this as MavenPublicationInternal + this.isAlias = true + } catch (_: Exception) { + + } +} \ No newline at end of file diff --git a/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/utils.kt b/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/utils.kt index 82eecb4..d4640c5 100644 --- a/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/utils.kt +++ b/gratatouille-gradle-plugin/src/main/kotlin/gratatouille/gradle/utils.kt @@ -1,9 +1,11 @@ package gratatouille.gradle +import com.gradleup.gratatouille.gradle.BuildConfig import org.gradle.api.Project +import org.gradle.api.artifacts.ExternalDependency -internal fun Project.withPlugins(vararg ids: String, block: () -> Unit) { +internal fun Project.withRequiredPlugins(vararg ids: String, block: () -> Unit) { val pending = ids.toMutableSet() ids.forEach { pluginId -> @@ -20,4 +22,16 @@ internal fun Project.withPlugins(vararg ids: String, block: () -> Unit) { error("Gratatouille requires the following plugin(s): '${pending.joinToString(",")}'") } } +} + +internal fun Project.configureDefaultVersionsResolutionStrategy() { + configurations.configureEach { configuration -> + // Use the API introduced in Gradle 4.4 to modify the dependencies directly before they are resolved: + configuration.withDependencies { dependencySet -> + val pluginVersion = VERSION + dependencySet.filterIsInstance() + .filter { it.group == BuildConfig.group && it.version.isNullOrEmpty() } + .forEach { it.version { constraint -> constraint.require(pluginVersion) } } + } + } } \ No newline at end of file diff --git a/gratatouille-processor/build.gradle.kts b/gratatouille-processor/build.gradle.kts index afd12c8..2a0365f 100644 --- a/gratatouille-processor/build.gradle.kts +++ b/gratatouille-processor/build.gradle.kts @@ -11,5 +11,6 @@ dependencies { implementation(libs.kotlinpoet) implementation(libs.kotlinpoet.ksp) implementation(libs.ksp.api) + implementation(libs.cast) } diff --git a/gratatouille-processor/src/main/kotlin/gratatouille/processor/GTaskAction.kt b/gratatouille-processor/src/main/kotlin/gratatouille/processor/GTaskAction.kt index 29396f9..6d022af 100644 --- a/gratatouille-processor/src/main/kotlin/gratatouille/processor/GTaskAction.kt +++ b/gratatouille-processor/src/main/kotlin/gratatouille/processor/GTaskAction.kt @@ -1,5 +1,6 @@ package gratatouille.processor +import cast.cast import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration @@ -11,20 +12,22 @@ import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.ksp.toTypeName +import com.sun.org.apache.xpath.internal.operations.Bool internal class GTaskAction( - val packageName: String, - val functionName: String, - val annotationName: String?, - val description: String?, - val group: String?, - val parameters: List, - val returnValues: List, - /** - * The coordinates where to find the implementation for this action - * May be null if the plugin is not isolated - */ - val implementationCoordinates: String? + val packageName: String, + val functionName: String, + val annotationName: String?, + val description: String?, + val group: String?, + val parameters: List, + val returnValues: List, + /** + * The coordinates where to find the implementation for this action + * May be null if the plugin is not isolated + */ + val implementationCoordinates: String?, + val pure: Boolean ) internal sealed interface Type @@ -39,198 +42,201 @@ internal class OutputFile : Type internal class OutputDirectory : Type internal class Property( - val type: Type, - val name: String, - val internal: Boolean, - val optional: Boolean, - val manuallyWired: Boolean + val type: Type, + val name: String, + val internal: Boolean, + val optional: Boolean, + val manuallyWired: Boolean ) internal fun KSFunctionDeclaration.toGTaskAction(implementationCoordinates: String?): GTaskAction { - val parameters = mutableListOf() - val returnValues = returnType.toReturnValues() - val reservedNames = setOf(taskName, taskDescription, taskGroup, classpath, workerExecutor) - val returnValuesNames = returnValues.map { it.name }.toSet() - - this.parameters.forEach { valueParameter -> - val resolvedType = valueParameter.type.resolve() - val typename = valueParameter.type.toTypeName() - val optional = typename.isNullable - val rawTypename = typename.copy(nullable = false) - val internal = valueParameter.annotations.containsGInternal() - val name = valueParameter.name?.asString() - ?: error("Gratatouille: anonymous parameters are not supported at ${valueParameter.location}") - - val manuallyWired = valueParameter.annotations.containsManuallyWired() - check(!reservedNames.contains(name)) { - "Gratatouille: parameter name '${name}' is reserved for internal uses. Please use another name at ${valueParameter.location}." - } - check(!returnValuesNames.contains(name)) { - "Gratatouille: parameter name '${name}' is already used as return value. Please use another name at ${valueParameter.location}." - } + val parameters = mutableListOf() + val returnValues = returnType.toReturnValues() + val reservedNames = setOf(taskName, taskDescription, taskGroup, classpath, workerExecutor) + val returnValuesNames = returnValues.map { it.name }.toSet() + + this.parameters.forEach { valueParameter -> + val resolvedType = valueParameter.type.resolve() + val typename = valueParameter.type.toTypeName() + val optional = typename.isNullable + val rawTypename = typename.copy(nullable = false) + val internal = valueParameter.annotations.containsGInternal() + val name = valueParameter.name?.asString() + ?: error("Gratatouille: anonymous parameters are not supported at ${valueParameter.location}") + + val manuallyWired = valueParameter.annotations.containsManuallyWired() + check(!reservedNames.contains(name)) { + "Gratatouille: parameter name '${name}' is reserved for internal uses. Please use another name at ${valueParameter.location}." + } + check(!returnValuesNames.contains(name)) { + "Gratatouille: parameter name '${name}' is already used as return value. Please use another name at ${valueParameter.location}." + } - val parameterType: Type = when { - rawTypename == ClassName("gratatouille", "GOutputFile") -> { - OutputFile() - } - - rawTypename == ClassName("gratatouille", "GOutputDirectory") -> { - OutputDirectory() - } - - rawTypename == ClassName("gratatouille", "GInputFile") -> InputFile - rawTypename == ClassName("gratatouille", "GInputFiles") -> { - check(!optional) { - "Gratatouille: optional GInputFiles are not supported ${valueParameter.location}" - } - InputFiles - } - - rawTypename == ClassName("gratatouille", "GInputDirectory") -> InputDirectory - rawTypename.isSimpleJvmObject() -> JvmType(rawTypename) - resolvedType.isSerializable() -> KotlinxSerializableInput(rawTypename) - else -> error("Gratatouille: '$rawTypename' is not a supported parameter at ${valueParameter.location}") + val parameterType: Type = when { + rawTypename == ClassName("gratatouille", "GOutputFile") -> { + OutputFile() + } + + rawTypename == ClassName("gratatouille", "GOutputDirectory") -> { + OutputDirectory() + } + + rawTypename == ClassName("gratatouille", "GInputFile") -> InputFile + rawTypename == ClassName("gratatouille", "GInputFiles") -> { + check(!optional) { + "Gratatouille: optional GInputFiles are not supported ${valueParameter.location}" } + InputFiles + } + + rawTypename == ClassName("gratatouille", "GInputDirectory") -> InputDirectory + rawTypename.isSimpleJvmObject() -> JvmType(rawTypename) + resolvedType.isSerializable() -> KotlinxSerializableInput(rawTypename) + else -> error("Gratatouille: '$rawTypename' is not a supported parameter at ${valueParameter.location}") + } - when (parameterType) { - is OutputDirectory, is OutputFile -> { - check(!internal) { - "Gratatouille: outputs cannot be annotated with @GInternal at ${valueParameter.location}" - } - check(!optional) { - "Gratatouille: outputs cannot be optional at ${valueParameter.location}" - } - } - - else -> { - check(!manuallyWired) { - "Gratatouille: inputs cannot be annotated with @GManuallyWired at ${valueParameter.location}" - } - } + when (parameterType) { + is OutputDirectory, is OutputFile -> { + check(!internal) { + "Gratatouille: outputs cannot be annotated with @GInternal at ${valueParameter.location}" } + check(!optional) { + "Gratatouille: outputs cannot be optional at ${valueParameter.location}" + } + } - parameters.add( - Property( - name = name, - type = parameterType, - internal = internal, - optional = optional, - manuallyWired = manuallyWired - ) - ) + else -> { + check(!manuallyWired) { + "Gratatouille: inputs cannot be annotated with @GManuallyWired at ${valueParameter.location}" + } + } } - val gTaskActionAnnotation = annotations.first { it.shortName.asString() == "GTaskAction" } - val name = gTaskActionAnnotation.arguments.firstOrNull { it.name?.asString() == "name" } - ?.takeIf { it.origin != Origin.SYNTHETIC }?.value?.toString() - val description = gTaskActionAnnotation.arguments.firstOrNull { it.name?.asString() == "description" } - ?.takeIf { it.origin != Origin.SYNTHETIC }?.value?.toString() - val group = gTaskActionAnnotation.arguments.firstOrNull { it.name?.asString() == "group" } - ?.takeIf { it.origin != Origin.SYNTHETIC }?.value?.toString() - - return GTaskAction( - packageName = this.packageName.asString(), - functionName = this.simpleName.asString(), - parameters = parameters, - returnValues = returnValues, - annotationName = name, - description = description, - group = group, - implementationCoordinates = implementationCoordinates + parameters.add( + Property( + name = name, + type = parameterType, + internal = internal, + optional = optional, + manuallyWired = manuallyWired + ) ) + } + + val gTaskActionAnnotation = annotations.first { it.shortName.asString() == "GTaskAction" } + val name = gTaskActionAnnotation.arguments.firstOrNull { it.name?.asString() == "name" } + ?.takeIf { it.origin != Origin.SYNTHETIC }?.value?.toString() + val description = gTaskActionAnnotation.arguments.firstOrNull { it.name?.asString() == "description" } + ?.takeIf { it.origin != Origin.SYNTHETIC }?.value?.toString() + val group = gTaskActionAnnotation.arguments.firstOrNull { it.name?.asString() == "group" } + ?.takeIf { it.origin != Origin.SYNTHETIC }?.value?.toString() + val pure = gTaskActionAnnotation.arguments.firstOrNull { it.name?.asString() == "pure" } + ?.takeIf { it.origin != Origin.SYNTHETIC }?.value?.cast() != false + + return GTaskAction( + packageName = this.packageName.asString(), + functionName = this.simpleName.asString(), + parameters = parameters, + returnValues = returnValues, + annotationName = name, + description = description, + group = group, + implementationCoordinates = implementationCoordinates, + pure = pure + ) } private fun KSTypeReference?.toReturnValues(): List { - if (this == null) { - return emptyList() - } - - val typename = toTypeName() - check(!typename.isNullable) { - "Gratatouille: optional outputs are not supported $location" - } - - if (typename == ClassName("kotlin", "Unit")) { - return emptyList() - } - - val resolvedType = this.resolve() - if (resolvedType.isSerializable()) { - return listOf(Property(KotlinxSerializableOutput(typename), outputFile, false, false, false)) - } + if (this == null) { + return emptyList() + } + + val typename = toTypeName() + check(!typename.isNullable) { + "Gratatouille: optional outputs are not supported $location" + } + + if (typename == ClassName("kotlin", "Unit")) { + return emptyList() + } + + val resolvedType = this.resolve() + if (resolvedType.isSerializable()) { + return listOf(Property(KotlinxSerializableOutput(typename), outputFile, false, false, false)) + } // if (typename.isSimpleJvmObject()) { // return listOf(ReturnValue(outputFile, JvmType(typename))) // } - val declaration = resolvedType.declaration - check(declaration is KSClassDeclaration) { - "Gratatouille: only classes are allowed as return values" - } + val declaration = resolvedType.declaration + check(declaration is KSClassDeclaration) { + "Gratatouille: only classes are allowed as return values" + } - return declaration.getAllProperties().filter { it.isPublic() }.toList().map { - it.toReturnValue() - } + return declaration.getAllProperties().filter { it.isPublic() }.toList().map { + it.toReturnValue() + } } private fun KSPropertyDeclaration.toReturnValue(): Property { - val typename = type.toTypeName() - val resolvedType = type.resolve() - - check(!typename.isNullable) { - "Gratatouille: optional outputs are not supported $location" - } - if (resolvedType.isSerializable()) { - return Property(KotlinxSerializableOutput(typename), simpleName.asString(), false, false, false) - } + val typename = type.toTypeName() + val resolvedType = type.resolve() + + check(!typename.isNullable) { + "Gratatouille: optional outputs are not supported $location" + } + if (resolvedType.isSerializable()) { + return Property(KotlinxSerializableOutput(typename), simpleName.asString(), false, false, false) + } // if (typename.isSimpleJvmObject()) { // return ReturnValue(outputFile, JvmType(typename)) // } - error("Gratatouille: property '${simpleName.asString()}' cannot be serialize to a file at $location") + error("Gratatouille: property '${simpleName.asString()}' cannot be serialize to a file at $location") } private fun KSType.isSerializable(): Boolean { - val declaration = this.declaration - if (declaration !is KSClassDeclaration) { - return false - } - return declaration.annotations.any { - it.annotationType.toTypeName() == ClassName("kotlinx.serialization", "Serializable") - } + val declaration = this.declaration + if (declaration !is KSClassDeclaration) { + return false + } + return declaration.annotations.any { + it.annotationType.toTypeName() == ClassName("kotlinx.serialization", "Serializable") + } } private fun Sequence.containsGInternal(): Boolean { - return any { - it.annotationType.toTypeName() == ClassName("gratatouille", "GInternal") - } + return any { + it.annotationType.toTypeName() == ClassName("gratatouille", "GInternal") + } } private fun Sequence.containsManuallyWired(): Boolean { - return any { - it.annotationType.toTypeName() == ClassName("gratatouille", "GManuallyWired") - } + return any { + it.annotationType.toTypeName() == ClassName("gratatouille", "GManuallyWired") + } } private fun TypeName.isSimpleJvmObject(): Boolean { - return when (this) { - is ClassName -> when (this.canonicalName) { - "kotlin.String", "kotlin.Float", "kotlin.Int", "kotlin.Boolean", "kotlin.Double" -> true - else -> false - } - - is ParameterizedTypeName -> when (this.rawType.canonicalName) { - "kotlin.collections.Set", "kotlin.collections.List", "kotlin.collections.Map" -> { - this.typeArguments.all { - it.isSimpleJvmObject() - } - } + return when (this) { + is ClassName -> when (this.canonicalName) { + "kotlin.String", "kotlin.Float", "kotlin.Int", "kotlin.Boolean", "kotlin.Double" -> true + else -> false + } - else -> false + is ParameterizedTypeName -> when (this.rawType.canonicalName) { + "kotlin.collections.Set", "kotlin.collections.List", "kotlin.collections.Map" -> { + this.typeArguments.all { + it.isSimpleJvmObject() } + } - else -> false + else -> false } + + else -> false + } } diff --git a/gratatouille-processor/src/main/kotlin/gratatouille/processor/Task.kt b/gratatouille-processor/src/main/kotlin/gratatouille/processor/Task.kt index 974a062..0ece0a5 100644 --- a/gratatouille-processor/src/main/kotlin/gratatouille/processor/Task.kt +++ b/gratatouille-processor/src/main/kotlin/gratatouille/processor/Task.kt @@ -9,6 +9,11 @@ internal fun GTaskAction.taskFile(): FileSpec { val fileSpec = FileSpec.builder(className) .addFunction(register()) + .apply { + if (!pure) { + addBodyComment("This task is impure and not cacheable") + } + } .addType(task()) .addType(workParameters()) .addType(workAction()) @@ -134,10 +139,16 @@ private fun Type.toProviderType(): TypeName { private fun GTaskAction.task(): TypeSpec { return TypeSpec.classBuilder(taskClassName().simpleName) - .addAnnotation( - AnnotationSpec.builder(ClassName("org.gradle.api.tasks", "CacheableTask")) - .build() - ) + .apply { + if (pure) { + addAnnotation( + AnnotationSpec.builder(ClassName("org.gradle.api.tasks", "CacheableTask")) + .build() + ) + } else { + addFunction(FunSpec.builder("init").addCode("outputs.upToDateWhen { false }").build()) + } + } .addModifiers(KModifier.ABSTRACT, KModifier.INTERNAL) .superclass(ClassName("org.gradle.api", "DefaultTask")) .apply { diff --git a/libs.versions.toml b/libs.versions.toml index e9fa635..cae0a56 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -20,6 +20,7 @@ kotlinpoet = { group = "com.squareup", name = "kotlinpoet", version.ref = "kotli kotlinpoet-ksp = { group = "com.squareup", name = "kotlinpoet-ksp", version.ref = "kotlinpoet" } librarian = { module = "com.gradleup.librarian:librarian-gradle-plugin", version.ref = "librarian" } okio = "com.squareup.okio:okio:3.9.0" +cast = "net.mbonnin.cast:cast:0.0.1" [plugins] librarian = { id = "com.gradleup.librarian", version.ref = "librarian"} \ No newline at end of file diff --git a/test-plugin/test-plugin/build.gradle.kts b/test-plugin/test-plugin/build.gradle.kts index 1613f66..1a9d135 100644 --- a/test-plugin/test-plugin/build.gradle.kts +++ b/test-plugin/test-plugin/build.gradle.kts @@ -13,4 +13,10 @@ gradlePlugin { this.id = "testplugin" } } +} + +gratatouille { + plugin("testplugin") { + implementationClass("testplugin.TestPlugin") + } } \ No newline at end of file