In an incremental task action, why must you handle ChangeType.REMOVED specially, and what goes wrong if you don't?
answer
- you own output cleanup, not Gradle
- REMOVED input -> delete derived output
- orphaned outputs = clean vs incremental mismatch
- map via normalizedPath
- full-wipe on non-incremental path
basics
~10 sBecause Gradle only tells your action about changed files, you alone must delete outputs for inputs that were removed. Skip it and stale, orphaned output files pile up and pollute later builds.
solid answer
~50 sAn incremental action processes only the files Gradle reports via `getFileChanges`. For ADDED/MODIFIED files you (re)generate outputs; for `ChangeType.REMOVED` you must delete the output that the now-gone input produced. Gradle will not clean it for you, because in an incremental run it never looks at the unchanged majority of files — it trusts your action to maintain the output directory. If you ignore REMOVED, the derived output for a deleted source lingers. That orphan then ships in artifacts, can be picked up by downstream tasks, and produces results that differ from a clean build — a classic 'works after `clean`, fails incrementally' bug. Mapping the input's `normalizedPath` to the corresponding output path and calling `delete()` on REMOVED is the fix. On a non-incremental run (`isIncremental == false`) you typically wipe the whole output directory up front, which sidesteps the REMOVED concern for that path.
code
kotlin · 1 lineChangeType.REMOVED -> outputDir.file(change.normalizedPath).get().asFile.delete()go deeper
Know that removed inputs need their outputs deleted by your code.
Explain the orphaned-output bug and the clean-vs-incremental mismatch it causes.
Discuss why Gradle can't do it (no input->output mapping; skips unchanged files) and the normalizedPath mapping technique.
Connect to reproducibility/determinism guarantees and review practices that catch missing REMOVED handling before it ships.
## The contract you take on When you opt a task into incremental execution by annotating an input `@Incremental` and accepting `InputChanges`, you make a deal with Gradle: 'On an incremental run, just tell me the changed files and I will keep the output directory correct.' Gradle holds up its end by giving you a precise `FileChange` list; you hold up yours by reacting to **all three** change types — including deletions. ## Why Gradle can't clean up for you In an incremental run Gradle deliberately does *not* enumerate or re-snapshot every output mapping; that is the whole point — it avoids touching the unchanged 99%. It has no general knowledge of *which* output file a given input produced (that mapping lives in your task's logic, e.g. `foo.proto` -> `Foo.java`). So only your action can know that deleting `foo.proto` means `Foo.java` must go. Gradle does track your declared `@OutputDirectory`/`@OutputFiles` for *up-to-date* purposes, but it won't surgically remove individual stale entries mid-incremental-run. ## The failure mode ``` Run 1: a.txt, b.txt present -> a.out, b.out generated Delete b.txt Run 2 (incremental): change = REMOVED b.txt - if your action ignores REMOVED: b.out STILL EXISTS ``` `b.out` is now an **orphaned output**. Symptoms: - A packaging/zip task includes a file that should be gone. - A consumer reads stale data. - `./gradlew clean build` produces a *different* result than an incremental build — non-determinism that is painful to debug and undermines reproducibility. ## Correct handling ```kotlin changes.getFileChanges(sources).forEach { change -> val outFile = outputDir.file(change.normalizedPath).get().asFile when (change.changeType) { ChangeType.REMOVED -> outFile.delete() ChangeType.ADDED, ChangeType.MODIFIED -> generate(change.file, outFile) } } ``` Key detail: use the **`normalizedPath`** (the path relative to the root of the input, after normalization) to compute the output location, so the same mapping works for both generation and deletion — including for files reported as REMOVED whose `change.file` no longer exists on disk. ## Interaction with the non-incremental path When `isIncremental == false`, the standard pattern is to delete the entire output directory first and then treat every reported change (all ADDED) as a generate. That full wipe inherently removes orphans, so the REMOVED branch is mainly load-bearing on the *incremental* path. Still, writing the REMOVED branch is mandatory because incremental runs are the common case.
- Why can't Gradle delete the orphaned output automatically?Gradle doesn't know the input-to-output mapping (that's your task's logic), and on an incremental run it intentionally avoids enumerating unchanged files.
- How does the non-incremental branch make REMOVED handling moot?If you delete the whole output directory when isIncremental is false, any orphans vanish; only the incremental path needs the explicit REMOVED deletion.
saying these in an interview costs you the question
- Assuming Gradle prunes stale outputs for incremental tasks automatically.
- Using change.file for REMOVED files — the file no longer exists; derive the output path from normalizedPath instead.