skip to content

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%

answer

  1. @InputArtifactDependencies FileCollection
  2. transitive deps, already transformed
  3. second injected property
  4. isolation: no project state access
  5. niche — instrumentation use case

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().

solid answer

~40 s

Beyond `@InputArtifact`, a transform can declare a property annotated `@InputArtifactDependencies` (a `FileCollection`). Gradle injects the **transitive dependencies** of the artifact currently being transformed — and importantly, those dependencies are themselves already transformed to the same target form. This lets a transform that needs context (for example, a bytecode instrumenter that must resolve symbols against the artifact's dependencies) see them without re-resolving anything. Like `@InputArtifact`, the property is abstract and injected. Because it changes what the action reads, it also participates in the cache key; you annotate it for normalization (e.g. `@Classpath`) when the transform is cacheable. It's a niche feature — most transforms only need the single input artifact — but it's the sanctioned way to get dependency context rather than reaching into project state, which transforms must never do.

code

kotlin · 8 lines
kotlin
@get:InputArtifactDependencies
@get:Classpath
abstract val dependencies: FileCollection

override fun transform(outputs: TransformOutputs) {
    val deps = dependencies.files // transformed transitive deps
    process(inputArtifact.get().asFile, deps, outputs.dir("out"))
}

go deeper

for a junior

Likely unaware of this; fine to not know at this level.

for a middle

May recall that dependency context can be injected but not the exact annotation.

for a senior

Names @InputArtifactDependencies, its FileCollection type, and that deps arrive transformed.

for a principal

Frames it around transform isolation guarantees and the cost/cache-key implications of pulling in dependency context.

## The need for dependency context Most transforms operate on a single artifact in isolation: unzip a jar, minify a file. But some genuinely need the artifact's *dependencies* — e.g. an instrumentation transform that rewrites bytecode and must see the classes the input depends on to resolve type hierarchies. ## `@InputArtifactDependencies` Gradle injects them via a second managed property: ```kotlin abstract class Instrument : TransformAction<TransformParameters.None> { @get:InputArtifact @get:Classpath abstract val inputArtifact: Provider<FileSystemLocation> @get:InputArtifactDependencies @get:Classpath abstract val dependencies: FileCollection override fun transform(outputs: TransformOutputs) { val classpath = dependencies.files // the transformed deps instrument(inputArtifact.get().asFile, classpath, outputs.dir("out")) } } ``` Key properties of this injection: - It is a `FileCollection` of the artifact's **transitive dependencies**, already transformed into the **same target attributes** as the current chain. So if you're transforming jars-to-instrumented-classes, the dependencies arrive already instrumented too (or at least in the requested form), giving a consistent view. - It is abstract and injected, just like `@InputArtifact`. - It contributes to the cache key, so for a `@CacheableTransform` you annotate it with the appropriate normalization (`@Classpath` is typical for classpath context). ## Why this matters architecturally Transforms run during resolution and are **isolated**: they may not read arbitrary project state, mutable configuration, or the build model — that would break correctness, caching, and parallelism. `@InputArtifactDependencies` is the *only* sanctioned channel for dependency context. If a transform tries to re-resolve a configuration itself to find dependencies, it violates isolation and risks deadlocks or non-determinism. ## When you don't need it The vast majority of transforms never declare it — they're context-free per artifact. Reach for it only when the transformation logic genuinely cannot proceed without the dependency set (instrumentation, linkage analysis, certain code generation). Adding it unnecessarily widens the cache key and forces resolving/transforming dependencies you don't use.

  • Are the injected dependencies raw or transformed?
    They arrive already transformed into the same target form as the current chain, giving the action a consistent view of its dependency context.
  • Why can't the transform just resolve a configuration itself to get dependencies?
    Transforms are isolated and must not read project/build state; doing so breaks determinism, caching, and parallelism. @InputArtifactDependencies is the sanctioned channel.
  • Does declaring it affect caching?
    Yes — it's an input, so it's part of the cache key; annotate it (e.g. @Classpath) for proper normalization on cacheable transforms.

saying these in an interview costs you the question

  • Re-resolving a configuration inside transform() to find dependencies instead of using @InputArtifactDependencies.
  • Assuming the dependencies are untransformed/raw.
  • Adding @InputArtifactDependencies when the transform doesn't actually need dependency context.

context