Skip to content
AgileLogics
7 min read

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 the Project object, 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.register creates 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 inputs and outputs lets 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.