Skip to content

Commit

Permalink
add code to generate plugin markers
Browse files Browse the repository at this point in the history
  • Loading branch information
martinbonnin committed Jan 3, 2025
1 parent 599ece9 commit 3789a02
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 178 deletions.
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions gratatouille-gradle-plugin/librarian.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
version.packageName=gratatouille.gradle
Original file line number Diff line number Diff line change
@@ -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<String>

@get:Input
abstract val implementationClass: Property<String>

@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)")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Directory>)

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<DescriptorConnection>) {
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<PluginSpec>) {
val spec = PluginSpec(id, project)
action.execute(spec)
}
}

Original file line number Diff line number Diff line change
@@ -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) {

}
}
Original file line number Diff line number Diff line change
@@ -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 ->
Expand All @@ -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<ExternalDependency>()
.filter { it.group == BuildConfig.group && it.version.isNullOrEmpty() }
.forEach { it.version { constraint -> constraint.require(pluginVersion) } }
}
}
}
1 change: 1 addition & 0 deletions gratatouille-processor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ dependencies {
implementation(libs.kotlinpoet)
implementation(libs.kotlinpoet.ksp)
implementation(libs.ksp.api)
implementation(libs.cast)
}

Loading

0 comments on commit 3789a02

Please sign in to comment.