skip to content

Artifact Transforms

Artifact transforms, which convert an artifact from one attribute-described form into another on demand during resolution. Interviewers ask because it is Gradle's most advanced dependency feature and it explains how Android processes jars.

on this pageshow

explore

questions

10

How do you actually trigger a registered artifact transform during resolution using an artifactView?

level: middleimportance: must knowfreq 42%

answer

  1. incoming.artifactView { attributes.attribute(...) }
  2. request to-attributes to trigger
  3. view is local, doesn't mutate config
  4. .artifacts / .artifactFiles = transformed
  5. lenient(true), componentFilter

basics

~10 s

Resolve the configuration with an artifactView that requests the transform's to attribute, e.g. configurations.runtimeClasspath.get().incoming.artifactView { attributes.attribute(artifactType, "classes") }.artifacts. Requesting that attribute makes Gradle insert the transform.

solid answer

~40 s

Registration alone does nothing — you trigger a transform by *asking* for the target attributes. The idiomatic way is `incoming.artifactView { ... }`, where you set the attribute(s) that match a transform's `to`: ```kotlin val classes = configurations.runtimeClasspath.get().incoming .artifactView { attributes.attribute(artifactType, "classes") } .artifacts.artifactFiles ``` Gradle resolves the graph normally, then for each artifact whose current attributes don't match what the view requested, it finds a registered transform chain from the artifact's attributes (`from`) to the requested ones (`to`) and inserts it. The view's `.artifacts` (or `.files`) are the *transformed* outputs. Using `artifactView` instead of changing the configuration's own attributes keeps the request local — you get a transformed view without mutating the configuration for everyone else. You can also pass `lenient(true)` to tolerate failures.

code

kotlin · 13 lines
kotlin
val artifactType = Attribute.of("artifactType", String::class.java)

val transformed = configurations.runtimeClasspath.get().incoming
    .artifactView {
        attributes.attribute(artifactType, "classes")
        lenient(true)
    }
    .artifacts
    .artifactFiles

tasks.register("useClasses") {
    inputs.files(transformed)
}

go deeper

for a junior

Know that you request the target attribute via incoming.artifactView { attributes.attribute(...) } and read .artifacts/.files.

for a middle

Explain that the view is local and read-only, doesn't mutate the configuration, and that requesting the to attribute is what inserts the transform.

for a senior

Discuss lenient/componentFilter/withVariantReselection and wiring .artifactFiles lazily into task inputs for cacheability.

for a principal

Reason about exposing transformed views as a stable internal contract and avoiding global attribute mutation that affects unrelated consumers.

## The trigger problem Registering a transform makes a rule *available*; it does not run anything. Something must **request** the `to` attributes for Gradle to insert the transform. The cleanest, most local way to do that is an **artifact view**. ## artifactView Every resolvable configuration exposes `incoming` (a `ResolvableDependencies`). On it, `artifactView { }` builds an `ArtifactView` — an alternative *view* of the resolution result with its own requested attributes: ```kotlin val artifactType = Attribute.of("artifactType", String::class.java) val classDirs = configurations.runtimeClasspath.get().incoming .artifactView { attributes.attribute(artifactType, "classes") } .artifacts // ArtifactCollection of the transformed artifacts .artifactFiles // FileCollection you can wire into a task input ``` Inside the view's `attributes { }` you set exactly the attributes you want the resulting artifacts to have. Gradle then: 1. Resolves the dependency graph using the configuration's normal attributes. 2. For each selected artifact, compares its variant attributes to the view's requested attributes. 3. Where they differ, it searches registered transforms for a chain whose `from` matches the artifact and whose `to` matches the request, and **inserts** it. 4. The view's `.artifacts`/`.files` expose the transformed outputs. ## Why artifactView (not configuration attributes) You *could* set `attributes` directly on the configuration, but that changes the variant selection for every consumer of that configuration. An `artifactView` is **local and read-only** to that one query — ideal when only one task needs the transformed shape (e.g. a task that needs `classes` dirs while everything else uses `jar`s). ## Useful view options - `lenient(true)` — don't fail the whole view if some artifacts can't be resolved/transformed. - `componentFilter { }` — restrict which components are included. - `withVariantReselection()` — allow re-selecting a different variant of a component (needed for some cross-variant transforms). ## Wiring into tasks Because `.artifactFiles` is a lazy `FileCollection`, you wire it straight into a task input (`inputs.files(...)` or a `ConfigurableFileCollection` property). The transform executes only when that input is actually realized, and the output is build-cache-eligible.

  • Why prefer an artifactView over setting attributes on the configuration itself?
    An artifactView is a local, read-only query — it requests transformed artifacts without mutating the configuration's variant selection for every other consumer. Setting attributes on the configuration changes selection globally.
  • What does lenient(true) do on an artifactView?
    It makes the view tolerate resolution/transform failures for individual artifacts rather than failing the entire view, so the remaining artifacts still resolve.

