Skip to content

๐Ÿ˜ Gradle

Build cache debugging

./gradlew clean build --build-cache -Dorg.gradle.caching.debug=true
Build cache key for task ':compileKotlin' is 6b60211f33e6467e5a0b02e250948c86
tar --gzip --list --verbose --file ~/.gradle/caches/build-cache-1/6b60211f33e6467e5a0b02e250948c86
-rw-r--r-- 0/0             322 0000-00-00 00:00 METADATA
drwxr-xr-x 0/0               0 0000-00-00 00:00 tree-classpathSnapshotProperties.classpathSnapshotDir/
-rw-r--r-- 0/0           37404 0000-00-00 00:00 tree-classpathSnapshotProperties.classpathSnapshotDir/shrunk-classpath-snapshot.bin
drwxr-xr-x 0/0               0 0000-00-00 00:00 tree-destinationDirectory/
...
drwxr-xr-x 0/0               0 0000-00-00 00:00 tree-destinationDirectory/META-INF/
-rw-r--r-- 0/0              66 0000-00-00 00:00 tree-destinationDirectory/META-INF/processor.kotlin_module
...

Cache Node Health

This is not publicly advertised and is subject to change.

curl -s 'https://my-gradle-cache-node.tld/cache-node-info/health'
AuthenticationCircuitBreaker : HEALTHY [Circuit breaker has not tripped recently]
ConfigBinding : HEALTHY
ConfigLoad : HEALTHY
JvmGcHeapPressure : HEALTHY [GC overhead at 0%]
JvmLowPoolMemory : HEALTHY [Old gen pool memory after last GC at 35%]
StartedUsingDeprecatedCliOptions : HEALTHY

Cleanup caches and build directories

Global cache

for d in ~/.gradle/{.tmp,build-scan-data,caches,daemon,jdks,native,wrapper}; do
    test -d "$d" && du --human-readable --summarize "$d" && rm --recursive "$d"
done
for d in ~/.gradle/{.tmp,build-scan-data,caches,daemon,jdks,native,wrapper}; do
    test -d "$d" && du -hs "$d" && rm -rf "$d"
done

Local cache

test -d .gradle && du --human-readable --summarize .gradle && rm --recursive .gradle
test -d .gradle && du -hs .gradle && rm -rf .gradle

Build directories

find . -type d -name 'build' -prune -exec du --human-readable --summarize '{}' \; -exec rm --recursive '{}' \;
find . -type d -name 'build' -prune -exec du -hs '{}' \; -exec rm -rf '{}' \;

Configuration avoidance

Avoid the cost of creating and configuring tasks during Gradleโ€™s configuration phase when those tasks will never be executed

  • How do I defer task creation?

    TaskContainer.create(String)
    
    TaskContainer.register(String)
    
  • How do I defer task configuration?

    Eager APIs will immediately create and configure any registered tasks.

    DomainObjectCollection.all(Action)
    DomainObjectCollection.withType(Class, Action)
    // Equivalent to
    DomainObjectCollection.withType(type).all(Action)
    
    DomainObjectCollection.withType(Class).configureEach(Action)
    
  • How do I reference a task without creating/configuring it?

    TaskContainer.create(String, โ€ฆ)
    TaskContainer.getByName(String, โ€ฆ)
    TaskContainer.getByPath(String)
    TaskContainer.findByName(String)
    TaskContainer.findByPath(String)
    
    TaskContainer.register(String, โ€ฆ)
    TaskContainer.named(String, โ€ฆ)
    
  • How do I order tasks with configuration avoidance in mind?

    Strong relationships, which will force the execution of referenced tasks, even if they wouldnโ€™t have been created otherwise.

    Task.dependsOn(โ€ฆ)
    Task.finalizedBy(โ€ฆ)
    

    Soft relationships, which can only change the order of existing tasks, but canโ€™t trigger their creation.

    Task.mustRunAfter(โ€ฆ)
    Task.shouldRunAfter(โ€ฆ)
    

๐Ÿ”—

Declaring a repository filter

Quote

Gradle exposes an API to declare what a repository may or may not contain.

