skip to content

How do you expose a ConfigurableFileCollection as a managed @InputFiles property on a custom task instead of an eager FileCollection?

level: middleimportance: must knowfreq 48%

answer

  1. abstract val : ConfigurableFileCollection
  2. @get:InputFiles + @PathSensitive
  3. Gradle injects the instance
  4. callers use from()/setFrom()
  5. lazy → up-to-date + config cache

basics

~10 s

Declare an abstract getter returning ConfigurableFileCollection, annotated with @InputFiles. Gradle manages the instance for you, so you never create or assign it — callers just use from() to set sources.

solid answer

~40 s

On a custom task, declare the input as an **abstract** read-only getter whose type is `ConfigurableFileCollection`, annotated with `@InputFiles` (and usually a path-sensitivity annotation). Because the getter is abstract, Gradle's managed-property machinery instantiates and injects the collection — you never call `objects.fileCollection()` or assign it yourself. Callers configure it with `task.inputs.from(...)` via the property's `from`/`setFrom`. This is superior to holding an eager `FileCollection`: the value stays lazy, integrates with up-to-date checking and the build/configuration cache, and avoids resolving paths at configuration time. Combine `@InputFiles` with `@PathSensitive(PathSensitivity.RELATIVE)` (or NAME_ONLY) so relocating the project doesn't bust the cache, and use `@SkipWhenEmpty` if the task should skip when there are no inputs.

code

kotlin · 11 lines
kotlin
abstract class BundleTask : DefaultTask() {
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val sources: ConfigurableFileCollection

    @get:OutputFile
    abstract val archive: RegularFileProperty

    @TaskAction
    fun run() = sources.files.forEach { /* bundle it */ }
}

go deeper

for a junior

Know the pattern: abstract ConfigurableFileCollection getter annotated @InputFiles, Gradle creates it.

for a middle

Explain why abstract/managed beats an eager field and pair @InputFiles with @PathSensitive and @SkipWhenEmpty.

for a senior

Discuss cache-key implications, @Classpath vs @InputFiles, and how laziness preserves configuration-cache compatibility.

for a principal

Set authoring standards: all task inputs as managed lazy types with explicit normalization, so the org's custom tasks are reliably cacheable.

## Managed properties Gradle can **manage** a task/extension property for you if you declare it as an `abstract` getter of a supported lazy type. For files, that type is `ConfigurableFileCollection`. You don't write a field, a constructor, or a factory call — Gradle generates the implementation and injects an instance backed by the `ObjectFactory`. ```kotlin abstract class BundleTask : DefaultTask() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sources: ConfigurableFileCollection @get:OutputFile abstract val archive: RegularFileProperty @TaskAction fun run() { sources.files.forEach { /* ... */ } } } ``` Usage: ```kotlin tasks.register<BundleTask>("bundle") { sources.from("src/main/resources") sources.from(layout.buildDirectory.dir("generated")) archive.set(layout.buildDirectory.file("out/bundle.zip")) } ``` ## Why not an eager FileCollection field? If you wrote `val sources: FileCollection = project.files(...)` in a constructor, you'd: - resolve sources too early (breaking the configuration cache and forcing eager evaluation), - couple the task to `Project`, - lose the clean lazy-wiring story. A managed `ConfigurableFileCollection` defers everything and lets Gradle track it for **incremental builds**. ## Annotations that pair with @InputFiles - **`@PathSensitive(PathSensitivity.RELATIVE | NAME_ONLY | NONE)`** — controls which part of file paths feeds into the up-to-date / cache key. RELATIVE is the common, relocatable choice. - **`@SkipWhenEmpty`** — skip the task when the collection has no files. - **`@IgnoreEmptyDirectories`**, **`@InputDirectory`** for the single-dir case. - For classpaths specifically, prefer **`@Classpath`** / **`@CompileClasspath`** which apply ordering/normalization semantics. ## Querying inside the action Inside `@TaskAction`, call `sources.files` (a `Set<File>`), iterate, or use `.asFileTree`. Resolution happens then, at execution time — exactly where you want it. ## Key takeaway Abstract getter + `ConfigurableFileCollection` + `@InputFiles` = a lazy, cache-aware, Gradle-managed input that needs no manual instantiation.

  • Why does the getter have to be abstract for Gradle to manage it?
    Abstract signals to Gradle's bytecode-generation that it should supply the implementation and inject an ObjectFactory-backed instance. A concrete getter would mean you take responsibility for creating and returning the value yourself.
  • What does @PathSensitive(RELATIVE) buy you over the default?
    It makes up-to-date checks and the build cache key depend only on relative paths and content, so moving the project to a different absolute directory (or another machine) still hits the cache instead of re-running the task.
  • When would you choose @Classpath over @InputFiles for a file collection?
    When the files represent a classpath: @Classpath applies classpath normalization (ignores timestamps/order-insensitive jar internals appropriately) so irrelevant changes don't bust the cache, which plain @InputFiles wouldn't handle as well.

saying these in an interview costs you the question

  • Creating the collection with objects.fileCollection() in the task constructor instead of leaving the getter abstract.
  • Using @Input instead of @InputFiles for a file collection.
  • Forgetting path sensitivity, so absolute-path changes needlessly invalidate the cache.

context