standards-gradle — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited standards-gradle (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
#### build.gradle.kts Structure
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")
}
}
}#### settings.gradle.kts Basics
// 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")#### Repository Configuration
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!
// }
// }#### Script Organization Best Practices
// 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.
#### The Three Build Phases
Every Gradle build runs through three distinct phases in order:
Understanding these phases helps you:
#### 1. Initialization Phase
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.
#### 2. Configuration Phase
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!
#### 3. Execution Phase
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.
#### When Code Runs - Quick Reference
| 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() |
#### Common Mistakes and Anti-Patterns
// ❌ 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#### Why Build Phases Matter
1. Build Performance
Configuration phase runs on every build:
./gradlew tasks # Configuration runs
./gradlew clean # Configuration runs
./gradlew build # Configuration runs
./gradlew --stop # Configuration runsSlow 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 secondsBenefits:
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 optimizations#### Best Practices for Build Phases
Do:
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#### Debugging Build Phases
# 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"#### Example: Full Build Lifecycle
// 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#### Dependency Configurations
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
// }#### Version Catalogs (Modern Gradle Approach)
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#### Dependency Constraints
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")
}
}#### Platform/BOM Dependencies
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")
}#### Excluding Transitive Dependencies
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")
}#### Dependency Notation
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"))
}#### Plugin Application
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#### Using Version Catalogs with Plugins
// 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)
}#### Common Plugins
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 installDistJava 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#### Configuring Plugin Extensions
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#### Conditional Plugin Application
plugins {
java
if (project.hasProperty("enableKotlin")) {
kotlin("jvm") version "2.0.0"
}
}
// Alternative: Apply plugin conditionally
if (project.findProperty("coverage") == "true") {
apply(plugin = "jacoco")
}#### Project Structure
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 (Recommended Approach)
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")
}#### Cross-Module Dependencies
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"))
}#### Shared Configuration Patterns
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 concernsPattern 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 vs Included Builds vs Composite Builds
buildSrc (For Convention Plugins):
project/
├── buildSrc/
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ └── java-conventions.gradle.kts
└── build.gradle.ktsIncluded 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")#### Dependency Management Across Modules
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 (Stable in Gradle 9)
Configuration cache dramatically speeds up builds by caching the result of the configuration phase.
Enable in gradle.properties:
org.gradle.configuration-cache=trueOr via command line:
./gradlew build --configuration-cacheBenefits:
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#### Build Cache (Enhanced in Gradle 9)
Enable in gradle.properties:
org.gradle.caching=trueOr via command line:
./gradlew build --build-cacheConfigure 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
}
}
}#### Improved Test Suites API
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 Toolchains (Enhanced)
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)
}
}#### Problems API (New in Gradle 8+, Refined in 9)
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!#### Isolated Projects (Experimental in Gradle 9)
Parallel configuration of subprojects for massive multi-module builds.
# gradle.properties
org.gradle.unsafe.isolated-projects=trueBenefits:
#### Deprecated Features to Avoid
// 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#### Performance Improvements in Gradle 9
#### Best Practices for Gradle 9
// 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.
#### Lazy vs Eager Task Registration
// 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)#### Task Actions
// 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)
}#### Abstract Task Classes (Recommended for Reusable Tasks)
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-"
}#### Input and Output Annotations
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)#### Task Dependencies
// 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
}#### Working Example: File Processing Task
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"
}#### Task Configuration Avoidance
// 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.
#### Simple Extension
// 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
}#### Extension Anti-Patterns
// 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
}
}#### Extension with Nested Configuration
// 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")
}
}#### Extension with Named Domain Objects
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()}")
}
}#### Extension Best Practices
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
// }#### Connecting Extension to Tasks
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.
#### Provider<T> Basics
// 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")#### Property<T> for Mutable Values
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)
}#### Transforming Providers
// 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"
}#### Connecting Providers
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 })
}#### File and Directory Providers
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()
}
}#### Collection Providers
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")
}#### Common Anti-Patterns to Avoid
// 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
}
}#### Provider Best Practices
// 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).
#### Build Cache Basics
The build cache stores task outputs and reuses them when inputs haven't changed.
Enable build cache (gradle.properties):
org.gradle.caching=trueOr via command line:
./gradlew build --build-cacheHow it works:
#### Writing Cache-Compatible Tasks
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")
}
}#### What Makes Tasks Cacheable
✅ GOOD - Cacheable:
❌ BAD - Not Cacheable:
#### Making Built-in Tasks 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
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-cacheBenefits:
#### Configuration Cache Compatibility
// 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#### Common Configuration Cache Issues
// 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")
}
}#### Cache Debugging
# 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#### Remote Build 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#### Cache Performance Tips
// 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#### Measuring Cache Effectiveness
# 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.
#### Binary Plugin (Plugin<Project>)
// 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 va~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.