By default, repositories include everything and exclude nothing:

  • If you declare an include, then it excludes everything but what is included.
  • If you declare an exclude, then it includes everything but what is excluded.
  • If you declare both includes and excludes, then it includes only what is explicitly included and not excluded.

See RepositoryContentDescriptor for all available methods.

repositories {
    maven {
        url = uri("https://repo.mycompany.com/maven2")
        content {
            // this repository *only* contains artifacts with group "my.company"
            includeGroup("my.company")
        }
    }
    mavenCentral {
        content {
            // this repository contains everything BUT artifacts with group starting with "my.company"
            excludeGroupByRegex("my\\.company.*")
        }
    }
}

Warning

Filters declared using the repository-level content filter are not exclusive. This means that declaring that a repository includes an artifact doesnโ€™t mean that the other repositories canโ€™t have it either: you must declare what every repository contains in extension.

๐Ÿ”—

Declaring content exclusively found in one repository

repositories {
    // This repository will _not_ be searched for artifacts in my.company despite being declared first
    mavenCentral()
    exclusiveContent {
        forRepository {
            maven {
                url = uri("https://repo.mycompany.com/maven2")
            }
        }
        filter {
            // this repository *only* contains artifacts with group "my.company"
            includeGroup("my.company")
        }
    }
}

๐Ÿ”—

Download dependencies upfront

This relies on the dependency verification feature:

./gradlew assemble lint test connectedAndroidTest --dry-run --write-verification-metadata sha256

This will then create gradle/verification-metadata.dryrun.xml as a side-effect.

Quote

Because --dry-run doesnโ€™t execute tasks, this would be much faster, but it will miss any resolution happening at task execution time.

Gradle properties are not passed to included builds

When requesting an included build like this:

settings.gradle.kts
pluginManagement {
    includeBuild("build-logic")
}

Gradle properties from the default gradle.properties are not passed to it. Instead you must have a dedicated build-logic/gradle.properties, or use a symbolic link.

๐Ÿ”—

Gradle tasks

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.

Gradle Enterprise tasks
-----------------------
buildScanPublishPrevious - Publishes the data captured by the last build.
provisionGradleEnterpriseAccessKey - Provisions a new access key for this build environment.

Help tasks
----------
javaToolchains - Displays the detected java toolchains.
projects - Displays the sub-projects of root project.
properties - Displays the properties of root project.

Kotlin extensions

fun Project.isRootProject() = rootProject === this

fun Project.isJava() = isJavaLibrary() || project.pluginManager.hasPlugin("java")
fun Project.isJavaLibrary() = project.pluginManager.hasPlugin("java-library")

fun Project.isKotlin() = isKotlinAndroid() || isKotlinJvm()
fun Project.isKotlinAndroid() = pluginManager.hasPlugin("org.jetbrains.kotlin.android")
fun Project.isKotlinJvm() = pluginManager.hasPlugin("org.jetbrains.kotlin.jvm")

fun Project.isAndroid() = isAndroidApplication() || isAndroidLibrary() || isAndroidTest() || isAndroidDynamicFeature()
fun Project.isAndroidApplication() = plugins.hasPlugin(AppPlugin::class.java)
fun Project.isAndroidLibrary() = plugins.hasPlugin(LibraryPlugin::class.java)
fun Project.isAndroidTest() = plugins.hasPlugin(TestPlugin::class.java)
fun Project.isAndroidDynamicFeature() = plugins.hasPlugin(DynamicFeaturePlugin::class.java)

fun Project.isUsingKapt() = pluginManager.hasPlugin("org.jetbrains.kotlin.kapt")
fun Project.isUsingKsp() = pluginManager.hasPlugin("com.google.devtools.ksp")
fun Project.isUsingLint() = pluginManager.hasPlugin("com.android.internal.lint")


/**
 * Best-effort tries to apply an [action] on a task with matching [name]. If the task doesn't exist
 * at the time this is called, a [TaskContainer.whenTaskAdded] callback is added to match on the
 * name and execute the action when it's added.
 *
 * This approach has caveats, namely that you won't get an immediate failure or indication if you've
 * requested action on a task that may never be added. This is intended to be similar to the
 * behavior of [PluginManager.withPlugin].
 */
