Incremental builds and the build cache

Gradle · Advanced · 12 min

Build speed depends on Gradle being able to skip work; misdeclared inputs make builds slow or wrong.

Gradle avoids work at three levels.

Up-to-date checks: every task declares inputs (files, properties) and outputs. Before running a task, Gradle hashes the inputs; if they and the outputs match the previous run, the task is skipped with UP-TO-DATE. Custom tasks must declare them with @InputFile, @InputDirectory, @Input, @OutputDirectory on typed task classes, or with inputs.files(...) / outputs.dir(...) in scripts. Undeclared inputs cause stale results; undeclared outputs cause needless reruns.

Build cache (org.gradle.caching=true in gradle.properties): task outputs are stored keyed by the input hash, so a task whose inputs were seen before, even on another branch or another machine via a remote cache (Develocity, HTTP cache), is restored as FROM-CACHE instead of executed. Compilation and tests are cacheable; tasks must be deterministic and declare all inputs. Mark custom task types @CacheableTask with path sensitivity annotations.

Configuration cache (org.gradle.configuration-cache=true): serializes the result of the configuration phase so later builds skip running scripts entirely. It requires scripts and plugins to avoid Project access at execution time and to use providers and Property types.

Inspect decisions with --info (reasons a task ran), --scan for a build scan, and ./gradlew help --task x. --rerun-tasks ignores up-to-date checks for debugging.

Example

// gradle.properties
// org.gradle.caching=true
// org.gradle.configuration-cache=true
// org.gradle.parallel=true

// build.gradle.kts: a custom cacheable task with declared inputs and outputs
abstract class Checksum : DefaultTask() {          // @CacheableTask in a plugin
    @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val source: RegularFileProperty
    @get:OutputFile
    abstract val target: RegularFileProperty

    @TaskAction
    fun run() {
        val bytes = source.get().asFile.readBytes()
        val sha = java.security.MessageDigest.getInstance("SHA-256").digest(bytes)
        target.get().asFile.writeText(sha.joinToString("") { "%02x".format(it) })
    }
}

tasks.register<Checksum>("checksum") {
    source = layout.projectDirectory.file("README.md")
    target = layout.buildDirectory.file("README.sha256")
    outputs.cacheIf { true }    // script-defined types also need this; plugin classes use @CacheableTask alone
}
// 1st run: > Task :checksum        2nd run: > Task :checksum UP-TO-DATE
// after clean with caching on:    > Task :checksum FROM-CACHE

Remember

  • Tasks skip when declared inputs and outputs are unchanged
  • The build cache restores outputs by input hash, locally or remotely
  • The configuration cache skips the configuration phase
  • Undeclared inputs cause stale builds; declare everything

Watch out for

  • Reading a file or environment variable in a task without declaring it as an input, so changes are never picked up.

Check yourself

  1. What does FROM-CACHE next to a task mean?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.gradle.org/current/userguide/build_cache.html