skip to content

How do you correctly produce derived files inside a TransformAction using TransformOutputs, and what rule governs where those files may live?

level: middleimportance: must knowfreq 28%

answer

  1. outputs.file vs outputs.dir
  2. relative path -> managed workspace
  3. absolute File must equal input (identity)
  4. registration order preserved
  5. arbitrary path = error / breaks cache

basics

~20 s

Call outputs.file(name) or outputs.dir(name) to get a location, then write your derived content there. Outputs must be either the input artifact itself or a path under the directory Gradle gives you — never an arbitrary location.

solid answer

~40 s

`TransformOutputs` is the only sanctioned way to declare a transform's results. Two methods matter: - `outputs.file(path)` — registers a regular file output and returns the `File` you write to. - `outputs.dir(path)` — registers a directory output; Gradle creates it and you fill it. The critical rule: an output location must be **either** the input artifact itself (identity/pass-through) **or** a relative path resolved inside the transform's own output directory that Gradle provisions. You cannot point an output at some arbitrary absolute path. If you pass a relative name, Gradle places it under a managed workspace; if you pass an absolute `File`, it must equal the input artifact. This guarantees outputs are content-addressable and cacheable. Order matters too: the order in which you register outputs is the order consumers see them.

code

kotlin · 9 lines
kotlin
override fun transform(outputs: TransformOutputs) {
    val input = inputArtifact.get().asFile
    if (input.name.endsWith("-no-op.jar")) {
        outputs.file(input)          // identity / pass-through
        return
    }
    val dir = outputs.dir("unzipped") // relative -> workspace
    unzipTo(input, dir)
}

go deeper

for a junior

Knows to call outputs.file/outputs.dir and write into the returned File.

for a middle

States the location rule (input itself or relative workspace path) and that multiple outputs are allowed.

for a senior

Connects the workspace-ownership rule to content-addressed caching and build-cache replay.

for a principal

Reasons about transform chains and how identity pass-through interacts with multi-step derivation and cache reuse across modules.

## TransformOutputs in depth `TransformOutputs` is the callback object Gradle hands to `transform()`. Every file or directory your transform produces **must** be announced through it; files you create elsewhere are invisible to resolution and break caching. ### The two registration methods ```kotlin override fun transform(outputs: TransformOutputs) { val input = inputArtifact.get().asFile // a directory output val classesDir = outputs.dir("classes") extractInto(input, classesDir) // a file output val manifest = outputs.file("manifest.txt") manifest.writeText("name=" + input.name) } ``` - `outputs.dir(name)` returns a `File` pointing at a directory Gradle has created for you; populate it. - `outputs.file(name)` returns a `File`; you write the content. ### The location rule An output **must** be one of: 1. **The input artifact itself** — `outputs.file(inputArtifact.get().asFile)`. This is the identity case: the transform passes the artifact through unchanged (common when one transform in a chain is a no-op for certain inputs). 2. **A path relative to the transform's output directory** — when you pass a *relative* name like `"classes"`, Gradle resolves it inside a managed, content-addressed workspace dedicated to this invocation. Passing an absolute `File` that is neither the input nor inside the workspace is an error. The reason is determinism and caching: Gradle must own the output location so it can hash, store, and replay it from the cache. If you wrote to `/tmp/whatever`, Gradle could neither track nor reuse it. ### Ordering and multiplicity A single input artifact can yield **many** outputs — call `file`/`dir` repeatedly. The registration order is preserved and is what downstream consumers iterate. Producing zero outputs filters the artifact away entirely. ### Why this enables caching Because the output lives in a Gradle-owned workspace and the inputs (input artifact content + normalized parameters) are hashed, Gradle stores the result keyed by that hash. The next build — or another project, via the build cache — skips re-running the action and replays the stored outputs. Writing outside the workspace would defeat all of this.

  • Can a single input artifact produce multiple outputs?
    Yes — call outputs.file/dir as many times as needed; the registration order is what consumers iterate over.
  • What's the one valid absolute-path output?
    The input artifact itself, via outputs.file(inputArtifact.get().asFile) — the identity/pass-through case.
  • Why can't you just write to /tmp and ignore TransformOutputs?
    Gradle wouldn't track, hash, or cache those files; resolution would miss them and the result couldn't be replayed from the build cache.

saying these in an interview costs you the question

  • Writing derived files to a hand-picked absolute path instead of the location TransformOutputs returns.
  • Believing only one output per input is allowed.
  • Thinking outputs.dir requires you to mkdir it yourself — Gradle creates it.

context