saying these in an interview costs you the question

  • Thinking you must invoke the transform by name — you request the `to` attributes, Gradle picks the transform.
  • Setting attributes on the configuration when only one task needs the transformed shape.
  • Forgetting that the view's `.artifacts`/`.files` are the transformed outputs, not the originals.

context

open as a page

What does it mean to register an artifact transform in Gradle, and what do `from` and `to` describe?

level: middleimportance: must knowfreq 45%

basics

~10 s

You call dependencies.registerTransform(MyTransform) { ... } and declare from.attribute(...) and to.attribute(...). Gradle uses those attributes to know which artifacts the transform can convert and what it produces.

open as a page

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%

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.

open as a page

What is a TransformAction in Gradle, and what are the two essential pieces you must implement when writing one?

level: middleimportance: must knowfreq 35%

basics

~10 s

A TransformAction converts an input artifact into one or more derived output files. You implement the transform(outputs) method and mark the input file with @InputArtifact so Gradle injects it.

open as a page

Where can artifact transforms be registered, and how do they become available to a project or an entire build?

level: middleimportance: should knowfreq 22%

basics

~20 s

You register transforms inside a project's dependencies { registerTransform(...) } block, usually from a plugin's apply. The registration is scoped to that project; to share, apply the plugin (or a convention/settings plugin) to every project that needs it.

open as a page

How does Gradle decide which registered transform to apply, and when does it chain multiple transforms?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Gradle compares an artifact's attributes to the requested attributes. If they differ, it searches registered transforms for a chain whose from matches the source and whose to reaches the request, applying one or several in sequence.

open as a page

Why would you register an artifact transform instead of using a normal task, and what caching benefit does triggering it via attribute matching give?

level: seniorimportance: should knowfreq 26%

basics

~20 s

A transform runs per-artifact, on demand, only when its to attributes are requested, and Gradle caches each result keyed by input + parameters. A task would run once for the whole set and isn't tied into dependency-graph variant matching.

open as a page

How do you make a TransformAction's results cacheable in the build cache, and what determines whether two invocations are considered identical?

level: seniorimportance: should knowfreq 18%

basics

~10 s

Annotate the action class with @CacheableTransform and give the @InputArtifact a @PathSensitive (or @Classpath/@Normalize) annotation. Two invocations match when the input artifact's normalized content and all parameter inputs hash equal.

open as a page

How do you parameterize a TransformAction, and how should parameter properties be annotated so the transform caches correctly?

level: seniorimportance: should knowfreq 22%

basics

~10 s

Define an interface extending TransformParameters with Property/ConfigurableFileCollection fields, annotate them like task inputs (@Input, @InputFiles + @PathSensitive), and reference them via getParameters(). Use TransformParameters.None when there's nothing to configure.

open as a page

Some transforms need not just the artifact but its dependencies. How does a TransformAction access the transitive dependencies of the artifact it is transforming?

level: principalimportance: nice to knowfreq 10%

basics

~10 s

Add a second injected property annotated @InputArtifactDependencies of type FileCollection. Gradle injects the already-transformed dependencies of the current artifact, which you can read inside transform().

open as a page