fun TaskContainer.withName(name: String, action: Action<Task>) {
    try {
        named(name, action)
    } catch (_: UnknownTaskException) {
        whenTaskAdded {
            if (this@whenTaskAdded.name == name) action(this)
        }
    }
}

๐Ÿ”—

List project properties

gradlew properties

# To get a specific property
gradlew -q properties --console=plain | grep "^<my-property>:" | awk '{printf $2}'

# Or on newer versions
gradlew -q properties --property=<my-property>

Override build type attribute

implementation(project(":lib")) {
    attributes {
        attribute(BuildTypeAttr.ATTRIBUTE, objects.named<BuildTypeAttr>(DEBUG))
    }
}

Override Version Catalog versions

dependencyResolutionManagement {
    versionCatalogs {
        val prefix = "libs_version_"
        val overrides = providers.systemPropertiesPrefixedBy(prefix)
            .get().mapKeys { (key, _) -> key.removePrefix(prefix) }
        maybeCreate("libs").apply {
            if (overrides.isNotEmpty()) logger.lifecycle("Overriding versions $overrides")
            overrides.forEach { (key, value) -> version(key, value) }
        }
    }
}
gradlew --system-prop "libs_version_=1.2.3" --system-prop "libs_version_=3.2.1"
# or simpler
gradlew -Dlibs_version_foo=1.2.3 -Dlibs_version_bar=3.2.1
Overriding versions {foo=1.2.3, bar=3.2.1}

Project properties

You can add properties directly to your Project object via the -P command line option. Gradle can also set project properties when it sees specially-named system properties or environment variables.

  • Setting a project property via a system property:

    org.gradle.project.foo=bar
    

  • Setting a project property via an environment variable:

    ORG_GRADLE_PROJECT_foo=bar
    

๐Ÿ”—

Reproducible builds

build.gradle.kts
tasks.withType<AbstractArchiveTask>().configureEach {
    isPreserveFileTimestamps = false
    isReproducibleFileOrder = true
}

๐Ÿ”—

Run UP-TO-DATE tests

--rerun-tasks will not work!

  • ./gradlew cleanTest test
    
  • tasks.withType<Test>().configureEach {
        outputs.upToDateWhen { false }
    }
    

Run specific tasks in serial

--no-parallel and --max-workers are applied globally!

SerialBuildService.kt
abstract class SerialBuildService : BuildService<None> {
    companion object {
        fun register(project: Project) = with(project.gradle.sharedServices) {
            registerIfAbsent("serial", SerialBuildService::class) {
                maxParallelUsages.set(1)
            }
        }
    }
}
build.gradle.kts
val service = SerialBuildService.register(this)

tasks.withType<Test>().configureEach {
    usesService(service)
}

System properties

Using the -D command-line option, you can pass a system property to the JVM which runs Gradle.
You can also set system properties in gradle.properties files with the prefix systemProp..

gradle.properties
systemProp.gradle.user.home=/gradle

Info

command-line options take precedence over system properties.

๐Ÿ”—

Test fixtures

Producer

repo/build.gradle.kts
plugins {
    kotlin("jvm")
    `java-test-fixtures`
}

dependencies {
    // Dependencies of test fixtures (not leaked to consumers)
    testFixturesImplementation("...")
}
repo/src/main/kotlin/com/example/Repository.kt
fun interface Repository {
    operator fun invoke(id: String): Boolean
}
repo/src/testFixtures/kotlin/com/example/FakeRepository.kt
class FakeRepository(
    private val block: (id: String) -> Boolean = ::fail
): Repository {
    override fun invoke(id: String): Boolean = block(id)
}

Consumer

app/build.gradle.kts
plugins {
    kotlin("jvm")
}

dependencies {
    implementation(project(":repo"))
    testImplementation(testFixtures(project(":repo")))
}
app/src/test/kotlin/com/example/Test.kt
class Test {

