skip to content

Path Sensitivity Normalization

The path-sensitivity settings that decide whether moving a file counts as a change, and how they widen or destroy cache hits. Asked because absolute-path inputs are the classic reason a remote cache never hits across machines.

on this pageshow

questions

5

What is path sensitivity in Gradle, and why does it matter for up-to-date checks and the build cache?

level: middleimportance: must knowfreq 45%

answer

  1. fingerprint = path view + content hash
  2. ABSOLUTE busts cross-machine cache
  3. RELATIVE strips checkout location
  4. NAME_ONLY ignores directory
  5. least sensitive that's still correct

basics

~20 s

Path sensitivity tells Gradle whether a file input's path (not just its content) is part of the cache key. Choosing RELATIVE or NAME_ONLY instead of ABSOLUTE lets the same content reuse cached results across different machines or directories.

solid answer

~40 s

When Gradle decides whether a task is up-to-date or can be loaded from cache, it computes a fingerprint of each file input. Path sensitivity controls **which part of each file's path** enters that fingerprint. With `@PathSensitive(ABSOLUTE)`, moving the project to a different directory or running on another machine changes every path and busts the cache. With `RELATIVE` only the path relative to the input root matters; with `NAME_ONLY` only the file name; with `NONE` only the content. Choosing the least sensitive option that is still **correct for the task** maximizes up-to-date and cache hits — especially for a shared remote cache where source trees live at different absolute paths on CI vs. developer machines. The trade-off is correctness: if a task genuinely depends on where a file sits, weakening sensitivity produces wrong reuse.

code

kotlin · 6 lines
kotlin
// Annotation form on an input property of a task type you consume/configure
abstract class TransformTask : DefaultTask() {
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val sources: ConfigurableFileCollection
}

go deeper

for a junior

Know that path sensitivity decides whether a file's path is part of the cache key, and that ABSOLUTE hurts cross-machine reuse.

for a middle

Explain all four modes, when each is correct, and the up-to-date vs. remote-cache implications of each.

for a senior

Discuss the correctness boundary — choosing the least-sensitive-but-still-correct mode — and how misuse causes silent wrong reuse.

for a principal

Frame path sensitivity as a lever in an org-wide cache-hit-rate strategy across heterogeneous checkout paths, and the governance needed to prevent unsafe relaxations.

## The problem path sensitivity solves Gradle avoids redoing work by **fingerprinting** task inputs. Before running a task it captures a snapshot of each input file: its identity (some view of its path) plus a content hash. If the new fingerprint equals the last successful run's fingerprint, the task is **UP-TO-DATE** and skipped; if a matching fingerprint exists in the (local or remote) **build cache**, the outputs are unpacked instead of recomputed. The key question is: **does the file's path belong in the fingerprint, and if so, how much of it?** That is path sensitivity. It is declared with `@PathSensitive(PathSensitivity.X)` on a `@InputFiles`/`@InputDirectory`/`@Classpath`-style property, or via runtime normalization rules. ## The four modes - `PathSensitivity.ABSOLUTE` — the full absolute path is part of the key. Any move of the project, or a different checkout location on another machine, changes the key. Almost never what you want for cacheable work; it kills cross-machine cache hits. - `PathSensitivity.RELATIVE` — only the path **relative to the input root** matters. `src/main/java/A.java` fingerprints the same regardless of where the repo is checked out. This is the sensible default for most source/resource inputs. - `PathSensitivity.NAME_ONLY` — only the file **name** matters, not its directory. Two files named `config.txt` in different folders fingerprint identically (given equal content). - `PathSensitivity.NONE` — only **content** matters; path is ignored entirely. Use when the task processes file contents and is indifferent to where they live (e.g. concatenating a set of files). ## Why it changes cache/up-to-date behavior A task is reused only when its complete input fingerprint matches. ABSOLUTE paths embed machine- and checkout-specific data, so a remote cache populated by CI yields **zero** hits for a developer whose repo lives elsewhere. Relaxing to RELATIVE strips that machine-specific noise, so identical content + identical relative layout reuses results. Going further to NAME_ONLY/NONE strips even more, but each relaxation must be **safe** for the task's semantics. ## Correctness boundary The rule of thumb: pick the **least** sensitive mode that still distinguishes inputs the task actually cares about. If renaming or relocating a file would legitimately change the output, you must NOT relax past the level that captures that difference. Over-relaxing produces silent wrong reuse — far worse than a cache miss. ```kotlin // Declaring the consumed input with a normalization tasks.register<MyProcessTask>("process") { // sources fingerprinted by their path relative to the source set root source.from(layout.projectDirectory.dir("src/main/resources")) } ``` For inputs you wire on existing task types, you usually get RELATIVE-ish defaults (e.g. source sets), but when authoring your own consuming wiring you choose the mode that matches reality.

  • Why is ABSOLUTE almost always wrong for a shared remote cache?
    Because the absolute checkout path differs between CI and each developer's machine, so identical content produces different cache keys and you get no cross-machine hits.
  • What's the danger of always picking NONE to maximize hits?
    If the task's output actually depends on file paths or names, NONE makes Gradle reuse stale outputs incorrectly — a correctness bug that's hard to spot.

Think of fingerprinting like labeling moving boxes. ABSOLUTE writes the full street address on every box, so the labels never match after you move. RELATIVE just writes the room name (kitchen, bedroom) — the same regardless of the house. NONE labels only by contents, ignoring rooms entirely.

saying these in an interview costs you the question

  • Claiming path sensitivity changes the content hash itself (it changes which path info joins the hash, not the hashing of bytes).
  • Saying RELATIVE means 'relative to the project root' in all cases — it's relative to the input/source root being fingerprinted.
  • Assuming weaker sensitivity is always safe to flip on for more hits.

context

open as a page

Compare RELATIVE, NAME_ONLY, and NONE path sensitivity. How do you choose the right one for a given file input?

level: seniorimportance: should knowfreq 35%

basics

~20 s

RELATIVE keys on the path relative to the input root; NAME_ONLY keys only on the file name; NONE keys only on content. Pick the loosest one where renaming or moving a file would NOT change the task's correct output.

open as a page

How does runtime classpath normalization help cache hits, and how do you configure it to ignore volatile files inside artifacts?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Runtime classpath normalization lets you tell Gradle to ignore specific files (like build-stamped property files) when fingerprinting a runtime classpath, so a harmless change inside a jar doesn't bust up-to-date or cache for tasks like test.

open as a page

A task you didn't write (e.g. a third-party plugin's task or `test`) keeps missing the remote cache despite no real change. How do you diagnose and fix it via normalization/path sensitivity from the consumer side?

level: principalimportance: should knowfreq 22%

basics

~20 s

Use build-cache debug logging or the build scan to compare input fingerprints between two runs, find which input changed (often an absolute path or a build-stamped file in a jar), then stabilize it with runtime classpath normalization or by adjusting the input's path sensitivity.

open as a page

What does @IgnoreEmptyDirectories do, and when does ignoring empty directories matter for incremental builds?

level: middleimportance: nice to knowfreq 15%

basics

~10 s

@IgnoreEmptyDirectories tells Gradle to leave empty directories out of an input's fingerprint, so stray or environment-created empty folders don't change up-to-date status or the cache key.

open as a page