๐ 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:
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¶
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!
abstract class SerialBuildService : BuildService<None> {
companion object {
fun register(project: Project) = with(project.gradle.sharedServices) {
registerIfAbsent("serial", SerialBuildService::class) {
maxParallelUsages.set(1)
}
}
}
}
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.
.
systemProp.gradle.user.home=/gradle
Info
command-line options take precedence over system properties.
Test fixtures¶
Producer
plugins {
kotlin("jvm")
`java-test-fixtures`
}
dependencies {
// Dependencies of test fixtures (not leaked to consumers)
testFixturesImplementation("...")
}
fun interface Repository {
operator fun invoke(id: String): Boolean
}
class FakeRepository(
private val block: (id: String) -> Boolean = ::fail
): Repository {
override fun invoke(id: String): Boolean = block(id)
}
Consumer
plugins {
kotlin("jvm")
}
dependencies {
implementation(project(":repo"))
testImplementation(testFixtures(project(":repo")))
}
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
:
[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¶
GradleVersion.current() >= GradleVersion.version("8.0")
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
.
org.gradle.welcome=never