    @Test
    fun test() {
        val repository: Repository = FakeRepository { id: String -> todo() }
        /* ... */
    }

}

๐Ÿ”—

Upgrading the Gradle Wrapper

# Update the version in gradle/wrapper/gradle-wrapper.properties
./gradlew wrapper --gradle-version x.y.z --distribution-type bin
# Update the in gradle-wrapper.jar
./gradlew wrapper
# Validate the version
./gradlew --version

๐Ÿ”—

Version Catalog extensions and delegates

Extension to access VersionCatalog:

fun Project.versionCatalog(name: String = "libs"): VersionCatalog =
    project.extensions.getByType<VersionCatalogsExtension>().named(name)

Extensions to fetch versions, libraries, plugins and bundles from a VersionCatalog as delegates:

fun VersionCatalog.versions(): ReadOnlyProperty<Any?, VersionConstraint> = ReadOnlyProperty { _, property ->
    findVersion(property.name).orElseThrow {
        IllegalStateException("Version alias named ${property.name} doesn't exist in catalog named $name")
    }
}

fun VersionCatalog.libraries(): ReadOnlyProperty<Any?, Provider<MinimalExternalModuleDependency>> = ReadOnlyProperty { _, property ->
    findLibrary(property.name).orElseThrow {
        IllegalStateException("Library alias named ${property.name} doesn't exist in catalog named $name")
    }
}

fun VersionCatalog.plugins(): ReadOnlyProperty<Any?, Provider<PluginDependency>> = ReadOnlyProperty { _, property ->
    findPlugin(property.name).orElseThrow {
        IllegalStateException("Plugin alias named ${property.name} doesn't exist in catalog named $name")
    }
}

fun VersionCatalog.bundles(): ReadOnlyProperty<Any?, Provider<ExternalModuleDependencyBundle>> = ReadOnlyProperty { _, property ->
    findBundle(property.name).orElseThrow {
        IllegalStateException("Bundle alias named ${property.name} doesn't exist in catalog named $name")
    }
}

This can then be used like this in a custom Plugin:

gradle/libs.versions.toml
[versions]
kotlin = "1.7.10"

[plugins]
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

[bundles]
example = ["lib1", "lib2"]

[libraries]
example1 = "com.example:lib1:#"
example2 = "com.example:lib2:#"
kotlinxCoroutinesCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "1.6.4" }
class MyCatalog(catalog: VersionCatalog): VersionCatalog by catalog {
    val kotlin by versions() // "1.7.10"
    val `org-jetbrains-kotlin-android` by plugins() // "org.jetbrains.kotlin.android:1.7.10"
    val example by bundles() // ["com.example:lib1:#", "com.example:lib2:#"]
    val kotlinxCoroutinesCore by library() // "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
}

class MyPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        val catalog = MyCatalog(target.versionCatalog())
        dependencies {
            add("implementation", catalog.kotlinxCoroutinesCore)
        }
    }
}

Version declaration semantics

  • An exact version: e.g. 1.3, 1.3.0-beta3, 1.0-20150201.131010-1
  • A Maven-style version range: e.g. [1.0,), [1.1, 2.0), (1.2, 1.5]
  • A prefix version range: e.g. 1.+, 1.3.+
  • A latest-status version: e.g. latest.integration, latest.release
  • A Maven SNAPSHOT version identifier: e.g. 1.0-SNAPSHOT, 1.4.9-beta1-SNAPSHOT

Shorthand notation for strict dependencies

dependencies {
    // short-hand notation with !!
    implementation("org.slf4j:slf4j-api:1.7.15!!")
    // is equivalent to
    implementation("org.slf4j:slf4j-api") {
        version {
           strictly("1.7.15")
        }
    }

๐Ÿ”—

Versions checks

Gradle
GradleVersion.current() >= GradleVersion.version("8.0")
Java
JavaVersion.current() >= JavaVersion.VERSION_17
JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)

Welcome message

Quote

Controls whether Gradle should print a welcome message. If set to never then the welcome message will be suppressed. If set to once then the message is printed once for each new version of Gradle. Default is once.

gradle.properties
org.gradle.welcome=never