Keeping growing software projects under control with Gradle

Joker Java Conference 2020 / Jendrik Johannes, Gradle Inc.

Who is Gradle?

Fully remote distributed company

gradle staff map

What is Gradle?

  • Gradle Build Tool

    • Open Source (APL)

    • for Java, Groovy, Kotlin, Scala, …​

    • Developed by Gradle Inc. with many community contributions

What is Gradle?

  • Gradle Enterprise

    • Works with multiple Built Tools: Gradle Build Tool, Maven, …​

    • Build Insights, Caching, Performance Analysis, Flaky Test Detection, …​

  • Public Build Scans for Gradle builds: scans.gradle.com

What’s special about Gradle Build Tool?

  • Can be used to write imperative scripts

    • like Ant, but with a proper programming language

  • Can be used to model your software

    • like Maven, but with a richer and more extensible model

  • Many Gradle builds today are a mix of both

    • Its strength is its weakness: spaghetti code build scripts are common :(

Perspective on Gradle in this talk

  • Look at Gradle as a tool to model your software

  • Let Gradle read that model to build your software



Even if you are new to Gradle, you can and should start this way!

Sample Architecture

overview reduced

Defining Components as Gradle Builds

How do you control Gradle?

  • You write "build scripts", which should be models of your software

  • Use Gradle Groovy DSL (Domain Specific Language)

  • Or Gradle Kotlin DSL (Domain Specific Language)



Gradle runs on the JVM and compiles to JVM bytecode. Other JVM language can also be used to control Gradle.

How does Gradle work?

  • A set of core systems which are language-agnostic

    • Execution engine, dependency management, caching, …​

  • Everything else is a Plugin

    • Core plugins: Java, Groovy, Scala, …​

    • Community plugins: Kotlin (by JetBrains), Android (by Google), …​

    • Your own plugins as part of your software!

Defining Components as Gradle Builds

plain components

Demo #1

We define each component in an independent Gradle build

─ domain-model
  └── settings.gradle.kts

─ server-application
  └── settings.gradle.kts

─ user-feature
  └── settings.gradle.kts

Component: User Feature

user feature 1
─ user-feature
  ├── data
  │   └─ build.gradle.kts
  │      | plugins { id("java-library") } // generic type (core plugin)
  │      | java    { toolchain { ... } }  // inidvidual configuration
  │      | ...
  │
  ├── table
  │   └─ build.gradle.kts
  │      | plugins { id("java-library") } // generic type (core plugin)
  │      | java    { toolchain { ... } }  // inidvidual configuration
  │      | ...
  │
  └── settings.gradle.kts
      | include("data", "table")        // inner component structure

Organising Build Configuration in a Build Logic Component

Adding a Build Logic Component

arch component 10

Demo #2 - We add a separate component (Gradle build) for build logic

- build-logic
  └── settings.gradle.kts

- domain-model
  └── settings.gradle.kts

- server-application
  └── settings.gradle.kts

- user-feature
  └── settings.gradle.kts

Component: User Feature

user feature 2
─ user-feature
  ├── data
  │   └── build.gradle.kts
  │       | plugins { id("com.example.java-library") } // project type
  │
  ├── table
  │   └── build.gradle.kts
  │       | plugins { id("com.example.java-library") } // project type
  │
  └── settings.gradle.kts
      | includeBuild("../build-logic")  // location of a source component
      | repositories { mavenCentral() } // location of binary components
      | include("data", "table")        // inner structure

Component: Build Logic

build logic
─ build-logic
  ├── java-library
  │   └── build.gradle.kts
  │       | plugins { `kotlin-dsl` } // project type for Gradle plugins
  │
  ├── kotlin-library
  │   └── build.gradle.kts
  │       | plugins { `kotlin-dsl` } // project type for Gradle plugins
  │
  └── settings.gradle.kts
      | // includeBuild(..)                   // location source component
      | repositories { gradlePluginPortal() } // location binary components
      | include("java-library", "kotlin-library") // inner structure

Composing a Product from Components

Composing a Product from Components

product

Demo #3 - We add dependencies between our components

- build-logic
  └── settings.gradle.kts

- domain-model
  └── settings.gradle.kts

- server-application
  └── settings.gradle.kts

- user-feature
  └── settings.gradle.kts

Component: User Feature

user feature 3
─ user-feature
  ├── data
  │   └── build.gradle.kts
  │       | plugins { id("com.example.java-library") } // project type
  │       | dependencies { api("com.example.myproduct:release") }
  │
  ├── table
  │   └── build.gradle.kts
  │       | plugins { id("com.example.java-library") } // project type
  │       | dependencies { ... }
  │
  └── settings.gradle.kts
      | includeBuild("../build-logic")  // location of a source component
      | includeBuild("../domain-model") // location of a source component
      | repositories { mavenCentral() } // location of binary components
      | include("data", "table")        // inner structure

Working with your Gradle builds

Gradle tasks

  • Gradle knows inputs and outputs

  • If output of a task is the input of another, there is a dependency

  • Incremental - a task only executes if input/output changes

  • Build cache - Outputs can be retrieved from (remote) cache

Demo #4

Exploring: task execution, incremental builds, build cache, custom tasks

Custom tasks

  • An abstract class extending DefaultTask

  • Written in Java, Scala, Groovy, Kotlin

  • Properties as annotated abstract getter methods (getPropertyName())

  • Each task has an action implemented in a @TaskAction method

Summary

  • Look at Gradle as a tool to model your software

  • Treat each component in your architecture as a separate Gradle build

  • Treat build configuration and customization as separate components

Try this at home!

Variants and dependency resolution

  • Each project (or binary component) has multiple variants

  • Gradle selects one variant during dependency resolution

  • For example: Java Libraries have an "API" and a "Runtime" variant

variants declared

Publishing and using binary components

  • Publish any project as binary component with maven-publish plugin

  • Published to repository (can be a local folder)

binary component
─ user-feature
  └── settings.gradle.kts
      | // includeBuild("domain-model") // find here ⤵️ instead
      | repositories { maven { url = uri("my-repository") } }

Gradle Antipatterns

  • Tasks implementation (imperative code) in build scripts

  • Don’t access state from other projects

    • project(":table").tasks.jar.archiveFile ← don’t do this

  • Using a "root project" build.gradle(.kts) to share build logic

    • You need to keep care of order yourself (when is a plugin applied exactly?)

    • Mix of concerns (one big if/else block)

    • Can’t access plugin extensions easily

allprojects { // Don't do this!
    if (plugins.hasPlugin("java-library")) { ... }
    if (plugins.hasPlugin("org.jetbrains.kotlin.jvm")) { ... }
}

Imperative code in the wrong places

  • If you need to run "clean" to trust your build…​

    • …​you may have imperative logic outside tasks (or transforms)

    • …​a custom task implementation is broken