How does getFileChanges behave when a task has several @Incremental inputs, and how should the action query and respond to changes across them?
answer
- query getFileChanges per @Incremental property
- scoped to that input only
- cross-input deps can force wider rebuild
- non-incremental => all props all ADDED
- only annotated inputs are queryable
basics
~10 sCall getFileChanges once per @Incremental input property; each call returns only that input's changes. If isIncremental is true you can process each independently; if false, every input reports all files as ADDED.
solid answer
~50 s`getFileChanges` is queried **per input property**: you pass the specific `@Incremental` `FileCollection`/`DirectoryProperty`/`RegularFileProperty`, and it returns only the changes for that property. With multiple incremental inputs you typically call it once for each. On an incremental run, each call returns just the real deltas for its property, so you can update outputs proportionally — but be careful about cross-input dependencies: if your output for file X depends on another input Y, a change to Y must also re-trigger X even though X itself is unchanged. When `isIncremental` is `false`, *every* `getFileChanges` call reports all of that property's files as ADDED, so the action rebuilds wholesale. A subtlety: you should only call `getFileChanges` for properties annotated `@Incremental`; querying a non-incremental input is not supported and a change to any non-`@Incremental` input forces the whole run non-incremental anyway. Design-wise, keep incremental inputs independent where possible so partial reruns stay correct.
code
kotlin · 5 lineschanges.getFileChanges(sources).forEach { handleSource(it) }
changes.getFileChanges(headers).forEach { h ->
// shared header changed -> may need to reprocess dependent sources
if (h.changeType != ChangeType.REMOVED) reprocessSourcesDependingOn(h)
}go deeper
Generally beyond scope; know you query per input property.
Explain that each getFileChanges call is scoped to one @Incremental input and the non-incremental uniform behavior.
Reason about cross-input dependencies and strategies to keep partial reruns correct.
Decide when multi-input incrementality is worth the maintenance cost versus clean rebuild plus caching, and set team patterns accordingly.
## Per-property querying `InputChanges.getFileChanges(...)` takes the actual input you want changes for. So with two tracked inputs you call it twice: ```kotlin abstract class Compile : DefaultTask() { @get:Incremental @get:InputFiles abstract val sources: ConfigurableFileCollection @get:Incremental @get:InputFiles abstract val headers: ConfigurableFileCollection @get:OutputDirectory abstract val out: DirectoryProperty @TaskAction fun run(changes: InputChanges) { if (!changes.isIncremental) out.get().asFile.deleteRecursively() changes.getFileChanges(sources).forEach { handleSource(it) } changes.getFileChanges(headers).forEach { handleHeader(it) } } } ``` Each call is scoped to its property; `sources` changes never appear in the `headers` result and vice versa. ## The cross-input dependency trap Incremental correctness assumes each changed input maps to a bounded, knowable set of outputs. When outputs have **cross-input dependencies** — e.g. every `.c` source output depends on a shared `config.h` header — a change to that header invalidates *all* source outputs even though the sources themselves didn't change. Naively reacting only to the `sources` delta (empty) would skip the needed rebuild. Handling options: - Treat a change in the shared input as a trigger for a fuller rebuild (e.g. if any `headers` change, reprocess all sources). This sacrifices some granularity for correctness. - Maintain your own fine-grained dependency map and reprocess exactly the affected outputs. - If dependencies are pervasive, consider whether incrementality is worth the complexity versus a clean per-run rebuild plus the build cache. ## Non-incremental mode is uniform When `isIncremental == false`, *all* `getFileChanges` calls report every file of their property as `ADDED`. So a loop-over-changes-and-regenerate structure naturally does a full rebuild for all inputs — provided you've cleared outputs first. ## Constraints to remember - Only query properties annotated `@Incremental`. Passing a non-incremental input is unsupported. - A change to *any* non-`@Incremental` declared input forces the entire run non-incremental — so adding many untracked inputs erodes the benefit. - Keep the number and volatility of tracked inputs deliberate; each adds a delta you must correctly fold into output maintenance. ## When to bother Multiple independent incremental inputs shine for large file-set processors (code generators, asset pipelines, native compilation) where per-file work is expensive and inputs change in small batches. If inputs are highly interdependent or small, the simpler single-input or non-incremental approach plus the build cache is often a better trade.
- Can you call getFileChanges on an input not annotated @Incremental?No — only @Incremental inputs are queryable; doing otherwise is unsupported, and any change to a non-@Incremental input forces a non-incremental run anyway.
- Sources are unchanged but a shared header changed. Why might you still rebuild source outputs?Because each source's output depends on the header; the sources delta is empty, so you must use the header change to trigger reprocessing the dependent outputs.
saying these in an interview costs you the question
- Assuming one getFileChanges call returns changes across all inputs.
- Ignoring cross-input dependencies, producing stale outputs when a shared input changes but the per-file input did not.