Convention and tooling plugins are how large organizations standardize builds; the lazy API is what makes them cache-friendly.
A binary plugin is a class implementing Plugin<Project>, usually written in Kotlin in build-logic (an included build) or a standalone repository published to a plugin repository. Its apply(project) method registers tasks, applies other plugins and creates an extension that users configure.
Gradle's modern API is lazy:
- Use
Property<T>,ListProperty<T>,RegularFilePropertyandDirectoryPropertyfor extension and task fields, withabstract valso Gradle injects managed instances. - Wire values with providers instead of reading them eagerly:
task.outputDir.set(ext.outputDir)links the two; the value is computed only when the task runs. Transform withmap { }andflatMap { };project.provider { }wraps arbitrary code. - Register tasks with
tasks.register, configure others withtasks.named, and avoidproject.afterEvaluate. - Never call
projectorProjectservices from@TaskAction; inject services (ExecOperations,FileSystemOperations,ObjectFactory) via@Injectinstead. This keeps tasks compatible with the configuration cache.
Annotate task properties (@Input, @InputFiles, @OutputDirectory, @Internal, @Nested) so up-to-date checks and caching work; mark the task @CacheableTask when outputs are deterministic. Work can be parallelized with the Worker API.
Test plugins with Gradle TestKit (GradleRunner), which runs a real build in a temp directory, and declare plugin ids in the gradlePlugin { plugins { } } block of the java-gradle-plugin so they can be applied by id. Publish with the com.gradle.plugin-publish plugin.
Example
// build-logic/src/main/kotlin/com/acme/GreetingPlugin.kt
package com.acme
import org.gradle.api.*
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
abstract class GreetingExtension { abstract val message: Property<String> }
@CacheableTask
abstract class GreetTask : DefaultTask() {
@get:Input abstract val message: Property<String>
@get:OutputFile abstract val output: RegularFileProperty
@TaskAction fun run() = output.get().asFile.writeText(message.get())
}
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
val ext = project.extensions.create("greeting", GreetingExtension::class.java)
ext.message.convention("Hello") // default, overridable
project.tasks.register("greet", GreetTask::class.java) {
it.message.set(ext.message.map { m -> "$m from ${project.name}" }) // lazy wiring
it.output.set(project.layout.buildDirectory.file("greeting.txt"))
}
}
}
// consumer build.gradle.kts: plugins { id("com.acme.greeting") } greeting { message = "Hi" }
// $ ./gradlew greet && cat build/greeting.txt -> Hi from appRemember
- A plugin class registers tasks and creates an extension in apply()
- Use Property/Provider types and map/flatMap for lazy wiring
- Annotate task inputs/outputs; inject services instead of using Project in actions
- Test with TestKit's GradleRunner
Watch out for
- Reading extension values eagerly in apply(), before the user's build script has set them.