skills/standards-gradle/SKILL.md
Gradle build tool standards focusing on Kotlin DSL. Covers project configuration, dependency management, and custom plugin/task development with Gradle 9 LTS.
npx skillsauth add b33eep/claude-code-setup standards-gradleInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides comprehensive guidance for Gradle build configuration using Kotlin DSL (.gradle.kts). It covers both everyday project configuration and advanced plugin/task development patterns based on Gradle 9 LTS.
This section covers the common scenarios developers encounter when configuring Gradle projects: setting up build scripts, managing dependencies, applying plugins, and structuring multi-module projects.
Organize your build script in a consistent, readable order:
// 1. Plugin declarations (always first)
plugins {
java
application
id("com.github.johnrengelman.shadow") version "8.1.1"
}
// 2. Project properties and versioning
group = "com.example"
version = "1.0.0"
// 3. Repositories
repositories {
mavenCentral()
}
// 4. Dependencies
dependencies {
implementation("com.google.guava:guava:33.0.0-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
// 5. Java/Kotlin configuration
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
// 6. Task configuration
tasks {
test {
useJUnitPlatform()
}
jar {
manifest {
attributes("Main-Class" to "com.example.Main")
}
}
}
// Root project name
rootProject.name = "my-project"
// Enable Gradle version catalogs (Gradle 9+)
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
// Include subprojects
include("app")
include("lib")
include("common")
// Optional: Customize subproject location
project(":app").projectDir = file("applications/app")
repositories {
// GOOD: Standard repositories first
mavenCentral()
// GOOD: Google repository for Android/Google libraries
google()
// GOOD: Custom repository with HTTPS
maven {
name = "CompanyRepo"
url = uri("https://repo.company.com/maven")
credentials {
username = providers.gradleProperty("repoUser").orNull
password = providers.gradleProperty("repoPassword").orNull
}
}
}
// BAD: Using HTTP instead of HTTPS (security risk)
// maven { url = uri("http://insecure-repo.com/maven") }
// BAD: Exposing credentials in build script
// maven {
// url = uri("https://repo.company.com/maven")
// credentials {
// username = "hardcoded-user" // Never do this!
// password = "hardcoded-pass" // Never do this!
// }
// }
// GOOD: Use extra properties for shared values
val mockitoVersion by extra("5.10.0")
val junitVersion by extra("5.10.2")
dependencies {
testImplementation("org.mockito:mockito-core:$mockitoVersion")
testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion")
}
// GOOD: Extract complex configuration to functions
fun configureJavaToolchain() {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ADOPTIUM
}
}
}
// Apply configuration
configureJavaToolchain()
Understanding Gradle's build phases is essential for writing efficient build scripts and understanding when your code executes.
Every Gradle build runs through three distinct phases in order:
Understanding these phases helps you:
Purpose: Determine project structure and which projects participate in the build.
What runs: settings.gradle.kts files
What happens:
settings.gradle.ktsProject instances for each projectExample:
// settings.gradle.kts (runs during initialization)
rootProject.name = "my-project"
println("Initialization phase") // Prints during initialization
include("app")
include("lib")
include("common")
// Optional: Customize subproject directories
project(":app").projectDir = file("applications/app")
Duration: Very fast (typically < 100ms)
Key Point: You cannot access Project objects yet - they're being created.
Purpose: Configure all tasks and build the task execution graph.
What runs: All build.gradle.kts files for participating projects
What happens:
Example:
// build.gradle.kts (runs during configuration)
plugins {
java // Runs during configuration
}
version = "1.0.0" // Runs during configuration
println("Configuration phase") // Runs during configuration
tasks.register("myTask") {
group = "custom" // Runs during configuration
description = "Example task" // Runs during configuration
println("Task configuration") // Runs during configuration
doLast {
println("Task execution") // Does NOT run during configuration!
}
}
// This runs during configuration
val projectVersion = version
println("Project version: $projectVersion")
// BAD: Expensive work during configuration
// val allFiles = File("src").walkTopDown().toList() // Slows every build!
// GOOD: Use providers for lazy evaluation
val sourceFiles: Provider<FileTree> = providers.provider {
fileTree("src") // Only evaluated when needed
}
Duration: Can be slow if not careful (seconds to minutes for large projects)
Key Point: Configuration runs on every build, even if no tasks execute. Keep it fast!
Purpose: Execute the selected tasks in dependency order.
What runs: Task actions (doFirst, doLast, @TaskAction)
What happens:
Example:
tasks.register("myTask") {
// Configuration phase
group = "custom"
doFirst {
// Execution phase - runs first
println("Starting task")
}
doLast {
// Execution phase - runs last
println("Task completed")
}
}
// Abstract task with @TaskAction
abstract class BuildTask : DefaultTask() {
@get:InputDirectory
abstract val sourceDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction // Execution phase
fun build() {
println("Building...")
// Actual work happens here
}
}
Duration: Depends on what tasks do (compile, test, package, etc.)
Key Point: Only requested tasks (and their dependencies) execute.
| Code Location | Phase | Example |
|---------------|-------|---------|
| settings.gradle.kts (top-level) | Initialization | rootProject.name = "app" |
| build.gradle.kts (top-level) | Configuration | version = "1.0" |
| plugins {} block | Configuration | java |
| dependencies {} block | Configuration | implementation(...) |
| tasks.register { } outer block | Configuration | group = "custom" |
| tasks.register { } inner block | Configuration | dependsOn("other") |
| Extension configuration blocks | Configuration | java { toolchain { } } |
| doFirst { } | Execution | println("starting") |
| doLast { } | Execution | println("done") |
| @TaskAction method | Execution | fun execute() { } |
| Provider.get() in doLast | Execution | val v = provider.get() |
// ❌ BAD: Expensive I/O during configuration
tasks.register("badTask") {
val files = File("src").listFiles() // I/O during configuration - runs every build!
println("Found ${files?.size} files")
doLast {
println("Processing ${files?.size} files")
}
}
// ✅ GOOD: Defer work to execution
tasks.register("goodTask") {
doLast {
val files = File("src").listFiles() // I/O during execution - only when task runs
println("Found ${files?.size} files")
println("Processing ${files.size} files")
}
}
// ❌ BAD: Accessing task outputs during configuration
tasks.register("badConsumer") {
val compileOutput = tasks.named("compileJava").get().outputs.files // Not ready yet!
doLast {
println(compileOutput)
}
}
// ✅ GOOD: Use providers to defer access
tasks.register("goodConsumer") {
val compileOutput = tasks.named("compileJava").map { it.outputs.files }
doLast {
println(compileOutput.get()) // Resolved during execution
}
}
// ❌ BAD: Network calls during configuration
tasks.register("badFetch") {
val response = URL("https://api.example.com/version").readText() // Slows every build!
doLast {
println("Version: $response")
}
}
// ✅ GOOD: Use providers for network calls
tasks.register("goodFetch") {
val response: Provider<String> = providers.provider {
URL("https://api.example.com/version").readText()
}
doLast {
println("Version: ${response.get()}") // Only called during execution
}
}
// ❌ BAD: Calling .get() on providers during configuration
tasks.register("badProvider") {
val version = providers.gradleProperty("version").get() // Eager evaluation!
doLast {
println("Version: $version")
}
}
// ✅ GOOD: Defer .get() until execution
tasks.register("goodProvider") {
val version = providers.gradleProperty("version") // Lazy - not evaluated yet
doLast {
println("Version: ${version.get()}") // Evaluated here
}
}
// ❌ BAD: Mutating shared state during configuration
var counter = 0 // Global mutable state
tasks.register("bad1") {
counter++ // Modifies global state during configuration
doLast { println("Counter: $counter") }
}
tasks.register("bad2") {
counter++ // Order-dependent!
doLast { println("Counter: $counter") }
}
// ✅ GOOD: Use build services or task outputs for shared state
1. Build Performance
Configuration phase runs on every build:
./gradlew tasks # Configuration runs
./gradlew clean # Configuration runs
./gradlew build # Configuration runs
./gradlew --stop # Configuration runs
Slow configuration = slow every command, even ./gradlew tasks!
2. Configuration Cache
Configuration cache stores the result of configuration phase:
# First run: Configuration + execution
./gradlew build --configuration-cache
# Configuration phase: 5 seconds
# Execution phase: 30 seconds
# Second run: Execution only
./gradlew clean build --configuration-cache
# Configuration phase: 0 seconds (reused from cache!)
# Execution phase: 30 seconds
Benefits:
Requirements:
project during execution3. Up-to-Date Checks
Tasks are up-to-date when:
Input/output annotations are evaluated during:
Proper annotations enable:
FROM-CACHE and UP-TO-DATE optimizationsDo:
tasks.register() for lazy task creation@Input/@Output annotations properly--configuration-cache to catch issuesDon't:
tasks.create() (eager - prefer register()).get() on providers during configurationproject references in task actions# See configuration time breakdown
./gradlew build --profile
# Open: build/reports/profile/profile-<timestamp>.html
# Measure configuration time
./gradlew build --configuration-cache --configuration-cache-problems=warn
# See what runs during configuration
./gradlew build --info | grep "Configuration"
# Test configuration cache compatibility
./gradlew build --configuration-cache
./gradlew clean build --configuration-cache # Should show "Reusing configuration cache"
// settings.gradle.kts
println("1. Initialization phase: settings.gradle.kts")
rootProject.name = "lifecycle-demo"
// build.gradle.kts
println("2. Configuration phase: build.gradle.kts top-level")
plugins {
java
println("3. Configuration phase: plugins block")
}
println("4. Configuration phase: after plugins")
tasks.register("demo") {
println("5. Configuration phase: task configuration")
group = "demo"
description = "Demonstrates build phases"
doFirst {
println("7. Execution phase: doFirst")
}
doLast {
println("8. Execution phase: doLast")
}
}
println("6. Configuration phase: after task registration")
// When you run: ./gradlew demo
// Output order:
// 1. Initialization phase: settings.gradle.kts
// 2. Configuration phase: build.gradle.kts top-level
// 3. Configuration phase: plugins block
// 4. Configuration phase: after plugins
// 5. Configuration phase: task configuration
// 6. Configuration phase: after task registration
// 7. Execution phase: doFirst
// 8. Execution phase: doLast
dependencies {
// GOOD: implementation - for internal dependencies (not exposed to consumers)
implementation("com.google.guava:guava:33.0.0-jre")
// GOOD: api - for dependencies exposed to consumers (libraries only)
// Only available with java-library plugin
api("org.apache.commons:commons-lang3:3.14.0")
// GOOD: compileOnly - compile-time only (not packaged)
compileOnly("org.projectlombok:lombok:1.18.30")
// GOOD: runtimeOnly - runtime only (not on compile classpath)
runtimeOnly("com.h2database:h2:2.2.224")
// GOOD: testImplementation - for test code only
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("org.mockito:mockito-core:5.10.0")
// GOOD: testRuntimeOnly - test runtime only
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
// BAD: Using 'compile' (deprecated in Gradle 7+)
// dependencies {
// compile("some:library:1.0") // Use 'implementation' instead
// }
// BAD: Using 'runtime' (deprecated in Gradle 7+)
// dependencies {
// runtime("some:library:1.0") // Use 'runtimeOnly' instead
// }
gradle/libs.versions.toml:
[versions]
guava = "33.0.0-jre"
junit = "5.10.2"
mockito = "5.10.0"
kotlin = "2.0.0"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
[bundles]
testing = ["junit-jupiter", "mockito-core"]
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" }
build.gradle.kts:
plugins {
alias(libs.plugins.kotlin.jvm)
}
dependencies {
// GOOD: Type-safe accessors from version catalog
implementation(libs.guava)
testImplementation(libs.bundles.testing)
testRuntimeOnly(libs.junit.platform.launcher)
}
// Benefits:
// - Centralized version management
// - Type-safe accessors with IDE completion
// - Easy to share across multi-module projects
// - Prevents version conflicts
dependencies {
implementation("com.example:library:1.0")
// GOOD: Force specific version to resolve conflicts
constraints {
implementation("org.slf4j:slf4j-api:2.0.9") {
because("Earlier versions have security vulnerabilities")
}
}
// GOOD: Align versions across dependency group
constraints {
implementation("org.springframework.boot:spring-boot-starter-web:3.2.0")
implementation("org.springframework.boot:spring-boot-starter-data-jpa:3.2.0")
}
}
dependencies {
// GOOD: Import BOM (Bill of Materials) for version alignment
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.0"))
// Now you can omit versions - they come from the BOM
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// GOOD: For testing, use testImplementation(platform(...))
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
dependencies {
// GOOD: Exclude specific transitive dependency
implementation("com.example:library:1.0") {
exclude(group = "commons-logging", module = "commons-logging")
}
// GOOD: Exclude all transitive dependencies (rare case)
implementation("com.example:utility:1.0") {
isTransitive = false
}
// Replace excluded dependency with alternative
implementation("org.slf4j:jcl-over-slf4j:2.0.9")
}
// GOOD: Exclude globally (affects all dependencies)
configurations.all {
exclude(group = "commons-logging", module = "commons-logging")
}
dependencies {
// GOOD: String notation (most common)
implementation("com.google.guava:guava:33.0.0-jre")
// GOOD: Map notation (when you need more control)
implementation(group = "com.google.guava", name = "guava", version = "33.0.0-jre")
// GOOD: With classifier
implementation("net.java.dev.jna:jna:5.13.0:jpms")
// GOOD: Local file dependency
implementation(files("libs/custom-library.jar"))
// GOOD: File tree dependency
implementation(fileTree("libs") { include("*.jar") })
// GOOD: Project dependency (multi-module)
implementation(project(":common"))
}
plugins {
// GOOD: Core plugins (no version needed)
java
application
// GOOD: External plugin with version
id("com.github.johnrengelman.shadow") version "8.1.1"
// GOOD: Kotlin plugin
kotlin("jvm") version "2.0.0"
// GOOD: Apply false (for root project in multi-module)
id("org.springframework.boot") version "3.2.0" apply false
}
// BAD: Old apply() syntax (avoid in new code)
// apply(plugin = "java") // Use plugins {} block instead
// gradle/libs.versions.toml
// [plugins]
// kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version = "2.0.0" }
// shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" }
plugins {
// GOOD: Type-safe plugin declaration from catalog
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.shadow)
}
Java Plugin:
plugins {
java
}
java {
// GOOD: Use Java toolchain (modern approach)
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ADOPTIUM
}
// GOOD: Configure compatibility (legacy approach)
// sourceCompatibility = JavaVersion.VERSION_21
// targetCompatibility = JavaVersion.VERSION_21
// GOOD: Enable automatic module name for JPMS
modularity.inferModulePath = true
// GOOD: Generate sources and javadoc JARs
withSourcesJar()
withJavadocJar()
}
Kotlin JVM Plugin:
plugins {
kotlin("jvm") version "2.0.0"
}
kotlin {
// GOOD: Set JVM target
jvmToolchain(21)
// GOOD: Enable explicit API mode (libraries)
explicitApi()
// GOOD: Compiler options
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
allWarningsAsErrors = true
}
}
Application Plugin:
plugins {
application
}
application {
// GOOD: Set main class
mainClass = "com.example.Main"
// GOOD: Configure application name
applicationName = "my-app"
// GOOD: Set default JVM args
applicationDefaultJvmArgs = listOf("-Xmx512m", "-Xms256m")
}
// Run with: ./gradlew run
// Package with: ./gradlew installDist
Java Library Plugin:
plugins {
`java-library` // Note the backticks for kebab-case
}
dependencies {
// GOOD: Use 'api' for exposed dependencies
api("org.apache.commons:commons-lang3:3.14.0")
// GOOD: Use 'implementation' for internal dependencies
implementation("com.google.guava:guava:33.0.0-jre")
}
// Consumers of this library get:
// - api dependencies on their compile classpath
// - implementation dependencies are hidden
plugins {
java
jacoco
}
// GOOD: Configure extension in dedicated block
jacoco {
toolVersion = "0.8.11"
reportsDirectory = layout.buildDirectory.dir("reports/jacoco")
}
// GOOD: Configure task created by plugin
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required = true
html.required = true
csv.required = false
}
}
// BAD: Accessing extension before plugin is applied
// jacoco { ... } // Will fail if jacoco plugin not applied
// plugins { jacoco } // Plugin should come first
plugins {
java
if (project.hasProperty("enableKotlin")) {
kotlin("jvm") version "2.0.0"
}
}
// Alternative: Apply plugin conditionally
if (project.findProperty("coverage") == "true") {
apply(plugin = "jacoco")
}
my-project/
├── settings.gradle.kts # Project structure definition
├── build.gradle.kts # Root build script
├── gradle/
│ └── libs.versions.toml # Shared version catalog
├── app/
│ ├── build.gradle.kts # Application module
│ └── src/
├── lib/
│ ├── build.gradle.kts # Library module
│ └── src/
└── common/
├── build.gradle.kts # Shared code module
└── src/
settings.gradle.kts:
rootProject.name = "my-project"
// Enable type-safe project accessors (Gradle 7+)
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include("app")
include("lib")
include("common")
// Optional: Nested modules
include("backend:api")
include("backend:service")
Root build.gradle.kts:
plugins {
// GOOD: Apply plugins to all subprojects
java apply false
kotlin("jvm") version "2.0.0" apply false
}
// GOOD: Configure all projects (including root)
allprojects {
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
}
// GOOD: Configure only subprojects
subprojects {
// Apply common configuration here
}
Convention plugins encapsulate shared configuration in a type-safe, reusable way.
buildSrc/build.gradle.kts:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
buildSrc/src/main/kotlin/java-conventions.gradle.kts:
plugins {
java
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
}
app/build.gradle.kts:
plugins {
id("java-conventions") // Apply convention plugin
application
}
application {
mainClass = "com.example.app.Main"
}
dependencies {
implementation(project(":lib"))
implementation(project(":common"))
}
lib/build.gradle.kts:
plugins {
id("java-conventions") // Apply convention plugin
`java-library`
}
dependencies {
api(project(":common"))
implementation("com.google.guava:guava:33.0.0-jre")
}
dependencies {
// GOOD: Type-safe project accessor (with TYPESAFE_PROJECT_ACCESSORS)
implementation(projects.common)
implementation(projects.backend.api)
// GOOD: String-based (works without feature preview)
implementation(project(":common"))
implementation(project(":backend:api"))
// GOOD: Depend on specific configuration
testImplementation(project(path = ":lib", configuration = "testFixtures"))
}
Pattern 1: subprojects {} (Quick but limited)
subprojects {
apply(plugin = "java")
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
}
// BAD: Hard to override, not type-safe, mixes concerns
Pattern 2: Convention Plugins (Recommended)
// buildSrc/src/main/kotlin/java-library-conventions.gradle.kts
plugins {
`java-library`
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
// GOOD: Type-safe, reusable, easy to override
// Modules apply with: plugins { id("java-library-conventions") }
buildSrc (For Convention Plugins):
project/
├── buildSrc/
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ └── java-conventions.gradle.kts
└── build.gradle.kts
Included Builds (For Build Logic Libraries):
settings.gradle.kts:
includeBuild("build-logic")
include("app")
include("lib")
Composite Builds (For Multi-Repo Projects):
settings.gradle.kts:
includeBuild("../other-project")
Using Version Catalogs (Recommended):
// gradle/libs.versions.toml (at root)
[versions]
guava = "33.0.0-jre"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
// All modules can use: implementation(libs.guava)
Platform Projects (Alternative):
// platform/build.gradle.kts
plugins {
`java-platform`
}
dependencies {
constraints {
api("com.google.guava:guava:33.0.0-jre")
api("org.slf4j:slf4j-api:2.0.9")
}
}
// Other modules:
dependencies {
implementation(platform(project(":platform")))
implementation("com.google.guava:guava") // Version from platform
}
Gradle 9 is the latest LTS (Long-Term Support) release with significant improvements to performance, developer experience, and build reliability.
Configuration cache dramatically speeds up builds by caching the result of the configuration phase.
Enable in gradle.properties:
org.gradle.configuration-cache=true
Or via command line:
./gradlew build --configuration-cache
Benefits:
Making Your Build Compatible:
// GOOD: Use providers instead of direct property access
val myProperty: Provider<String> = providers.gradleProperty("myProp")
tasks.register("example") {
doLast {
println(myProperty.get()) // Lazy evaluation
}
}
// BAD: Direct property access (breaks configuration cache)
// val value = project.findProperty("myProp") // Evaluated at configuration time
Enable in gradle.properties:
org.gradle.caching=true
Or via command line:
./gradlew build --build-cache
Configure cache:
buildCache {
local {
isEnabled = true
directory = file("${rootDir}/.gradle/build-cache")
removeUnusedEntriesAfterDays = 30
}
remote<HttpBuildCache> {
isEnabled = true
url = uri("https://cache.example.com/")
isPush = System.getenv("CI") == "true" // Only push from CI
credentials {
username = providers.gradleProperty("cacheUser").orNull
password = providers.gradleProperty("cachePassword").orNull
}
}
}
testing {
suites {
val test by getting(JvmTestSuite::class) {
useJUnitJupiter("5.10.2")
}
// GOOD: Define integration test suite
val integrationTest by registering(JvmTestSuite::class) {
testType = TestSuiteType.INTEGRATION_TEST
dependencies {
implementation(project())
implementation("org.testcontainers:junit-jupiter:1.19.3")
}
targets {
all {
testTask.configure {
shouldRunAfter(test)
}
}
}
}
}
}
// Run with: ./gradlew integrationTest
java {
toolchain {
// GOOD: Specify vendor
vendor = JvmVendorSpec.ADOPTIUM
languageVersion = JavaLanguageVersion.of(21)
// GOOD: Gradle auto-downloads if not available
}
}
// GOOD: Use different toolchain for specific task
tasks.register<JavaExec>("runWithJava17") {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(17)
}
}
Better error reporting and problem aggregation:
// Gradle automatically collects and reports problems
// Your build output now shows:
// - Aggregated problems
// - Actionable error messages
// - Problem locations with file:line references
// No configuration needed - it just works better!
Parallel configuration of subprojects for massive multi-module builds.
# gradle.properties
org.gradle.unsafe.isolated-projects=true
Benefits:
// BAD: compile, runtime configurations (removed in Gradle 8+)
// dependencies {
// compile("some:library:1.0") // Use 'implementation'
// runtime("some:library:1.0") // Use 'runtimeOnly'
// }
// BAD: Old task creation API (prefer register)
// tasks.create("myTask") { ... } // Use tasks.register("myTask") { ... }
// BAD: Convention properties (use extensions)
// project.convention.plugins // Use project.extensions
// BAD: Direct task execution during configuration
// tasks.named("build").get().execute() // Never execute tasks during configuration
// GOOD: Enable all performance features
// gradle.properties:
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.vfs.watch=true
// GOOD: Use Java toolchains instead of sourceCompatibility
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
// GOOD: Use lazy task registration
tasks.register("myTask") {
doLast { ... }
}
// GOOD: Use Provider API for task inputs
abstract class MyTask : DefaultTask() {
@get:Input
abstract val message: Property<String>
@TaskAction
fun execute() {
println(message.get())
}
}
This section covers advanced topics for developers building custom Gradle plugins and tasks: proper input/output handling, extensions, lazy configuration with Providers API, and build caching.
// BAD: Eager task creation (always executed during configuration)
tasks.create("eagerTask") {
doLast {
println("Task executed")
}
}
// Problem: Task is configured immediately, slowing configuration phase
// GOOD: Lazy task registration (configured only when needed)
tasks.register("lazyTask") {
doLast {
println("Task executed")
}
}
// Benefit: Task configured only if needed (e.g., when explicitly run)
// GOOD: Simple task with doLast
tasks.register("hello") {
doLast {
println("Hello from task")
}
}
// GOOD: Multiple actions (executed in order)
tasks.register("multiAction") {
doFirst {
println("First action")
}
doLast {
println("Last action")
}
}
// GOOD: Named action (can be removed later if needed)
tasks.register("namedAction") {
val myAction = Action<Task> {
println("Named action")
}
doLast(myAction)
}
import org.gradle.api.DefaultTask
import org.gradle.api.file.*
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
// GOOD: Abstract task with typed properties
abstract class ProcessFilesTask : DefaultTask() {
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Input
@get:Optional
abstract val prefix: Property<String>
init {
// Set defaults
prefix.convention("processed-")
}
@TaskAction
fun process() {
val input = inputDir.get().asFile
val output = outputDir.get().asFile
output.mkdirs()
input.listFiles()?.forEach { file ->
val processed = output.resolve("${prefix.get()}${file.name}")
processed.writeText(file.readText().uppercase())
}
println("Processed ${input.listFiles()?.size ?: 0} files")
}
}
// Register task with configuration
tasks.register<ProcessFilesTask>("processFiles") {
inputDir = layout.projectDirectory.dir("src/data")
outputDir = layout.buildDirectory.dir("processed")
prefix = "PROCESSED-"
}
Critical for up-to-date checking and caching:
abstract class AdvancedTask : DefaultTask() {
// GOOD: Input file
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE) // Content-only sensitivity
abstract val inputFile: RegularFileProperty
// GOOD: Input files
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE) // Path matters
abstract val inputFiles: ConfigurableFileCollection
// GOOD: Input directory
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputDir: DirectoryProperty
// GOOD: Input property (string, boolean, etc.)
@get:Input
abstract val message: Property<String>
// GOOD: Optional input
@get:Input
@get:Optional
abstract val optionalFlag: Property<Boolean>
// GOOD: Output file
@get:OutputFile
abstract val outputFile: RegularFileProperty
// GOOD: Output directory
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
// GOOD: Internal property (not an input/output)
@get:Internal
abstract val internalState: Property<String>
@TaskAction
fun execute() {
// Task implementation
}
}
PathSensitivity options:
NONE - Only file content matters (not path or name)NAME_ONLY - File name mattersRELATIVE - Relative path matters (most common)ABSOLUTE - Absolute path matters (rare)// GOOD: Task depends on another task
tasks.register("taskA") {
doLast { println("Task A") }
}
tasks.register("taskB") {
dependsOn("taskA") // taskA runs before taskB
doLast { println("Task B") }
}
// GOOD: Multiple dependencies
tasks.register("taskC") {
dependsOn("taskA", "taskB")
doLast { println("Task C") }
}
// GOOD: Ordering without hard dependency
tasks.register("taskD") {
mustRunAfter("taskB") // If both run, D runs after B
doLast { println("Task D") }
}
tasks.register("taskE") {
shouldRunAfter("taskD") // Ordering hint (not enforced)
doLast { println("Task E") }
}
// GOOD: Finalization
tasks.register("taskF") {
doLast { println("Task F") }
}
tasks.register("cleanup") {
doLast { println("Cleanup") }
}
tasks.named("taskF") {
finalizedBy("cleanup") // cleanup always runs after taskF
}
abstract class TransformMarkdownTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val markdownFiles: ConfigurableFileCollection
@get:OutputDirectory
abstract val htmlOutputDir: DirectoryProperty
@get:Input
abstract val title: Property<String>
init {
title.convention("Documentation")
}
@TaskAction
fun transform() {
val outputDir = htmlOutputDir.get().asFile
outputDir.mkdirs()
markdownFiles.forEach { mdFile ->
val htmlFile = outputDir.resolve("${mdFile.nameWithoutExtension}.html")
val content = mdFile.readText()
htmlFile.writeText("""
<!DOCTYPE html>
<html>
<head><title>${title.get()}</title></head>
<body>
<pre>$content</pre>
</body>
</html>
""".trimIndent())
}
logger.lifecycle("Transformed ${markdownFiles.files.size} markdown files")
}
}
// Register and configure
tasks.register<TransformMarkdownTask>("transformMarkdown") {
markdownFiles.from(fileTree("docs") { include("**/*.md") })
htmlOutputDir = layout.buildDirectory.dir("html")
title = "My Project Documentation"
}
// GOOD: Configure task only when needed
tasks.named<JavaCompile>("compileJava") {
options.compilerArgs.add("-Xlint:unchecked")
}
// BAD: Getting task eagerly (forces configuration)
// val compileJava = tasks.getByName("compileJava") // Avoid this
// GOOD: Lazy task reference
val compileJavaTask = tasks.named("compileJava")
// GOOD: Configure all tasks of type
tasks.withType<Test>().configureEach {
useJUnitPlatform()
maxParallelForks = Runtime.getRuntime().availableProcessors()
}
Extensions provide a DSL for configuring plugins. They're essential for creating user-friendly custom plugins.
// Define extension (in buildSrc or custom plugin)
abstract class GreetingExtension {
abstract val message: Property<String>
abstract val times: Property<Int>
init {
// Set default values
message.convention("Hello")
times.convention(1)
}
}
// Register extension in plugin
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Create extension
val extension = project.extensions.create("greeting", GreetingExtension::class.java)
// Use extension to configure task
project.tasks.register("greet") {
doLast {
repeat(extension.times.get()) {
println(extension.message.get())
}
}
}
}
}
// Usage in build.gradle.kts
plugins {
id("greeting-plugin")
}
greeting {
message = "Hello, Gradle!"
times = 3
}
// BAD: Using plain variables instead of Property<T>
abstract class BadExtension {
var message: String = "Hello" // Not lazy, not compatible with config cache
var times: Int = 1 // Cannot be wired to providers
}
// Problem: Breaks configuration cache, not lazy, no provider wiring
// GOOD: Always use Property<T>
abstract class GoodExtension {
abstract val message: Property<String>
abstract val times: Property<Int>
}
// BAD: Eager evaluation in extension
class BadPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("bad", BadExtension::class.java)
// Evaluates immediately during configuration!
val msg = extension.message.get() // BAD: Too early
project.tasks.register("bad") {
doLast { println(msg) } // Value captured at configuration time
}
}
}
// GOOD: Lazy evaluation with providers
class GoodPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("good", GoodExtension::class.java)
project.tasks.register("good") {
doLast {
// Evaluated at execution time
println(extension.message.get())
}
}
}
}
// BAD: No default values
abstract class ExtensionWithoutDefaults {
abstract val required: Property<String>
// User MUST set this or build fails - poor UX
}
// GOOD: Provide sensible defaults
abstract class ExtensionWithDefaults {
abstract val optional: Property<String>
init {
optional.convention("sensible-default") // User can override if needed
}
}
// Nested extension for database configuration
abstract class DatabaseExtension {
abstract val host: Property<String>
abstract val port: Property<Int>
abstract val username: Property<String>
abstract val password: Property<String>
}
// Main extension
abstract class AppExtension(objects: ObjectFactory) {
// Simple properties
abstract val appName: Property<String>
abstract val version: Property<String>
// Nested object (always created)
val database: DatabaseExtension = objects.newInstance(DatabaseExtension::class.java)
// Configure nested object with DSL
fun database(action: Action<DatabaseExtension>) {
action.execute(database)
}
init {
appName.convention("MyApp")
version.convention("1.0.0")
database.port.convention(5432)
}
}
// Usage in build.gradle.kts
app {
appName = "CoolApp"
version = "2.0.0"
database {
host = "localhost"
port = 5432
username = "admin"
password = providers.gradleProperty("db.password").orElse("default")
}
}
For collections of similar configurations:
import org.gradle.api.NamedDomainObjectContainer
// Define a server configuration
abstract class ServerConfig(val name: String) {
abstract val host: Property<String>
abstract val port: Property<Int>
init {
port.convention(8080)
}
}
// Extension with container
abstract class DeploymentExtension(objects: ObjectFactory) {
// Container of servers
val servers: NamedDomainObjectContainer<ServerConfig> =
objects.domainObjectContainer(ServerConfig::class.java)
// DSL method for configuring servers
fun servers(action: Action<NamedDomainObjectContainer<ServerConfig>>) {
action.execute(servers)
}
}
// Usage in build.gradle.kts
deployment {
servers {
create("production") {
host = "prod.example.com"
port = 443
}
create("staging") {
host = "staging.example.com"
port = 8080
}
}
}
// Access servers in task
tasks.register("deployToProduction") {
doLast {
val prodServer = extensions.getByType<DeploymentExtension>()
.servers.getByName("production")
println("Deploying to ${prodServer.host.get()}:${prodServer.port.get()}")
}
}
abstract class WellDesignedExtension @Inject constructor(
private val objects: ObjectFactory,
private val providers: ProviderFactory
) {
// GOOD: Use Property<T> for mutable configuration
abstract val apiKey: Property<String>
// GOOD: Provide sensible defaults
abstract val timeout: Property<Int>
// GOOD: Use Provider for derived values
val apiUrl: Provider<String> = apiKey.map { key ->
"https://api.example.com?key=$key"
}
// GOOD: Validate in finalizer (not during configuration)
init {
timeout.convention(30)
// Validation happens when value is accessed
apiKey.finalizeValueOnRead()
}
// GOOD: Provide configuration methods with clear names
fun useDefaultCredentials() {
apiKey.set(providers.environmentVariable("API_KEY"))
}
fun useCustomCredentials(key: String) {
apiKey.set(key)
}
}
// BAD: Using plain variables (not lazy)
// class BadExtension {
// var apiKey: String = "" // Not lazy, no defaults, no validation
// }
abstract class PublishExtension {
abstract val version: Property<String>
abstract val repository: Property<String>
init {
version.convention("1.0.0")
repository.convention("https://repo.example.com")
}
}
class PublishPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("publish", PublishExtension::class.java)
project.tasks.register<PublishTask>("publish") {
// GOOD: Wire extension properties to task properties
version.set(extension.version)
repository.set(extension.repository)
}
}
}
abstract class PublishTask : DefaultTask() {
@get:Input
abstract val version: Property<String>
@get:Input
abstract val repository: Property<String>
@TaskAction
fun publish() {
println("Publishing version ${version.get()} to ${repository.get()}")
}
}
The Providers API enables lazy configuration, which is essential for configuration cache and fast builds.
// GOOD: Provider wraps a value that's computed lazily
val messageProvider: Provider<String> = providers.provider {
"Message computed at ${System.currentTimeMillis()}"
}
// Value is only computed when accessed
tasks.register("printMessage") {
doLast {
println(messageProvider.get()) // Computed here
}
}
// GOOD: Provider from environment variable
val apiKeyProvider: Provider<String> = providers.environmentVariable("API_KEY")
// GOOD: Provider from system property
val debugProvider: Provider<String> = providers.systemProperty("debug")
// GOOD: Provider from gradle property
val versionProvider: Provider<String> = providers.gradleProperty("app.version")
abstract class ConfigurableTask : DefaultTask() {
// GOOD: Property<T> for task inputs (can be set and connected)
@get:Input
abstract val message: Property<String>
@get:Input
abstract val count: Property<Int>
init {
// Set default values
message.convention("Default message")
count.convention(1)
}
@TaskAction
fun execute() {
repeat(count.get()) {
println(message.get())
}
}
}
// Configure task
tasks.register<ConfigurableTask>("configurable") {
message.set("Hello from property")
count.set(5)
}
// GOOD: map - transform provider value
val version: Provider<String> = providers.gradleProperty("version")
val fullVersion: Provider<String> = version.map { v ->
"v$v-${System.currentTimeMillis()}"
}
// GOOD: flatMap - chain providers
val baseUrl: Provider<String> = providers.gradleProperty("baseUrl")
val apiUrl: Provider<String> = baseUrl.flatMap { base ->
providers.provider { "$base/api/v1" }
}
// GOOD: orElse - provide fallback
val timeout: Provider<Int> = providers.gradleProperty("timeout")
.map { it.toInt() }
.orElse(30)
// GOOD: zip - combine two providers
val host: Provider<String> = providers.gradleProperty("host")
val port: Provider<Int> = providers.gradleProperty("port").map { it.toInt() }
val endpoint: Provider<String> = host.zip(port) { h, p ->
"$h:$p"
}
abstract class SourceTask : DefaultTask() {
@get:Input
abstract val sourceMessage: Property<String>
init {
sourceMessage.convention("Source data")
}
@TaskAction
fun execute() {
println("Source: ${sourceMessage.get()}")
}
}
abstract class TargetTask : DefaultTask() {
@get:Input
abstract val targetMessage: Property<String>
@TaskAction
fun execute() {
println("Target: ${targetMessage.get()}")
}
}
// GOOD: Connect provider from one task to another
val sourceTask = tasks.register<SourceTask>("source")
tasks.register<TargetTask>("target") {
// Wire output from source to input of target
targetMessage.set(sourceTask.flatMap { it.sourceMessage })
}
abstract class FileTask : DefaultTask() {
// GOOD: Use RegularFileProperty for files
@get:OutputFile
abstract val outputFile: RegularFileProperty
// GOOD: Use DirectoryProperty for directories
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun execute() {
// Get file and directory
val file = outputFile.get().asFile
val dir = outputDir.get().asFile
file.writeText("Output content")
println("Wrote to ${file.absolutePath}")
}
}
tasks.register<FileTask>("fileTask") {
// GOOD: Use layout.buildDirectory for build outputs
outputFile.set(layout.buildDirectory.file("output.txt"))
outputDir.set(layout.buildDirectory.dir("outputs"))
}
// GOOD: Map file providers
tasks.register("processFile") {
val inputProvider: Provider<RegularFile> = layout.buildDirectory.file("input.txt")
val outputProvider: Provider<RegularFile> = inputProvider.map { input ->
layout.buildDirectory.file("processed-${input.asFile.name}").get()
}
}
abstract class CollectionTask : DefaultTask() {
// GOOD: ListProperty for list of values
@get:Input
abstract val items: ListProperty<String>
// GOOD: SetProperty for unique values
@get:Input
abstract val tags: SetProperty<String>
// GOOD: MapProperty for key-value pairs
@get:Input
abstract val config: MapProperty<String, String>
@TaskAction
fun execute() {
println("Items: ${items.get()}")
println("Tags: ${tags.get()}")
println("Config: ${config.get()}")
}
}
tasks.register<CollectionTask>("collections") {
// Set collections
items.set(listOf("a", "b", "c"))
items.add("d") // Add single item
tags.set(setOf("gradle", "kotlin"))
tags.add("build")
config.set(mapOf("env" to "prod", "region" to "us"))
config.put("version", "1.0")
}
// BAD: Eager evaluation during configuration
// val version = project.findProperty("version") as String // Evaluated immediately
// GOOD: Lazy evaluation with provider
val version: Provider<String> = providers.gradleProperty("version")
// BAD: Calling .get() during configuration phase
// tasks.register("bad") {
// val msg = messageProvider.get() // Forces evaluation too early
// doLast { println(msg) }
// }
// GOOD: Call .get() only in task action
tasks.register("good") {
doLast {
println(messageProvider.get()) // Evaluated at execution time
}
}
// BAD: Using plain variables in task configuration
// var myVar = "value"
// tasks.register("bad") {
// doLast { println(myVar) } // Captures current value, not lazy
// }
// GOOD: Using properties
abstract class GoodTask : DefaultTask() {
@get:Input
abstract val myProperty: Property<String>
@TaskAction
fun execute() {
println(myProperty.get()) // Lazy, cached, compatible with config cache
}
}
// GOOD: Use providers for external inputs
val externalConfig: Provider<String> = providers.fileContents(
layout.projectDirectory.file("config.txt")
).asText
// GOOD: Finalize values to catch configuration errors early
val criticalValue: Property<String> = objects.property(String::class.java)
criticalValue.finalizeValueOnRead() // Value can't change after first read
// GOOD: Use conventions for defaults
val timeout: Property<Int> = objects.property(Int::class.java)
timeout.convention(30) // Default value if not set
// GOOD: Validate provider values
val port: Provider<Int> = providers.gradleProperty("port")
.map { it.toInt() }
.map { p ->
require(p in 1..65535) { "Port must be between 1 and 65535" }
p
}
// GOOD: Use providers.provider for expensive computations
val expensiveValue: Provider<String> = providers.provider {
// This expensive computation only runs when needed
Thread.sleep(100)
"Computed value"
}
Caching is essential for fast Gradle builds. There are two types of caching: build cache (task outputs) and configuration cache (build configuration).
The build cache stores task outputs and reuses them when inputs haven't changed.
Enable build cache (gradle.properties):
org.gradle.caching=true
Or via command line:
./gradlew build --build-cache
How it works:
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
// GOOD: Cache-compatible task with proper annotations
@CacheableTask // Mark class as cacheable (must import org.gradle.api.tasks.CacheableTask)
abstract class CacheableProcessTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputFiles: ConfigurableFileCollection
@get:Input
abstract val processMode: Property<String>
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun process() {
val output = outputDir.get().asFile
output.mkdirs()
inputFiles.forEach { file ->
val processed = output.resolve(file.name)
when (processMode.get()) {
"uppercase" -> processed.writeText(file.readText().uppercase())
"lowercase" -> processed.writeText(file.readText().lowercase())
}
}
}
}
// BAD: Task that's not cacheable (no @CacheableTask, uses external state)
abstract class BadTask : DefaultTask() {
@TaskAction
fun execute() {
val timestamp = System.currentTimeMillis() // Non-deterministic!
File("output.txt").writeText("Built at $timestamp")
}
}
✅ GOOD - Cacheable:
❌ BAD - Not Cacheable:
// GOOD: Configure tasks to be cacheable
tasks.withType<Test>().configureEach {
outputs.cacheIf { true } // Enable caching for tests
}
// GOOD: Normalize file paths for cache portability
normalization {
runtimeClasspath {
ignore("META-INF/MANIFEST.MF") // Ignore non-functional differences
}
}
Configuration cache stores the configured task graph, eliminating configuration phase on subsequent builds.
Enable configuration cache (gradle.properties):
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn # Or 'fail'
Or via command line:
./gradlew build --configuration-cache
Benefits:
// GOOD: Configuration cache compatible
tasks.register("compatible") {
val message: Provider<String> = providers.gradleProperty("message")
doLast {
println(message.get()) // Lazy evaluation
}
}
// BAD: Not configuration cache compatible
// tasks.register("incompatible") {
// val message = project.findProperty("message") // Eager evaluation
// doLast {
// println(message) // Captures project state at configuration time
// }
// }
// GOOD: Use build services for shared state
interface MyBuildService : BuildService<BuildServiceParameters.None> {
fun performWork() {
println("Build service performing work")
}
}
abstract class SharedStateTask : DefaultTask() {
@get:Internal // Build services are not inputs
abstract val myService: Property<MyBuildService>
@TaskAction
fun execute() {
myService.get().performWork()
}
}
// Register the build service
val myServiceProvider = gradle.sharedServices.registerIfAbsent("myService", MyBuildService::class) {
// Configure service parameters here if needed
}
// Wire service to task
tasks.register<SharedStateTask>("taskWithService") {
myService.set(myServiceProvider)
}
// BAD: Using static/global state instead of build services
object BadSharedState {
var counter = 0 // Mutable global state - not serializable!
}
// Problem: Breaks configuration cache, not thread-safe, not isolated
// BAD: Trying to share data via files without proper task dependencies
tasks.register("badProducer") {
doLast {
File("shared.txt").writeText("data") // No output annotation!
}
}
tasks.register("badConsumer") {
doLast {
val data = File("shared.txt").readText() // No input annotation!
println(data)
}
}
// Problem: No dependency declared, may run in wrong order or break caching
// GOOD: Use task outputs/inputs or build services for shared state
// PROBLEM: Accessing project at execution time
// tasks.register("bad") {
// doLast {
// println(project.name) // Configuration cache error!
// }
// }
// SOLUTION: Capture value during configuration
tasks.register("good") {
val projectName = project.name // Captured during configuration
doLast {
println(projectName) // OK - uses captured value
}
}
// PROBLEM: Using mutable shared state
// val sharedList = mutableListOf<String>()
// tasks.register("bad") {
// doLast {
// sharedList.add("item") // Not serializable!
// }
// }
// SOLUTION: Use build services or task outputs
abstract class GoodTask : DefaultTask() {
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun execute() {
outputFile.get().asFile.appendText("item\n")
}
}
# Check what's not cacheable
./gradlew build --build-cache --info | grep "Caching disabled"
# Explain why task wasn't cached
./gradlew help --task processFiles
# Clear build cache
rm -rf ~/.gradle/caches/build-cache-*
rm -rf .gradle/build-cache
# Check configuration cache problems
./gradlew build --configuration-cache --configuration-cache-problems=warn
# Rerun without cache to compare
./gradlew clean build --no-build-cache --no-configuration-cache
./gradlew clean build --build-cache --configuration-cache
// settings.gradle.kts
buildCache {
local {
isEnabled = true
}
remote<HttpBuildCache> {
url = uri("https://cache.example.com/")
isPush = providers.environmentVariable("CI")
.map { it == "true" }
.getOrElse(false)
credentials {
username = providers.environmentVariable("CACHE_USER").orNull
password = providers.environmentVariable("CACHE_PASSWORD").orNull
}
}
}
// Benefits:
// - Share cache across CI and developers
// - Dramatically faster CI builds
// - Consistent build performance
// GOOD: Use relative path sensitivity when possible
abstract class OptimizedTask : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE) // Better cache hits
abstract val sources: ConfigurableFileCollection
}
// GOOD: Exclude non-functional files
normalization {
runtimeClasspath {
ignore("**/*.txt") // If .txt files don't affect behavior
ignore("META-INF/MANIFEST.MF")
}
}
// GOOD: Use file collections instead of file trees for better caching
val sources: ConfigurableFileCollection = objects.fileCollection()
sources.from(fileTree("src") { include("**/*.java") })
// GOOD: Split large tasks into smaller cacheable units
tasks.register("compileAll") {
dependsOn("compileModule1", "compileModule2", "compileModule3")
}
// Each module compiled separately = better cache granularity
# Build scan (best way to analyze caching)
./gradlew build --scan
# The build scan shows:
# - Cache hit rate
# - Which tasks were cached
# - Why tasks were not cached
# - Performance timeline
# Enable with:
# plugins {
# id("com.gradle.develocity") version "3.16"
# }
#
# develocity {
# buildScan {
# publishing.onlyIf { true }
# }
# }
Custom plugins encapsulate build logic for reuse across projects or modules.
// buildSrc/src/main/kotlin/GreetingPlugin.kt
import org.gradle.api.Plugin
import org.gradle.api.Project
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Create extension for configuration
val extension = project.extensions.create(
"greeting",
GreetingExtension::class.java
)
// Register task that uses extension
project.tasks.register("greet") {
group = "custom"
description = "Prints a greeting message"
doLast {
repeat(extension.times.get()) {
println(extension.message.get())
}
}
}
}
}
// Extension for configuration
abstract class GreetingExtension {
abstract val message: Property<String>
abstract val times: Property<Int>
init {
message.convention("Hello from plugin")
times.convention(1)
}
}
// buildSrc/src/main/resources/META-INF/gradle-plugins/greeting.properties
implementation-class=GreetingPlugin
// Usage in build.gradle.kts:
// plugins {
// id("greeting")
// }
//
// greeting {
// message = "Hello, World!"
// times = 3
// }
Easier approach using Kotlin DSL directly:
// buildSrc/src/main/kotlin/java-library-conventions.gradle.kts
plugins {
`java-library`
`maven-publish`
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
withSourcesJar()
withJavadocJar()
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}
// Usage in build.gradle.kts:
// plugins {
// id("java-library-conventions")
// }
// buildSrc/src/main/kotlin/DocumentationPlugin.kt
import org.gradle.api.*
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
class DocumentationPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Register extension
val extension = project.extensions.create(
"documentation",
DocumentationExtension::class.java
)
// Configure defaults from extension
extension.sourceDir.convention(
project.layout.projectDirectory.dir("docs")
)
extension.outputDir.convention(
project.layout.buildDirectory.dir("docs")
)
// Register task
project.tasks.register<GenerateDocsTask>("generateDocs") {
sourceDir.set(extension.sourceDir)
outputDir.set(extension.outputDir)
format.set(extension.format)
group = "documentation"
description = "Generates documentation"
}
// Hook into build lifecycle
project.tasks.named("build") {
dependsOn("generateDocs")
}
}
}
abstract class DocumentationExtension {
abstract val sourceDir: DirectoryProperty
abstract val outputDir: DirectoryProperty
abstract val format: Property<String>
init {
format.convention("html")
}
}
abstract class GenerateDocsTask : DefaultTask() {
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val sourceDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Input
abstract val format: Property<String>
@TaskAction
fun generate() {
val output = outputDir.get().asFile
output.mkdirs()
sourceDir.get().asFileTree.forEach { file ->
val outputFile = output.resolve("${file.nameWithoutExtension}.${format.get()}")
outputFile.writeText("Generated from ${file.name}")
}
logger.lifecycle("Generated documentation in ${output.absolutePath}")
}
}
For shared state across tasks:
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
abstract class MetricsService : BuildService<BuildServiceParameters.None> {
private val metrics = mutableMapOf<String, Long>()
fun record(metric: String, value: Long) {
metrics[metric] = value
}
fun report() {
println("Build Metrics:")
metrics.forEach { (key, value) ->
println(" $key: $value")
}
}
}
class MetricsPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Register build service
val metricsService = project.gradle.sharedServices.registerIfAbsent(
"metrics",
MetricsService::class.java
) {}
// Use service in tasks
project.tasks.register<MetricsTask>("recordMetrics") {
this.metricsService.set(metricsService)
}
// Report at end of build
project.gradle.buildFinished {
metricsService.get().report()
}
}
}
abstract class MetricsTask : DefaultTask() {
@get:ServiceReference("metrics")
abstract val metricsService: Property<MetricsService>
@TaskAction
fun record() {
metricsService.get().record("task_count", 42)
}
}
// buildSrc/src/test/kotlin/GreetingPluginTest.kt
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class GreetingPluginTest {
@Test
fun `plugin registers greet task`() {
val project = ProjectBuilder.builder().build()
project.pluginManager.apply("greeting")
val task = project.tasks.findByName("greet")
assertNotNull(task)
}
@Test
fun `extension has default values`() {
val project = ProjectBuilder.builder().build()
project.pluginManager.apply("greeting")
val extension = project.extensions.getByType(GreetingExtension::class.java)
assertEquals("Hello from plugin", extension.message.get())
assertEquals(1, extension.times.get())
}
@Test
fun `can configure extension`() {
val project = ProjectBuilder.builder().build()
project.pluginManager.apply("greeting")
val extension = project.extensions.getByType(GreetingExtension::class.java)
extension.message.set("Custom message")
extension.times.set(5)
assertEquals("Custom message", extension.message.get())
assertEquals(5, extension.times.get())
}
}
// buildSrc/build.gradle.kts (for publishing to plugin portal)
plugins {
`kotlin-dsl`
`maven-publish`
id("com.gradle.plugin-publish") version "1.2.1"
}
group = "com.example"
version = "1.0.0"
gradlePlugin {
website = "https://github.com/example/plugin"
vcsUrl = "https://github.com/example/plugin"
plugins {
create("greetingPlugin") {
id = "com.example.greeting"
displayName = "Greeting Plugin"
description = "A plugin that greets users"
tags = listOf("greeting", "example")
implementationClass = "com.example.GreetingPlugin"
}
}
}
publishing {
repositories {
maven {
name = "Internal"
url = uri("https://repo.company.com/maven")
}
}
}
// Publish with: ./gradlew publishPlugins
class WellDesignedPlugin : Plugin<Project> {
override fun apply(project: Project) {
// GOOD: Validate environment
require(project.hasProperty("requiredProp")) {
"Plugin requires 'requiredProp' property"
}
// GOOD: Use lazy registration
val extension = project.extensions.create("wellDesigned", Extension::class.java)
// GOOD: Register tasks lazily
project.tasks.register("myTask") {
// Configure with extension
}
// GOOD: Apply other plugins if needed
project.pluginManager.apply("java")
// GOOD: Configure other plugins
project.plugins.withType<JavaPlugin> {
project.java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
}
// GOOD: Hook into lifecycle cleanly
project.afterEvaluate {
// Configuration that needs evaluated project
}
}
}
// BAD: Applying plugins eagerly
// project.apply(plugin = "java") // Use pluginManager.apply()
// BAD: Configuring in constructor
// class BadPlugin : Plugin<Project> {
// init {
// // Plugin not yet applied!
// }
// }
There are multiple strategies for sharing build logic across projects and modules.
Best for: Single repository, convention plugins, shared code within one project.
project/
├── buildSrc/
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ └── src/
│ └── main/kotlin/
│ ├── java-conventions.gradle.kts
│ ├── kotlin-conventions.gradle.kts
│ └── MyCustomPlugin.kt
├── app/
│ └── build.gradle.kts
└── lib/
└── build.gradle.kts
buildSrc/build.gradle.kts:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
// Add dependencies needed by your plugins
implementation("com.github.johnrengelman:shadow:8.1.1")
}
buildSrc/settings.gradle.kts:
rootProject.name = "buildSrc"
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
mavenCentral()
gradlePluginPortal()
}
}
buildSrc/src/main/kotlin/java-conventions.gradle.kts:
plugins {
java
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
tasks.test {
useJUnitPlatform()
}
Usage in app/build.gradle.kts:
plugins {
id("java-conventions") // Automatically available
application
}
Pros:
Cons:
Best for: Multi-repo setups, versioned build logic, independent releases.
company-builds/
├── my-project/
│ ├── settings.gradle.kts (includes build-logic)
│ ├── app/
│ └── lib/
└── build-logic/
├── settings.gradle.kts
├── build.gradle.kts
└── src/
└── main/kotlin/
└── conventions/
├── java-conventions.gradle.kts
└── kotlin-conventions.gradle.kts
my-project/settings.gradle.kts:
rootProject.name = "my-project"
// Include build-logic
includeBuild("../build-logic")
include("app")
include("lib")
build-logic/settings.gradle.kts:
rootProject.name = "build-logic"
dependencyResolutionManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
build-logic/build.gradle.kts:
plugins {
`kotlin-dsl`
}
group = "com.example.build"
version = "1.0.0"
dependencies {
implementation("com.github.johnrengelman:shadow:8.1.1")
}
gradlePlugin {
plugins {
register("javaConventions") {
id = "com.example.java-conventions"
implementationClass = "conventions.JavaConventionsPlugin"
}
}
}
Usage in my-project/app/build.gradle.kts:
plugins {
id("com.example.java-conventions")
}
Pros:
Cons:
Best for: Many projects, organization-wide standards, versioned releases.
Plugin Project Structure:
gradle-plugins/
├── settings.gradle.kts
├── build.gradle.kts
└── src/
└── main/
├── kotlin/
│ └── com/example/plugins/
│ └── JavaConventionsPlugin.kt
└── resources/
└── META-INF/gradle-plugins/
└── com.example.java-conventions.properties
build.gradle.kts:
plugins {
`kotlin-dsl`
`maven-publish`
}
group = "com.example.gradle"
version = "1.0.0"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11) // Wide compatibility
}
}
publishing {
repositories {
maven {
name = "Company"
url = uri("https://repo.company.com/maven")
credentials {
username = System.getenv("REPO_USER")
password = System.getenv("REPO_PASSWORD")
}
}
}
publications {
create<MavenPublication>("plugin") {
from(components["java"])
}
}
}
Consumer settings.gradle.kts:
pluginManagement {
repositories {
maven {
url = uri("https://repo.company.com/maven")
}
gradlePluginPortal()
}
}
Consumer build.gradle.kts:
plugins {
id("com.example.java-conventions") version "1.0.0"
}
Pros:
Cons:
Best for: Multiple independent projects that need to work together.
workspace/
├── project-a/
│ ├── settings.gradle.kts
│ └── build.gradle.kts
├── project-b/
│ ├── settings.gradle.kts
│ └── build.gradle.kts
└── shared-library/
├── settings.gradle.kts
└── build.gradle.kts
project-a/settings.gradle.kts:
rootProject.name = "project-a"
// Include other project as composite build
includeBuild("../shared-library")
project-a/build.gradle.kts:
dependencies {
// Depend on shared library
implementation("com.example:shared-library:1.0.0")
// Gradle substitutes with composite build automatically
}
Pros:
Cons:
| Scenario | Recommended Strategy | |----------|---------------------| | Single repo, simple conventions | buildSrc | | Multi-repo, same org | Included builds | | Organization-wide standards | Published plugins | | Multiple independent projects | Composite builds | | Experimenting with new patterns | buildSrc → Included builds |
Important: Precompiled script plugins in buildSrc automatically get plugin IDs based on their file path. A file at buildSrc/src/main/kotlin/conventions/java-base.gradle.kts becomes plugin id("conventions.java-base").
// Pattern 1: Pure configuration plugin
// Location: buildSrc/src/main/kotlin/conventions/java-base.gradle.kts
// Plugin ID: conventions.java-base (auto-generated from file path)
plugins {
java
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(21)
}
// Pattern 2: Conditional configuration
// Location: buildSrc/src/main/kotlin/conventions/java-app.gradle.kts
// Plugin ID: conventions.java-app
plugins {
id("conventions.java-base") // References Pattern 1 by its auto-generated ID
application
}
if (project.hasProperty("enableJacoco")) {
apply(plugin = "jacoco")
}
// Pattern 3: Composed plugins
// conventions/java-library.gradle.kts
plugins {
id("conventions.java-base")
`java-library`
`maven-publish`
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}
// buildSrc/src/main/resources/checkstyle.xml
// buildSrc/src/main/kotlin/conventions.gradle.kts
plugins {
checkstyle
}
checkstyle {
configFile = file("${project.rootDir}/buildSrc/src/main/resources/checkstyle.xml")
toolVersion = "10.12.5"
}
// All modules get consistent checkstyle configuration
// build-logic/build.gradle.kts
version = "1.2.0" // Increment when making changes
// Consumers can pin versions
// settings.gradle.kts
pluginManagement {
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.example.conventions") {
useVersion("1.2.0")
}
}
}
}
This section helps developers migrate existing Groovy DSL build scripts to Kotlin DSL.
This section provides side-by-side comparisons of common syntax patterns.
| Feature | Groovy DSL | Kotlin DSL |
|---------|------------|------------|
| File name | build.gradle | build.gradle.kts |
| Settings file | settings.gradle | settings.gradle.kts |
| Assignment | version = '1.0' | version = "1.0" |
| String literals | 'single' or "double" | "double" only |
| String interpolation | "Version $version" | "Version $version" |
| Method calls | implementation 'lib' | implementation("lib") |
| Configuration blocks | java { ... } | java { ... } |
| Task configuration | task myTask { ... } | tasks.register("myTask") { ... } |
Groovy:
version = '1.0.0'
group = 'com.example'
ext.customProp = 'value'
ext {
anotherProp = 'value'
}
Kotlin:
version = "1.0.0"
group = "com.example"
extra["customProp"] = "value"
// Or with type-safe accessor
val customProp by extra("value")
Groovy:
// Both work in Groovy
implementation 'com.google.guava:guava:33.0.0-jre'
implementation "com.google.guava:guava:33.0.0-jre"
// String interpolation
def myVersion = '1.0'
println "Version: $myVersion"
Kotlin:
// Only double quotes work in Kotlin
implementation("com.google.guava:guava:33.0.0-jre")
// String interpolation (same as Groovy)
val myVersion = "1.0"
println("Version: $myVersion")
Groovy (implicit parentheses):
// Groovy allows omitting parentheses
implementation 'com.google.guava:guava:33.0.0-jre'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
// Configuration blocks
repositories {
mavenCentral()
}
Kotlin (explicit parentheses):
// Kotlin requires parentheses for method calls
implementation("com.google.guava:guava:33.0.0-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
// Configuration blocks (same)
repositories {
mavenCentral()
}
Groovy:
// Groovy uses property syntax for getters/setters
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
tasks.test {
maxParallelForks = 4
}
Kotlin:
// Kotlin also uses property syntax
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
tasks.named<Test>("test") {
maxParallelForks = 4
}
Groovy:
// List
def myList = ['item1', 'item2', 'item3']
// Map
def myMap = [key1: 'value1', key2: 'value2']
Kotlin:
// List
val myList = listOf("item1", "item2", "item3")
// Map
val myMap = mapOf("key1" to "value1", "key2" to "value2")
Groovy (implicit delegate):
tasks.create('myTask') {
// 'doLast' resolves through task delegate
doLast {
println 'Task executed'
}
}
Kotlin (explicit receiver):
tasks.register("myTask") {
// 'this' refers to the task
doLast {
println("Task executed")
}
}
Groovy (old style):
apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'com.github.johnrengelman.shadow'
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'com.github.johnrengelman:shadow:8.1.1'
}
}
Kotlin (modern style):
plugins {
java
application
id("com.github.johnrengelman.shadow") version "8.1.1"
}
// No buildscript block needed for plugins from Gradle Plugin Portal
Groovy:
apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'application'
apply plugin: 'groovy'
Kotlin:
plugins {
java
`java-library` // Note backticks for kebab-case
application
groovy
}
Groovy:
apply plugin: 'org.jetbrains.kotlin.jvm'
buildscript {
dependencies {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0'
}
}
Kotlin:
plugins {
kotlin("jvm") version "2.0.0"
// Short form for Kotlin plugins
// Alternative: id("org.jetbrains.kotlin.jvm") version "2.0.0"
}
Groovy:
if (project.hasProperty('enableKotlin')) {
apply plugin: 'org.jetbrains.kotlin.jvm'
}
Kotlin:
plugins {
java
if (project.hasProperty("enableKotlin")) {
kotlin("jvm") version "2.0.0"
}
}
// Alternative: Apply outside plugins block
if (project.findProperty("enableKotlin") == "true") {
apply(plugin = "org.jetbrains.kotlin.jvm")
}
Groovy:
plugins {
id 'org.springframework.boot' version '3.2.0' apply false
}
Kotlin:
plugins {
id("org.springframework.boot") version "3.2.0" apply false
}
Groovy:
dependencies {
implementation 'com.google.guava:guava:33.0.0-jre'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
compileOnly 'org.projectlombok:lombok:1.18.30'
runtimeOnly 'com.h2database:h2:2.2.224'
}
Kotlin:
dependencies {
implementation("com.google.guava:guava:33.0.0-jre")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
compileOnly("org.projectlombok:lombok:1.18.30")
runtimeOnly("com.h2database:h2:2.2.224")
}
Groovy:
dependencies {
implementation group: 'com.google.guava', name: 'guava', version: '33.0.0-jre'
implementation([group: 'com.google.guava', name: 'guava', version: '33.0.0-jre'])
}
Kotlin:
dependencies {
implementation(group = "com.google.guava", name = "guava", version = "33.0.0-jre")
// Or stick with string notation (more common)
implementation("com.google.guava:guava:33.0.0-jre")
}
Groovy:
dependencies {
implementation('com.example:library:1.0') {
exclude group: 'commons-logging', module: 'commons-logging'
}
}
Kotlin:
dependencies {
implementation("com.example:library:1.0") {
exclude(group = "commons-logging", module = "commons-logging")
}
}
Groovy:
dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:3.2.0')
implementation 'org.springframework.boot:spring-boot-starter-web'
}
Kotlin:
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.0"))
implementation("org.springframework.boot:spring-boot-starter-web")
}
Groovy:
dependencies {
implementation project(':common')
implementation project(path: ':lib', configuration: 'testFixtures')
}
Kotlin:
dependencies {
implementation(project(":common"))
implementation(project(path = ":lib", configuration = "testFixtures"))
// With type-safe accessors (requires TYPESAFE_PROJECT_ACCESSORS)
implementation(projects.common)
}
Groovy:
dependencies {
implementation files('libs/custom.jar')
implementation fileTree(dir: 'libs', include: '*.jar')
}
Kotlin:
dependencies {
implementation(files("libs/custom.jar"))
implementation(fileTree("libs") { include("*.jar") })
}
Groovy:
configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}
dependencies {
integrationTestImplementation 'org.testcontainers:junit-jupiter:1.19.3'
}
Kotlin:
val integrationTestImplementation by configurations.creating {
extendsFrom(configurations.testImplementation.get())
}
val integrationTestRuntimeOnly by configurations.creating {
extendsFrom(configurations.testRuntimeOnly.get())
}
dependencies {
integrationTestImplementation("org.testcontainers:junit-jupiter:1.19.3")
}
Groovy:
allprojects {
group = 'com.example'
version = '1.0.0'
repositories {
mavenCentral()
}
}
subprojects {
apply plugin: 'java'
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
}
Kotlin:
allprojects {
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
}
subprojects {
apply(plugin = "java")
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
}
Groovy:
buildscript {
repositories {
gradlePluginPortal()
mavenCentral()
}
dependencies {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0'
}
}
Kotlin (old way):
buildscript {
repositories {
gradlePluginPortal()
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0")
}
}
Kotlin (modern way - use plugins block instead):
plugins {
kotlin("jvm") version "2.0.0"
}
// No buildscript needed!
Groovy:
task myTask {
doLast {
println 'Task executed'
}
}
tasks.withType(Test) {
useJUnitPlatform()
}
tasks.named('build') {
dependsOn 'myTask'
}
Kotlin:
tasks.register("myTask") {
doLast {
println("Task executed")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.named("build") {
dependsOn("myTask")
}
Groovy:
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
withSourcesJar()
withJavadocJar()
}
compileJava {
options.encoding = 'UTF-8'
}
Kotlin:
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
withSourcesJar()
withJavadocJar()
}
tasks.named<JavaCompile>("compileJava") {
options.encoding = "UTF-8"
}
Groovy:
publishing {
publications {
maven(MavenPublication) {
from components.java
groupId = 'com.example'
artifactId = 'my-library'
version = '1.0.0'
}
}
repositories {
maven {
url = 'https://repo.example.com/maven'
credentials {
username = project.findProperty('repoUser')
password = project.findProperty('repoPassword')
}
}
}
}
Kotlin:
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
groupId = "com.example"
artifactId = "my-library"
version = "1.0.0"
}
}
repositories {
maven {
url = uri("https://repo.example.com/maven")
credentials {
username = providers.gradleProperty("repoUser").orNull
password = providers.gradleProperty("repoPassword").orNull
}
}
}
}
Groovy:
test {
useJUnitPlatform()
testLogging {
events 'passed', 'skipped', 'failed'
exceptionFormat 'full'
}
maxParallelForks = Runtime.runtime.availableProcessors()
}
Kotlin:
tasks.named<Test>("test") {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
maxParallelForks = Runtime.getRuntime().availableProcessors()
}
Groovy:
ext {
springBootVersion = '3.2.0'
junitVersion = '5.10.2'
}
ext.kotlinVersion = '2.0.0'
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
}
Kotlin:
// Option 1: Using extra properties
extra["springBootVersion"] = "3.2.0"
extra["junitVersion"] = "5.10.2"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:${extra["springBootVersion"]}")
testImplementation("org.junit.jupiter:junit-jupiter:${extra["junitVersion"]}")
}
// Option 2: Type-safe delegates (recommended)
val springBootVersion by extra("3.2.0")
val junitVersion by extra("5.10.2")
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion")
}
// Option 3: Regular Kotlin variables (best for build script only)
val springBootVersion = "3.2.0"
val junitVersion = "5.10.2"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion")
}
Problem:
// ERROR: Single quotes don't work in Kotlin
implementation('com.google.guava:guava:33.0.0-jre') // Compilation error!
Solution:
// Use double quotes in Kotlin
implementation("com.google.guava:guava:33.0.0-jre") // Correct
Why: Kotlin only supports double quotes for strings. Single quotes are for Char type.
Problem:
// ERROR: Missing parentheses
implementation "com.google.guava:guava:33.0.0-jre" // Compilation error!
Solution:
// Always use parentheses for method calls
implementation("com.google.guava:guava:33.0.0-jre") // Correct
Why: Kotlin requires explicit parentheses for method calls (no implicit syntax like Groovy).
Problem:
plugins {
// ERROR: This doesn't work in Kotlin DSL
kotlin("jvm") version 2.0.0 // Compilation error - version is not a string!
}
Solution:
plugins {
// Correct: Version must be a string literal
kotlin("jvm") version "2.0.0" // Correct
// Alternative explicit form
id("org.jetbrains.kotlin.jvm") version "2.0.0" // Also correct
}
Why: The version infix function expects a string parameter, not an expression.
Problem:
// Groovy way (not type-safe)
tasks.getByName("test") {
useJUnitPlatform() // No type information!
}
Solution:
// Kotlin way (type-safe)
tasks.named<Test>("test") {
useJUnitPlatform() // Type-safe! 'this' is Test
}
// Or using getByName with cast
tasks.getByName<Test>("test") {
useJUnitPlatform()
}
Why: Kotlin DSL encourages type-safe accessors for better IDE support and compile-time checks.
Problem:
plugins {
// ERROR: Hyphens in plugin names need backticks
java-library // Compilation error!
}
Solution:
plugins {
// Use backticks for kebab-case names
`java-library` // Correct
}
Why: Hyphens aren't valid in Kotlin identifiers, so backticks escape them.
Problem:
// Confusing when to use = vs method call
task {
description = "My task" // Property assignment
group("custom") // Method call?
}
Solution:
tasks.register("myTask") {
description = "My task" // Property assignment (setter)
group = "custom" // Also property assignment!
// Both work, but property syntax is more common in Kotlin
doLast {
println("Task executed")
}
}
Why: Kotlin has property syntax for getters/setters. Use = for properties, () for methods.
Problem:
// Groovy style (not type-safe)
ext.myVersion = "1.0"
println(ext.myVersion) // Error in Kotlin!
Solution:
// Option 1: Map-style access
extra["myVersion"] = "1.0"
println(extra["myVersion"])
// Option 2: Type-safe delegate (recommended)
val myVersion by extra("1.0")
println(myVersion)
// Option 3: Regular Kotlin variable (simplest)
val myVersion = "1.0"
println(myVersion)
Why: Kotlin DSL uses extra property with map-style or delegate access for type safety.
Problem:
// Ambiguous delegate in nested blocks
repositories {
maven {
// Is 'url' from repository or project?
url = uri("https://example.com/maven") // Unclear!
}
}
Solution:
repositories {
maven {
// Explicitly use 'this' if ambiguous
this.url = uri("https://example.com/maven")
// Or use the receiver parameter name
url = uri("https://example.com/maven") // Usually clear from context
}
}
Why: Kotlin DSL sometimes requires explicit receiver (this) to disambiguate nested scopes.
Problem:
// Variable not interpolated correctly
val myVersion = "1.0"
dependencies {
implementation("com.example:lib:$myVersion") // OK
implementation("com.example:lib:${project.version}") // project not in scope!
}
Solution:
val myVersion = "1.0"
val projectVersion = project.version // Capture outside if needed
dependencies {
implementation("com.example:lib:$myVersion") // OK
implementation("com.example:lib:$projectVersion") // OK
}
Why: Be aware of variable scope in configuration blocks. Capture values early if needed.
Problem:
// Groovy's dynamic properties don't work
project.myCustomProperty = "value" // Error in Kotlin!
println(project.myCustomProperty) // Error!
Solution:
// Use extra properties
extra["myCustomProperty"] = "value"
println(extra["myCustomProperty"])
// Or define extensions properly
abstract class MyExtension {
abstract val myProperty: Property<String>
}
val myExt = extensions.create<MyExtension>("myExt")
myExt.myProperty.set("value")
Why: Kotlin is statically typed; use extra for dynamic properties or define proper extensions.
Problem:
// Groovy uses 'all' without explicit call
tasks.withType(Test) { // Error in Kotlin
useJUnitPlatform()
}
Solution:
// Kotlin requires type parameter and explicit methods
tasks.withType<Test> {
useJUnitPlatform()
}
// Or with configureEach (lazy)
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
Why: Kotlin DSL uses generic type parameters (<Test>) for type safety.
Problem:
// Old Groovy pattern (eager)
tasks.create("myTask") {
doLast { println("Task") }
}
Solution:
// Modern Kotlin pattern (lazy)
tasks.register("myTask") {
doLast { println("Task") }
}
Why: register is lazy (better for configuration cache), create is eager. Prefer register in modern Gradle.
When migrating from Groovy to Kotlin DSL:
.gradle → .gradle.kts<Test>, <MavenPublication>)ext with extra or delegatestasks.register instead of tasks.createtasks.named<Type> instead of tasks.getByNameproject.property with providers.gradlePropertyWhile manual migration is often best, these tools can help:
# IntelliJ IDEA has built-in Groovy → Kotlin conversion
# Right-click build.gradle → Convert Groovy to Kotlin
# Gradle also provides a migration guide
# https://docs.gradle.org/current/userguide/migrating_from_groovy_to_kotlin_dsl.html
| Tool | Purpose |
|------|---------|
| gradle wrapper | Use Gradle Wrapper for version consistency |
| gradle init | Initialize new projects with proper structure |
| gradle build --scan | Build scans for performance analysis |
| gradle --configuration-cache | Enable configuration cache for faster builds |
| gradle --build-cache | Enable build cache for incremental builds |
development
Guide users through creating, reviewing, and fixing custom skills for Claude — both command skills (invoked via /slash) and context skills (auto-loaded by tech stack). Use when the user asks to create a skill, build a skill, make a new slash command skill, add a coding standards skill, review an existing skill, update a skill, or fix a skill that doesn't trigger.
development
Build or edit Slidev (sli.dev) presentations for tech talks, workshops, conference sessions, and live-coding demos. Use when the user asks to create slides, a deck, a presentation, a workshop deck, a conference talk, or edit an existing slides.md.
documentation
Download YouTube video transcripts with automatic frame extraction for visual references. Use when analyzing YouTube videos, tutorials, or conference talks.
documentation
Write INVEST-compliant user stories with Given-When-Then acceptance criteria. Use when writing user stories, creating acceptance criteria, or during /design Step 4.