What is an incremental task in Gradle, and why would you author one instead of relying on Gradle's normal UP-TO-DATE checking?
answer
- @Incremental on file input
- InputChanges param on @TaskAction
- getFileChanges -> ChangeType ADD/MODIFY/REMOVE
- isIncremental false => all ADDED
- delete output on REMOVED
basics
~20 sA task that, when re-run, processes only the input files that changed since the last run instead of all of them. You author one to avoid redoing work for the whole input set when a few files changed.
solid answer
~40 sGradle's standard incremental build works at task granularity: if any input changes, the whole task action re-runs. An incremental task goes finer — it inspects exactly which input files were added, modified, or removed and processes only those. You declare an `org.gradle.work.InputChanges` parameter on your `@TaskAction` method and mark a file input property with `@Incremental`. Then `inputChanges.getFileChanges(theProperty)` yields `FileChange` records carrying a `ChangeType` (ADDED/MODIFIED/REMOVED) and the file. You author one when per-file work is expensive (code generation, transpiling, image processing) and the input set is large but usually only a few files change between runs, so processing the delta is far cheaper than reprocessing everything.
code
kotlin · 17 linesabstract class Transpile : DefaultTask() {
@get:Incremental
@get:InputDirectory
abstract val sources: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun run(changes: InputChanges) {
changes.getFileChanges(sources).forEach { c ->
val out = outputDir.file(c.normalizedPath).get().asFile
if (c.changeType == ChangeType.REMOVED) out.delete()
else transpile(c.file, out)
}
}
}go deeper
Know that it processes only changed files and needs an InputChanges parameter; rough idea is enough.
Explain @Incremental + InputChanges + getFileChanges + ChangeType, and why per-file deltas beat full reruns for expensive work.
Stress the isIncremental/all-ADDED fallback, deterministic input→output mapping, and handling REMOVED to avoid stale outputs.
Frame when incrementality is worth the complexity vs. simpler UP-TO-DATE, and the maintenance cost of correct delta handling across a plugin used org-wide.
## The two layers of "incremental" Gradle's incremental build at the **task** level is coarse: Gradle snapshots each task's declared inputs and outputs. If nothing changed, the task is `UP-TO-DATE` and skipped entirely; if anything changed, the **entire** task action runs again. That is great when the task is cheap or when changing one input genuinely invalidates all output. An **incremental task** is finer-grained: it lets the task action see *which specific input files changed* so it can reprocess only those and leave the rest of its outputs alone. This is a property of the task's *implementation*, not a separate task type. ## The API Two pieces wire it up: 1. Mark the file input property with `@Incremental` (from `org.gradle.work.Incremental`). This tells Gradle to track per-file changes for that property. 2. Add an `InputChanges` parameter to the `@TaskAction` method. Gradle injects it. Inside the action you call `inputChanges.getFileChanges(inputProperty)` which returns an `Iterable<FileChange>`. Each `FileChange` exposes `getFile()`, `getChangeType()` (an enum `ChangeType.ADDED`, `MODIFIED`, or `REMOVED`), `getNormalizedPath()`, and `getFileType()`. ## The isIncremental fallback — the part people forget `inputChanges.isIncremental` is `false` whenever Gradle **cannot** provide a reliable delta — first run, output directory missing, a non-`@Incremental` input changed, the task's code changed, the build cache served a different result, etc. When it is `false`, `getFileChanges` reports **every** input file as `ADDED`. Your code must therefore treat a non-incremental run as a full rebuild. The robust pattern is: on `MODIFIED`/`ADDED` regenerate the corresponding output; on `REMOVED` delete the stale output. You usually do **not** need to special-case `isIncremental` separately, because all-ADDED already produces a full rebuild — but you should clear the output directory yourself when you cannot map inputs to outputs deterministically. ## Worked example ```kotlin abstract class Transpile : DefaultTask() { @get:Incremental @get:PathSensitive(PathSensitivity.RELATIVE) @get:InputDirectory abstract val sources: DirectoryProperty @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun execute(inputChanges: InputChanges) { inputChanges.getFileChanges(sources).forEach { change -> if (change.fileType == FileType.DIRECTORY) return@forEach val target = outputDir.file(change.normalizedPath).get().asFile when (change.changeType) { ChangeType.REMOVED -> target.delete() else -> transpile(change.file, target) // ADDED or MODIFIED } } } } ``` Key correctness rules: only ask `getFileChanges` for a property annotated `@Incremental`; map each input deterministically to its output (so REMOVED can delete the right thing); and never assume the delta is non-empty or that you are on an incremental run.
- When does Gradle decide it cannot give you an incremental delta?First execution, missing/changed output dir, a non-@Incremental input changing, task implementation change, or a different build-cache hit. Then isIncremental is false and every file is reported as ADDED.
- Why must you handle REMOVED explicitly?Because Gradle does not delete your outputs for you. If a source file is deleted you must delete its generated output, or you'll leave stale files that pollute the output directory and break reproducibility.
Like a translator who, instead of re-translating the whole book each edition, only retranslates the paragraphs the author actually edited — and crosses out the ones that were deleted.
saying these in an interview costs you the question
- Saying an incremental task is a special task type rather than an implementation pattern.
- Assuming getFileChanges only ever returns the changed subset — it returns all-ADDED on non-incremental runs.
- Forgetting to delete outputs for REMOVED inputs.