Gradle Build Scripts
Prerequisites: Building domain-specific languages
Gradle is the build tool used by Android and a large share of Java,
Kotlin, and Groovy projects. Its build files come in two flavors:
build.gradle (the Groovy DSL) and build.gradle.kts (the Kotlin
DSL). Everything you've learned about closures, delegates, and command
chains is what makes the Groovy DSL work.
A typical build.gradle
// illustrative — build.gradle
plugins {
id 'groovy'
id 'application'
}
group = 'com.example'
version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.groovy:groovy:5.0.3'
implementation 'org.apache.groovy:groovy-json:5.0.3'
testImplementation platform('org.spockframework:spock-bom:2.4-groovy-5.0')
testImplementation 'org.spockframework:spock-core'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
application {
mainClass = 'com.example.App'
}
tasks.named('test') {
useJUnitPlatform()
}
Read it as Groovy:
plugins { ... },repositories { ... },dependencies { ... }are method calls taking closures, whose delegates are Gradle objects.implementation 'group:name:version'is a method call without parentheses.version = '1.0.0'sets a property on theProjectobject, which is the delegate of the whole script.- The Groovy and Spock versions above are examples — use the ones your project targets (Spock publishes a separate artifact per Groovy major version).
Running builds
gradle init --type groovy-application # create a new project
./gradlew build # compile, test, and package
./gradlew test --tests '*CartSpec' # run some tests
./gradlew run # run the application
./gradlew tasks # list available tasks
./gradlew dependencies # show the dependency tree
Always commit and use the Gradle wrapper (gradlew, gradlew.bat,
and gradle/wrapper/), so everyone builds with the same Gradle
version.
Custom tasks
// illustrative — build.gradle
tasks.register('releaseNotes') {
group = 'documentation'
description = 'Writes release notes from git tags'
def output = layout.buildDirectory.file('release-notes.md')
outputs.file(output)
doLast {
def notes = ['git', 'log', '--oneline', '-10'].execute().text
output.get().asFile.text = "# ${project.version}\n\n$notes"
println "wrote ${output.get().asFile.name}"
}
}
tasks.register('printDeps') {
doLast {
configurations.runtimeClasspath.resolvedConfiguration
.resolvedArtifacts.collect { it.moduleVersion.id.toString() }
.sort().each { println it }
}
}
tasks.registercreates a task lazily (only when it's needed).- Code in
doLast { }runs when the task executes; code directly in the configuration block runs during configuration, on every build — keep that part cheap. - Declaring
inputsandoutputslets Gradle skip up-to-date tasks and cache results.
Multi-project builds and convention plugins
// illustrative — settings.gradle
rootProject.name = 'shop'
include 'api', 'core', 'worker'
Shared configuration belongs in convention plugins under
buildSrc/ or an included build (build-logic/), written as
*.gradle files in the Groovy DSL or as Groovy/Java classes. Avoid
allprojects { } blocks that reach into other projects — they slow
down builds and break configuration caching.
Groovy DSL or Kotlin DSL?
Gradle now defaults to the Kotlin DSL for new projects because of its stronger IDE support. The Groovy DSL remains fully supported and is what most existing builds, tutorials, and plugins' documentation use — so reading it is an essential skill even if you write Kotlin DSL.