Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Gradle: Your Build, Your Rules

Avatar for Aurimas Liutikas Aurimas Liutikas
September 25, 2025
7

Gradle: Your Build, Your Rules

Talk by Aurimas Liutikas at DPE Summit 2025 in San Francisco

Avatar for Aurimas Liutikas

Aurimas Liutikas

September 25, 2025
Tweet

Transcript

  1. Target Audience You use Gradle You have a build team

    Care about consistency Can no longer review every build.gradle.kts change
  2. plugins { id("com.android.library") id("org.jetbrains.kotlin.android") } android { namespace = "com.example.mylibrary"

    compileSdk = 36 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "11" } } dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) }
  3. Leave only dependencies {} and custom extension plugins { id("FancyPlugin")

    id("com.android.library") } dependencies { implementation("foo:bar:1.0.0") } fancy { allowedToggleFoo = true
  4. Add reviewers for change patterns For changes outside of dependencies

    and fancy DSLs automatically add reviewers from the build team banana { allowedToggleFoo = true allowedToggleBar = true } android { compileSdk = 36 }
  5. banana { allowedToggleFoo = true allowedToggleBar = true } android

    { compileSdk = 36 } Add reviewers for change patterns REST API call /repos/{owner}/{repo}/ pulls/{pull_number}/ requested_reviewers
  6. disallowChanges on DSL properties private fun Project.configureKotlin() { val kotlin

    = extensions.getByType( KotlinAndroidExtension::class.java ) kotlin.compilerOptions.jvmTarget.setAndDisallow( JvmTarget.JVM_1_8 ) }
  7. disallowChanges on DSL properties FAILURE: Build failed with an exception.

    * Where: Build file 'your-build-your-rules/build.gradle.kts' line: 45 * What went wrong: The value for property 'jvmTarget' cannot be changed any further. ❌
  8. finalizeDsl for Android projects private fun Project.configureAppPlugin() { val android

    = extensions.getByType( ApplicationAndroidComponentsExtension::class.java ) android.finalizeDsl { it.compileSdk = 36 } }
  9. Last resort afterEvaluate private fun Project.configureJavaPlugin() { val java =

    extensions.getByType( JavaPluginExtension::class.java) afterEvaluate { java.sourceCompatibility = JavaVersion.VERSION_1_8 java.targetCompatibility = JavaVersion.VERSION_1_8 } }
  10. afterEvaluate { androidComponents.finalizeDsl { it.compileSdk = 35 } } afterEvaluate

    { afterEvaluate { java.sourceCompatibility = JavaVersion.VERSION_17 java.targetCompatibility = JavaVersion.VERSION_17 } }
  11. Extension validation abstract class DslValidationTask : DefaultTask() { @get:Input abstract

    val testRunner: Property<String> @TaskAction fun validate() { if (testRunner.get() != EXPECTED_TEST_RUNNER) throw Exception("Use $EXPECTED_TEST_RUNNER!") } } private const val EXPECTED_TEST_RUNNER = "androidx.test..."
  12. private fun Project.configureAppPlugin() { val android = extensions.getByType( ApplicationExtension::class.java) val

    testRunner = provider { android.defaultConfig.testInstrumentationRunner } tasks.register( "validateDsl", DslValidationTask::class.java) { it.testRunner.set(testRunner) } }
  13. Extension validation android { defaultConfig { testInstrumentationRunner = "com.BadRunner" }

    } * What went wrong: Execution failed for task ':app:validateDsl'. > java.lang.Exception: Use androidx.test.runner.AndroidJUnitRunner ❌
  14. Task output validation abstract class ValidateManifestTask : DefaultTask() { @get:InputFile

    abstract val manifest: RegularFileProperty @TaskAction fun validate() { if (!manifest.get().asFile.readText().contains( """<uses-sdk android:minSdkVersion="24" />""" )) throw Exception("minSdkVersion was not set to 24") } }
  15. Task output validation private fun Project.configureAndroidLibraryPlugin() { extensions.getByType( LibraryAndroidComponentsExtension::class.java ).onVariants

    { tasks.register("validateManifest${it.name}", ValidateManifestTask::class.java) { task -> task.manifest.set( it.artifacts.get(SingleArtifact.MERGED_MANIFEST)) } } }
  16. Task output validation android { defaultConfig { minSdk = 30

    } } * What went wrong: Execution failed for task ':lib:validateManifestdebug'. > java.lang.Exception: minSdkVersion was not set to 24 ❌
  17. build.gradle.kts content validation abstract class ValidateBuildGradleTask : DefaultTask() { @get:InputFile

    abstract val build: RegularFileProperty @TaskAction fun validate() { val buildContents = build.get().asFile.readText() if (buildContents.contains("kotlin {")) throw Exception("Do not configure Kotlin ...") } }
  18. build.gradle.kts content validation kotlin { // any configuration } *

    What went wrong: Execution failed for task ':app:validateBuildGradle'. > java.lang.Exception: Do not configure Kotlin plugin directly. ❌
  19. Move to a custom build.json definition file { "type": "androidLibrary",

    "dependencies": { "implementation": [ "androidx.annotation:annotation:1.9.1" ] } }
  20. Basic model of JSON using moshi @JsonClass(generateAdapter = true) data

    class BuildFile( val type: ProjectType, val dependencies: Dependencies, ) enum class ProjectType { androidLibrary, androidApplication } @JsonClass(generateAdapter = true) data class Dependencies(val implementation: List<String>)
  21. Parse the JSON class JsonProjectPlugin : Plugin<Project> { override fun

    apply(project: Project) { val buildJsonFile = project.layout.projectDirectory.file("build.json").asFile val moshi: Moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter<BuildFile>() val build = jsonAdapter.fromJson(buildJsonFile.readText())!! // ..
  22. Configure based on build.json class JsonProjectPlugin : Plugin<Project> { override

    fun apply(project: Project) { // .. val build = jsonAdapter.fromJson(buildJsonFile.readText())!! when (build.type) { ProjectType.androidLibrary -> project.library(build) ProjectType.androidApplication -> project.app(build) } }
  23. Configure based on build.json private fun Project.library(build: BuildFile) { configureKotlin()

    plugins.apply("com.android.library") configureAndroidCommon() build.dependencies.implementation.forEach { dep -> dependencies.add("implementation", dep) } }
  24. Other formats - XML <?xml version="1.0" encoding="UTF-8"?> <androidLibrary> <dependencies> <implementation>

    androidx.annotation:annotation:1.9.1 </implementation> </dependencies> </androidLibrary>
  25. Declarative Gradle javaApplication { javaVersion = 21 mainClass = "com.example.App"

    dependencies { implementation(project(":java-util")) implementation("com.google.guava:guava:32.1.3-jre") } }
  26. Takeaways Many ways to enforce consistency As build grows strictness

    tends to increase Pick the strictness based on the severity of making a mistake
  27. Takeaways Many ways to enforce consistency As build grows strictness

    tends to increase Pick the strictness based on the severity of making a mistake build.gradle(.kts) is not required to configure projects
  28. Things we didn’t talk about Dependency locking, substitution, blocking Versioning

    of projects Locking repositories used Allowlist of Gradle plugins
  29. Things we didn’t talk about Dependency locking, substitution, blocking Versioning

    of projects Locking repositories used Allowlist of Gradle plugins Static analysis of Gradle plugins …