# Artifact Transforms

version 9.5.0-20260401020245+0000

What if you want to make changes to the files contained in one of your dependencies before you use it?

For example, you might want to unzip a compressed file, adjust the contents of a JAR, or delete unnecessary files from a dependency that contains multiple files prior to using the result in a task.

Gradle has a built-in feature for this called Artifact Transforms. With Artifact Transforms, you can modify, add to, remove from the set files (or artifacts) - like JAR files - contained in a dependency. This is done as the last step when resolving artifacts, before tasks or tools like the IDE can consume the artifacts.

## Artifact Transforms Overview

Each component exposes a set of variants, where each variant is identified by a set of attributes (i.e., key-value pairs such as `debug=true`).

When Gradle resolves a configuration, it looks at each dependency, resolves it to a component, and selects the corresponding variant from that component that matches the requested attributes. The variant contains one or more artifacts, which represent the concrete outputs produced by a component (such as JAR files, resources, or native binaries). However, a consumer may need artifacts in a format that doesn’t directly match any available variant. Rather than requiring producers to explicitly publish every possible variant, Gradle provides a powerful mechanism to dynamically adapt artifacts to the required form.

Artifact Transforms are a mechanism for converting one type of artifact into another during the build process. They provide the consumer an efficient and flexible mechanism for transforming the artifacts of a given producer to the required format without needing the producer to expose variants in that format.

Artifact Transforms are a lot like tasks. They are units of work with some inputs and outputs. Mechanisms like `UP-TO-DATE` and caching work for transforms as well.

The primary difference between tasks and transforms is how they are scheduled and put into the chain of actions Gradle executes when a build configures and runs. At a high level, transforms always run before tasks because they are executed during dependency resolution. Transforms modify artifacts BEFORE they become an input to a task.

Here’s a brief overview of how to create and use Artifact Transforms:

1. Implement a Transform: You define an artifact transform by creating a class that implements the `TransformAction` interface. This class specifies how the input artifact should be transformed into the output artifact.

2. Declare request Attributes: Attributes (key-value pairs used to describe different variants of a component) like `org.gradle.usage=java-api` and `org.gradle.usage=java-runtime` are used to specify the desired artifact format or type.

3. Register a Transform: You register the transform by using the `registerTransform()` method of the `dependencies` block. This method tells Gradle that a transform can be used to modify the artifacts of any variant that possesses the given "from" attributes. It also tells Gradle what new set of "to" attributes will describe the format or type of the resulting artifacts.

4. Use the Transform: When a resolution requires an artifact that isn’t already present in the selected component (because none of the actual artifact possess compatible attributes to the requested attributes), Gradle doesn’t just give up! Instead, Gradle first automatically searches all registered transforms to see if it can construct a chain of transformations that will ultimately produce a match. If Gradle finds such a chain, it then runs each transform in sequence, and delivers the transformed artifacts as a result.

### 1. Implement a Transform

A transform is typically written as an abstract class that implements the `TransformAction` interface. It can optionally have parameters defined in a separate interface.

Each transform has exactly one input artifact. It must be annotated with the `@InputArtifact` annotation.

Then, you implement the `transform(TransformOutputs)` method from the `TransformAction` interface. This method’s implementation defines what the transform should do when triggered. The method has a `TransformOutputs` parameter that you use to tell Gradle what artifacts the transform produces.

Here, `MyTransform` is the custom transform action that converts a `jar` artifact to a `transformed-jar` artifact:

build.gradle.kts

    abstract class MyTransform : TransformAction<TransformParameters.None> {
        @get:InputArtifact
        abstract val inputArtifact: Provider<FileSystemLocation>

        override fun transform(outputs: TransformOutputs) {
            val inputFile = inputArtifact.get().asFile
            val outputFile = outputs.file(inputFile.name.replace(".jar", "-transformed.jar"))
            // Perform transformation logic here
            inputFile.copyTo(outputFile, overwrite = true)
        }
    }

build.gradle

    abstract class MyTransform implements TransformAction<TransformParameters.None> {
        @InputArtifact
        abstract Provider<FileSystemLocation> getInputArtifact()

        @Override
        void transform(TransformOutputs outputs) {
            def inputFile = inputArtifact.get().asFile
            def outputFile = outputs.file(inputFile.name.replace(".jar", "-transformed.jar"))
            // Perform transformation logic here
            inputFile.withInputStream { input ->
                outputFile.withOutputStream { output ->
                    output << input
                }
            }
        }
    }

### 2. Declare request Attributes

Attributes specify the required properties of a dependency.

Here we specify that we need the `transformed-jar` format for the `runtimeClasspath` configuration:

build.gradle.kts

    configurations.named("runtimeClasspath") {
        attributes {
            attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
        }
    }

build.gradle

    configurations.named("runtimeClasspath") {
        attributes {
            attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
        }
    }

### 3. Register a Transform

A transform must be registered using the `dependencies.registerTransform()` method.

Here, our transform is registered with the `dependencies` block:

build.gradle.kts

    dependencies {
        registerTransform(MyTransform::class) {
            from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
            to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
        }
    }

build.gradle

    dependencies {
        registerTransform(MyTransform) {
            from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
            to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "transformed-jar")
        }
    }

"To" attributes are used to describe the format or type of the artifacts that this transform can use as an input, and "from" attributes to describe the format or type of the artifacts that it produces as an output.

### 4. Use the Transform

During a build, Gradle automatically runs registered transforms to satisfy a resolution request if a match is not directly available.

Since no variants exist supplying artifacts of requested format (as none contain the `artifactType` attribute with a value of `"transformed-jar"`), Gradle attempts to construct a chain of transformations that will supply an artifact matching the requested attributes.

Gradle’s search finds `MyTransform`, which is registered as producing the requested format, so it will automatically be run. Running this transform action modifies the artifacts of an existing source variant to produce new artifacts that are delivered to the consumer, in the requested format.

Gradle produces a "virtual artifact set" of the component as part of this process.

## Understanding Artifact Transforms

Dependencies can have different variants, essentially different versions or forms of the same dependency. These variants can each provide a different artifact set, meant to satisfy different use cases, such as compiling code, browsing documentation or running applications.

Each variant is identified by a set of attributes. Attributes are key-value pairs that describe specific characteristics of the variant.

Let’s use the following example where an external Maven dependency has two variants:

Table 1. Maven Dependencies
Variant | Description
--- | ---
`org.gradle.usage=java-api` | Used for compiling against the dependency.
`org.gradle.usage=java-runtime` | Used for running an application that uses the dependency.

And a project dependency has even more variants:

Table 2. Project Dependencies
Variant | Description
--- | ---
`org.gradle.usage=java-api` `org.gradle.libraryelements=classes` | Represents classes directories.
`org.gradle.usage=java-api` `org.gradle.libraryelements=jar` | Represents a packaged JAR file, containing classes and resources.

The variants of a dependency may differ in their transitive dependencies or in the set of artifacts they contain, or both.

For example, the `java-api` and `java-runtime` variants of the Maven dependency only differ in their transitive dependencies, and both use the same artifact — the JAR file. For the project dependency, the `java-api,classes` and the `java-api,jars` variants have the same transitive dependencies but different artifacts — the `classes` directories and the `JAR` files respectively.

When Gradle resolves a configuration, it uses the attributes defined to select the appropriate variant of each dependency. The attributes that Gradle uses to determine which variant to select are called the requested attributes.

For example, if a configuration requests `org.gradle.usage=java-api` and `org.gradle.libraryelements=classes`, Gradle will select the variant of each dependency that matches these attributes (in this case, classes directories intended for use as an API during compilation). Matches do not have to exact, as some attribute values can be identified to Gradle as compatible with other values and used interchangeably during matching.

Sometimes, a dependency might not have a variant with attributes that match the requested attributes. In such cases, Gradle can transform one variant’s artifacts into another "virtual artifact set" by modifying its artifacts without changing its transitive dependencies.

| Gradle will not attempt to select or run Artifact Transforms when a variant of the dependency matching the requested attributes already exists.

For example, if the requested variant is `java-api,classes`, but the dependency only has `java-api,jar`, Gradle can potentially transform the `JAR` file into a `classes` directory by unzipping it using an Artifact Transform that is registered with these attributes.

| Gradle applies transformations to artifacts, not variants.

## Executing Artifact Transforms

Gradle automatically selects Artifact Transforms as needed to satisfy resolution requests.

To run an Artifact Transform, you can configure a custom Artifact View to request an artifact set that is not exposed by any variant of the target component.

When resolving the `ArtifactView` Gradle will search for appropriate Artifact Transforms based on the requested attributes in the view. Gradle will run these transformations on the original artifacts found in a variant of the target component to produce a result compatible with the attributes in the view.

In the example below, the `TestTransform` class defines a transformation that is registered to process artifacts of type "jar" into artifacts of type "stub":

build.gradle.kts

    // The TestTransform class implements TransformAction,
    // transforming input JAR files into text files with specific content
    abstract class TestTransform : TransformAction<TransformParameters.None> {
        @get:InputArtifact
        abstract val inputArtifact: Provider<FileSystemLocation>

        override fun transform(outputs: TransformOutputs) {
            val outputFile = outputs.file("transformed-stub.txt")
            outputFile.writeText("Transformed from ${inputArtifact.get().asFile.name}")
        }
    }

    // The transform is registered to convert artifacts from the type "jar" to "stub"
    dependencies {
        registerTransform(TestTransform::class.java) {
            from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
            to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "stub")
        }
    }

    dependencies {
        runtimeOnly("com.github.javafaker:javafaker:1.0.2")
    }

    // The testArtifact task queries and prints the attributes of resolved artifacts,
    // showing the type conversion in action.
    tasks.register("testArtifact") {
        val resolvedArtifacts = configurations.runtimeClasspath.get().incoming.artifactView {
            attributes {
                attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "stub")
            }
        }.artifacts
        inputs.files(resolvedArtifacts.artifactFiles)

        doLast {
            resolvedArtifacts.resolvedArtifacts.get().forEach {
                println("Resolved artifact variant:")
                println("- ${it.variant}")
                println("Resolved artifact attributes:")
                println("- ${it.variant.attributes}")
                println("Resolved artifact type:")
                println("- ${it.variant.attributes.getAttribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE)}")
            }
        }
    }

build.gradle

    // The TestTransform class implements TransformAction,
    // transforming input JAR files into text files with specific content
    abstract class TestTransform implements TransformAction<TransformParameters.None> {
        @InputArtifact
        abstract Provider<FileSystemLocation> getInputArtifact()

        @Override
        void transform(TransformOutputs outputs) {
            def outputFile = outputs.file("transformed-stub.txt")
            outputFile.text = "Transformed from ${getInputArtifact().get().asFile.name}"
        }
    }

    // The transform is registered to convert artifacts from the type "jar" to "stub"
    dependencies {
        registerTransform(TestTransform) {
            from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "jar")
            to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "stub")
        }
    }

    dependencies {
        runtimeOnly("com.github.javafaker:javafaker:1.0.2")
    }

    // The testArtifact task queries and prints the attributes of resolved artifacts,
    // showing the type conversion in action.
    tasks.register("testArtifact") {
        def resolvedArtifacts = configurations.runtimeClasspath.incoming.artifactView {
            attributes {
                attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "stub")
            }
        }.artifacts
        inputs.files(resolvedArtifacts.artifactFiles)

        doLast {
            resolvedArtifacts.resolvedArtifacts.get().each {
                println "Resolved artifact variant:"
                println "- ${it.variant}"
                println "Resolved artifact attributes:"
                println "- ${it.variant.attributes}"
                println "Resolved artifact type:"
                println "- ${it.variant.attributes.getAttribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE)}"
            }
        }
    }

The `testArtifact` task resolves artifacts of type "stub" using the `runtimeClasspath` configuration. This is achieved by creating an `ArtifactView` that filters for artifacts with `ARTIFACT_TYPE_ATTRIBUTE = "stub"`.

## Understanding Artifact Transforms Chains

When Gradle resolves a configuration and a variant in the graph does not have an artifact set with the requested attributes, it attempts to find a chain of one or more Artifact Transforms that can be run sequentially to create the desired artifact set. This process is called Artifact Transform selection:

### The Artifact Transform Selection Process:

1. Start with requested Attributes:

   - Gradle starts with the attributes specified on the configuration being resolved, appends any attributes specified on an `ArtifactView`, and finally appends any attributes declared directly on the dependency.

   - It considers all registered transforms that modify these attributes.

2. Find a path to existing Variants:

   - Gradle works backwards, trying to find a path from the requested attributes to an existing variant.

For example, if the `minified` attribute has values `true` and `false`, and a transform can change `minified=false` to `minified=true`, Gradle will use this transform if only `minified=false` variants are available but `minified=true` is requested.

Gradle selects a chain of transforms using the following process:

- If there is only one possible chain that produces the requested attributes, it is selected.

- If there are multiple such chains, then only the shortest chains are considered.

- If there are still multiple chains remaining that are equally suitable but produce different results, the selection fails, and an error is reported.

- If all the remaining chains produce the same set of resulting attributes, Gradle arbitrarily selects one.

How can multiple chains produce different suitable results? Transforms can alter multiple attributes at a time. A suitable result of a transformation chain is one possessing attributes compatible with the requested attributes. But a result may contain other attributes as well, that were not requested, and are irrelevant to the result.

For example: if attributes `A=a` and `B=b` are requested, and variant `V1` contains attributes `A=a`, `B=b`, and `C=c`, and variant `V2` contains attributes `A=a`, `B=b`, and `D=d`, then since all the values of `A` and `B` are identical (or compatible) either `V1` or `V2` would satisfy the request.

### A Full Example

Let’s continue exploring the `minified` example begun above: a configuration requests `org.gradle.usage=java-runtime, org.gradle.libraryelements=jar, minified=true`. The dependencies are:

- External `guava` dependency with variants:

  - `org.gradle.usage=java-runtime, org.gradle.libraryelements=jar, minified=false`

  - `org.gradle.usage=java-api, org.gradle.libraryelements=jar, minified=false`

- Project `producer` dependency with variants:

  - `org.gradle.usage=java-runtime, org.gradle.libraryelements=jar, minified=false`

  - `org.gradle.usage=java-runtime, org.gradle.libraryelements=classes, minified=false`

  - `org.gradle.usage=java-api, org.gradle.libraryelements=jar, minified=false`

  - `org.gradle.usage=java-api, org.gradle.libraryelements=classes, minified=false`

Gradle uses the `minify` transform to convert `minified=false` variants to `minified=true`.

- For `guava`, Gradle converts

  - `org.gradle.usage=java-runtime, org.gradle.libraryelements=jar, minified=false` to

  - `org.gradle.usage=java-runtime, org.gradle.libraryelements=jar, minified=true`.

- For `producer`, Gradle converts

  - `org.gradle.usage=java-runtime, org.gradle.libraryelements=jar, minified=false` to

  - `org.gradle.usage=java-runtime, org.gradle.libraryelements=jar, minified=true`.

Then, during execution:

- Gradle downloads the `guava` JAR and runs the transform to minify it.

- Gradle executes the `producer:jar` task to produce the JAR and then runs the transform to minify it.

- These tasks and transforms are executed in parallel where possible.

To set up the `minified` attribute so that the above works you must add the attribute to all JAR variants being produced, and also add it to all resolvable configurations being requested. You should also register the attribute in the attributes schema.

build.gradle.kts

    val artifactType = Attribute.of("artifactType", String::class.java)
    val minified = Attribute.of("minified", Boolean::class.javaObjectType)
    dependencies {
        attributesSchema {
            attribute(minified)                      (1)
        }
        artifactTypes.getByName("jar") {
            attributes.attribute(minified, false)    (2)
        }
    }

    configurations.runtimeClasspath.configure {
        attributes {
            attribute(minified, true)                (3)
        }
    }

    dependencies {
        registerTransform(Minify::class) {
            from.attribute(minified, false).attribute(artifactType, "jar")
            to.attribute(minified, true).attribute(artifactType, "jar")
        }
    }

    dependencies {                                 (4)
        implementation("com.google.guava:guava:27.1-jre")
        implementation(project(":producer"))
    }

    tasks.register<Copy>("resolveRuntimeClasspath") { (5)
        from(configurations.runtimeClasspath)
        into(layout.buildDirectory.dir("runtimeClasspath"))
    }

build.gradle

    def artifactType = Attribute.of('artifactType', String)
    def minified = Attribute.of('minified', Boolean)
    dependencies {
        attributesSchema {
            attribute(minified)                      (1)
        }
        artifactTypes.getByName("jar") {
            attributes.attribute(minified, false)    (2)
        }
    }

    configurations.runtimeClasspath {
        attributes {
            attribute(minified, true)                (3)
        }
    }

    dependencies {
        registerTransform(Minify) {
            from.attribute(minified, false).attribute(artifactType, "jar")
            to.attribute(minified, true).attribute(artifactType, "jar")
        }
    }
    dependencies {                                 (4)
        implementation('com.google.guava:guava:27.1-jre')
        implementation(project(':producer'))
    }

    tasks.register("resolveRuntimeClasspath", Copy) {(5)
        from(configurations.runtimeClasspath)
        into(layout.buildDirectory.dir("runtimeClasspath"))
    }

1 | Add the attribute to the schema
2 | All JAR files are not minified
3 | Request that the runtime classpath is minified
4 | Add the dependencies which will be transformed
5 | Add task that requires the transformed artifacts

You can now see what happens when we run the `resolveRuntimeClasspath` task, which resolves the `runtimeClasspath` configuration. Gradle transforms the project dependency before the `resolveRuntimeClasspath` task starts. Gradle transforms the binary dependencies when it executes the `resolveRuntimeClasspath` task:

    $ ./gradlew resolveRuntimeClasspath

    > Task :producer:compileJava
    > Task :producer:processResources NO-SOURCE
    > Task :producer:classes
    > Task :producer:jar

    > Transform producer.jar (project :producer) with Minify
    Nothing to minify - using producer.jar unchanged

    > Task :resolveRuntimeClasspath
    Minifying guava-27.1-jre.jar
    Nothing to minify - using listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar unchanged
    Nothing to minify - using jsr305-3.0.2.jar unchanged
    Nothing to minify - using checker-qual-2.5.2.jar unchanged
    Nothing to minify - using error_prone_annotations-2.2.0.jar unchanged
    Nothing to minify - using j2objc-annotations-1.1.jar unchanged
    Nothing to minify - using animal-sniffer-annotations-1.17.jar unchanged
    Nothing to minify - using failureaccess-1.0.1.jar unchanged

    BUILD SUCCESSFUL in 0s
    3 actionable tasks: 3 executed

## Implementing Artifact Transforms

Similar to task types, an artifact transform consists of an action and some optional parameters. The major difference from custom task types is that the action and the parameters are implemented as two separate classes.

### Artifact Transforms without Parameters

An artifact transform action is provided by a class implementing TransformAction. Such a class implements the `transform()` method, which converts the input artifacts into zero, one, or multiple output artifacts.

Most Artifact Transforms are one-to-one, so the `transform` method will be used to transform each input artifact contained in the from variant into exactly one output artifact.

The implementation of the artifact transform action needs to register each output artifact by calling TransformOutputs.dir() or TransformOutputs.file().

You can supply two types of paths to the `dir` or `file` methods:

- An absolute path to the input artifact or within the input artifact (for an input directory).

- A relative path.

Gradle uses the absolute path as the location of the output artifact. For example, if the input artifact is an exploded WAR, the transform action can call `TransformOutputs.file()` for all JAR files in the `WEB-INF/lib` directory. The output of the transform would then be the library JARs of the web application.

For a relative path, the `dir()` or `file()` method returns a workspace to the transform action. The transform action needs to create the transformed artifact(s) at the location of the provided workspace.

The output artifact(s) replace the input artifact(s) in the transformed variant in the order they were registered. For example, if the selected input variant contains the artifacts `lib1.jar`, `lib2.jar`, `lib3.jar`, and the transform action registers a minified output artifact `<artifact-name>-min.jar` for each input artifact, then the transformed configuration will consist of the artifacts `lib1-min.jar`, `lib2-min.jar`, and `lib3-min.jar`.

Here is the implementation of an `Unzip` transform, which unzips a JAR file into a `classes` directory. The `Unzip` transform does not require any parameters:

build.gradle.kts

    abstract class Unzip : TransformAction<TransformParameters.None> {          (1)
        @get:InputArtifact                                                      (2)
        abstract val inputArtifact: Provider<FileSystemLocation>

        override
        fun transform(outputs: TransformOutputs) {
            val input = inputArtifact.get().asFile
            val unzipDir = outputs.dir(input.name + "-unzipped")                (3)
            unzipTo(input, unzipDir)                                            (4)
        }

        private fun unzipTo(zipFile: File, unzipDir: File) {
            // implementation...
        }
    }

build.gradle

    abstract class Unzip implements TransformAction<TransformParameters.None> { (1)
        @InputArtifact                                                          (2)
        abstract Provider<FileSystemLocation> getInputArtifact()

        @Override
        void transform(TransformOutputs outputs) {
            def input = inputArtifact.get().asFile
            def unzipDir = outputs.dir(input.name + "-unzipped")                (3)
            unzipTo(input, unzipDir)                                            (4)
        }

        private static void unzipTo(File zipFile, File unzipDir) {
            // implementation...
        }
    }

1 | Use `TransformParameters.None` if the transform does not use parameters
2 | Inject the input artifact
3 | Request an output location for the unzipped files
4 | Do the actual work of the transform

Note how the implementation uses `@InputArtifact` to inject an artifact to transform into the action class, so that it can be accessed within the `transform` method. This method requests a directory for the unzipped classes by using `TransformOutputs.dir()` and then unzips the JAR file into this directory.

### Artifact Transforms with Parameters

An artifact transform may require parameters, such as a `String` for filtering or a file collection used to support the transformation of the input artifact. To pass these parameters to the transform action, you must define a new type with the desired parameters. This type must implement the marker interface TransformParameters.

The parameters must be represented using managed properties and the parameter type must be a managed type. You can use an interface or abstract class to declare the getters, and Gradle will generate the implementation. All getters need to have proper input annotations, as described in the incremental build annotations table.

Here is the implementation of a `Minify` transform that makes JARs smaller by only keeping certain classes in them. The `Minify` transform requires knowledge of the classes to keep within each JAR, which is provided as an `Map` property within its parameters:

build.gradle.kts

    abstract class Minify : TransformAction<Minify.Parameters> {   (1)
        interface Parameters : TransformParameters {               (2)
            @get:Input
            var keepClassesByArtifact: Map<String, Set<String>>

        }

        @get:PathSensitive(PathSensitivity.NAME_ONLY)
        @get:InputArtifact
        abstract val inputArtifact: Provider<FileSystemLocation>

        override
        fun transform(outputs: TransformOutputs) {
            val fileName = inputArtifact.get().asFile.name
            for (entry in parameters.keepClassesByArtifact) {      (3)
                if (fileName.startsWith(entry.key)) {
                    val nameWithoutExtension = fileName.substring(0, fileName.length - 4)
                    minify(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}-min.jar"))
                    return
                }
            }
            println("Nothing to minify - using ${fileName} unchanged")
            outputs.file(inputArtifact)                            (4)
        }

        private fun minify(artifact: File, keepClasses: Set<String>, jarFile: File) {
            println("Minifying ${artifact.name}")
            // Implementation ...
        }
    }

build.gradle

    abstract class Minify implements TransformAction<Parameters> { (1)
        interface Parameters extends TransformParameters {         (2)
            @Input
            Map<String, Set<String>> getKeepClassesByArtifact()
            void setKeepClassesByArtifact(Map<String, Set<String>> keepClasses)
        }

        @PathSensitive(PathSensitivity.NAME_ONLY)
        @InputArtifact
        abstract Provider<FileSystemLocation> getInputArtifact()

        @Override
        void transform(TransformOutputs outputs) {
            def fileName = inputArtifact.get().asFile.name
            for (entry in parameters.keepClassesByArtifact) {      (3)
                if (fileName.startsWith(entry.key)) {
                    def nameWithoutExtension = fileName.substring(0, fileName.length() - 4)
                    minify(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}-min.jar"))
                    return
                }
            }
            println "Nothing to minify - using ${fileName} unchanged"
            outputs.file(inputArtifact)                            (4)
        }

        private void minify(File artifact, Set<String> keepClasses, File jarFile) {
            println "Minifying ${artifact.name}"
            // Implementation ...
        }
    }

1 | Declare the parameter type
2 | Interface for the transform parameters
3 | Use the parameters
4 | Use the unchanged input artifact when no minification is required

Observe how you can obtain the parameters by `TransformAction.getParameters()` in the `transform()` method. The implementation of the `transform()` method requests a location for the minified JAR by using `TransformOutputs.file()` and then creates the minified JAR at this location.

Remember that the input artifact is a dependency, which may have its own dependencies. Suppose your artifact transform needs access to those transitive dependencies. In that case, it can declare an abstract getter returning a `FileCollection` and annotate it with @InputArtifactDependencies. When your transform runs, Gradle will inject the transitive dependencies into the `FileCollection` property by implementing the getter.

Note that using input artifact dependencies in a transform has performance implications; only inject them when needed.

### Artifact Transforms with Caching

Artifact Transforms can make use of the build cache to store their outputs and avoid rerunning their transform actions when the result is known.

To enable the build cache to store the results of an artifact transform, add the `@CacheableTransform` annotation on the action class.

For cacheable transforms, you must annotate its @InputArtifact property — and any property marked with @InputArtifactDependencies — with normalization annotations such as @PathSensitive.

The following example demonstrates a more complex transform that relocates specific classes within a JAR to a different package. This process involves rewriting the bytecode of both the relocated classes and any classes that reference them (class relocation):

build.gradle.kts

    @CacheableTransform                                                          (1)
    abstract class ClassRelocator : TransformAction<ClassRelocator.Parameters> {
        interface Parameters : TransformParameters {                             (2)
            @get:CompileClasspath                                                (3)
            val externalClasspath: ConfigurableFileCollection
            @get:Input
            val excludedPackage: Property<String>
        }

        @get:Classpath                                                           (4)
        @get:InputArtifact
        abstract val primaryInput: Provider<FileSystemLocation>

        @get:CompileClasspath
        @get:InputArtifactDependencies                                           (5)
        abstract val dependencies: FileCollection

        override
        fun transform(outputs: TransformOutputs) {
            val primaryInputFile = primaryInput.get().asFile
            if (parameters.externalClasspath.contains(primaryInputFile)) {       (6)
                outputs.file(primaryInput)
            } else {
                val baseName = primaryInputFile.name.substring(0, primaryInputFile.name.length - 4)
                relocateJar(outputs.file("$baseName-relocated.jar"))
            }
        }

        private fun relocateJar(output: File) {
            // implementation...
            val relocatedPackages = (dependencies.flatMap { it.readPackages() } + primaryInput.get().asFile.readPackages()).toSet()
            val nonRelocatedPackages = parameters.externalClasspath.flatMap { it.readPackages() }
            val relocations = (relocatedPackages - nonRelocatedPackages).map { packageName ->
                val toPackage = "relocated.$packageName"
                println("$packageName -> $toPackage")
                Relocation(packageName, toPackage)
            }
            JarRelocator(primaryInput.get().asFile, output, relocations).run()
        }
    }

build.gradle

    @CacheableTransform                                                          (1)
    abstract class ClassRelocator implements TransformAction<Parameters> {
        interface Parameters extends TransformParameters {                       (2)
            @CompileClasspath                                                    (3)
            ConfigurableFileCollection getExternalClasspath()
            @Input
            Property<String> getExcludedPackage()
        }

        @Classpath                                                               (4)
        @InputArtifact
        abstract Provider<FileSystemLocation> getPrimaryInput()

        @CompileClasspath
        @InputArtifactDependencies                                               (5)
        abstract FileCollection getDependencies()

        @Override
        void transform(TransformOutputs outputs) {
            def primaryInputFile = primaryInput.get().asFile
            if (parameters.externalClasspath.contains(primaryInput)) {           (6)
                outputs.file(primaryInput)
            } else {
                def baseName = primaryInputFile.name.substring(0, primaryInputFile.name.length - 4)
                relocateJar(outputs.file("$baseName-relocated.jar"))
            }
        }

        private relocateJar(File output) {
            // implementation...
            def relocatedPackages = (dependencies.collectMany { readPackages(it) } + readPackages(primaryInput.get().asFile)) as Set
            def nonRelocatedPackages = parameters.externalClasspath.collectMany { readPackages(it) }
            def relocations = (relocatedPackages - nonRelocatedPackages).collect { packageName ->
                def toPackage = "relocated.$packageName"
                println("$packageName -> $toPackage")
                new Relocation(packageName, toPackage)
            }
            new JarRelocator(primaryInput.get().asFile, output, relocations).run()
        }
    }

1 | Declare the transform cacheable
2 | Interface for the transform parameters
3 | Declare input type for each parameter
4 | Declare a normalization for the input artifact
5 | Inject the input artifact dependencies
6 | Use the parameters

Note the classes to be relocated are determined by examining the packages of the input artifact and its dependencies. Additionally, the transform ensures that packages contained in JAR files on an external classpath are not relocated.

### Incremental Artifact Transforms

Similar to incremental tasks, Artifact Transforms can avoid some work by only processing files that have changed since the last execution. This is done by using the InputChanges interface.

For Artifact Transforms, only the input artifact is an incremental input; therefore, the transform can only query for changes there. To use InputChanges in the transform action, inject it into the action.

For more information on how to use InputChanges, see the corresponding documentation for incremental tasks.

Here is an example of an incremental transform that counts the lines of code in Java source files:

build.gradle.kts

    abstract class CountLoc : TransformAction<TransformParameters.None> {

        @get:Inject                                                         (1)
        abstract val inputChanges: InputChanges

        @get:PathSensitive(PathSensitivity.RELATIVE)
        @get:InputArtifact
        abstract val input: Provider<FileSystemLocation>

        override
        fun transform(outputs: TransformOutputs) {
            val outputDir = outputs.dir("${input.get().asFile.name}.loc")
            println("Running transform on ${input.get().asFile.name}, incremental: ${inputChanges.isIncremental}")
            inputChanges.getFileChanges(input).forEach { change ->          (2)
                val changedFile = change.file
                if (change.fileType != FileType.FILE) {
                    return@forEach
                }
                val outputLocation = outputDir.resolve("${change.normalizedPath}.loc")
                when (change.changeType) {
                    ChangeType.ADDED, ChangeType.MODIFIED -> {

                        println("Processing file ${changedFile.name}")
                        outputLocation.parentFile.mkdirs()

                        outputLocation.writeText(changedFile.readLines().size.toString())
                    }
                    ChangeType.REMOVED -> {
                        println("Removing leftover output file ${outputLocation.name}")
                        outputLocation.delete()
                    }
                }
            }
        }
    }

build.gradle

    abstract class CountLoc implements TransformAction<TransformParameters.None> {

        @Inject                                                             (1)
        abstract InputChanges getInputChanges()

        @PathSensitive(PathSensitivity.RELATIVE)
        @InputArtifact
        abstract Provider<FileSystemLocation> getInput()

        @Override
        void transform(TransformOutputs outputs) {
            def outputDir = outputs.dir("${input.get().asFile.name}.loc")
            println("Running transform on ${input.get().asFile.name}, incremental: ${inputChanges.incremental}")
            inputChanges.getFileChanges(input).forEach { change ->          (2)
                def changedFile = change.file
                if (change.fileType != FileType.FILE) {
                    return
                }
                def outputLocation = new File(outputDir, "${change.normalizedPath}.loc")
                switch (change.changeType) {
                    case ADDED:
                    case MODIFIED:
                        println("Processing file ${changedFile.name}")
                        outputLocation.parentFile.mkdirs()

                        outputLocation.text = changedFile.readLines().size()

                    case REMOVED:
                        println("Removing leftover output file ${outputLocation.name}")
                        outputLocation.delete()

                }
            }
        }
    }

1 | Inject `InputChanges`
2 | Query for changes in the input artifact

This transform will only run on source files that have changed since the last run, as otherwise the line count would not need to be recalculated.

## Registering Artifact Transforms

You need to register the artifact transform actions, providing parameters if necessary so that they can be selected when resolving dependencies.

To register an artifact transform, you must use registerTransform() within the `dependencies {}` block.

There are a few points to consider when using `registerTransform()`:

- At least one `from` and `to` attributes are required.

- Each `to` attribute must have a corresponding `from` attribute.

- Additional `from` attributes can be included which do not have corresponding `to` attributes.

- The transform action itself can have configuration options. You can configure them with the `parameters {}` block.

- You must register the transform on the project that has the configuration that will be resolved.

- You can supply any type implementing TransformAction to the `registerTransform()` method.

For example, imagine you want to unpack some dependencies and put the unpacked directories and files on the classpath. You can do so by registering an artifact transform action of type `Unzip`, as shown here:

build.gradle.kts

    dependencies {
        registerTransform(Unzip::class.java) {
            from.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named<LibraryElements>(LibraryElements.JAR))
            from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE)
            to.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named<LibraryElements>(LibraryElements.CLASSES_AND_RESOURCES))
            to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE)
        }
    }

build.gradle

    dependencies {
        registerTransform(Unzip) {
            from.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR))
            from.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE)
            to.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.CLASSES_AND_RESOURCES))
            to.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE)
        }
    }

Another example is that you want to minify JARs by only keeping some `class` files from them. Note the use of the `parameters {}` block to provide the classes to keep in the minified JARs to the `Minify` transform:

build.gradle.kts

    val artifactType = Attribute.of("artifactType", String::class.java)
    val minified = Attribute.of("minified", Boolean::class.javaObjectType)
    val keepPatterns = mapOf(
        "guava" to setOf(
            "com.google.common.base.Optional",
            "com.google.common.base.AbstractIterator"
        )
    )

    dependencies {
        registerTransform(Minify::class) {
            from.attribute(minified, false).attribute(artifactType, "jar")
            to.attribute(minified, true).attribute(artifactType, "jar")

            parameters {
                keepClassesByArtifact = keepPatterns
            }
        }
    }

build.gradle

    def artifactType = Attribute.of('artifactType', String)
    def minified = Attribute.of('minified', Boolean)
    def keepPatterns = [
        "guava": [
            "com.google.common.base.Optional",
            "com.google.common.base.AbstractIterator"
        ] as Set
    ]

    dependencies {
        registerTransform(Minify) {
            from.attribute(minified, false).attribute(artifactType, "jar")
            to.attribute(minified, true).attribute(artifactType, "jar")

            parameters {
                keepClassesByArtifact = keepPatterns
            }
        }
    }

## Executing Artifact Transforms

On the command line, Gradle runs tasks; not Artifact Transforms: `./gradlew build.` So how and when does it run transforms?

There are two ways Gradle executes a transform:

1. Artifact Transforms execution for project dependencies can be discovered ahead of task execution and therefore can be scheduled before the task execution.

2. Artifact Transforms execution for external module dependencies cannot be discovered ahead of task execution and, therefore are scheduled inside the task execution.

In well-declared builds, project dependencies can be fully discovered during task configuration ahead of task execution scheduling. If the project dependency is badly declared (e.g., missing a task input), the transform execution will happen inside the task.

It’s important to remember that Artifact Transforms:

- will only ever be run if no matching variants exist to satisfy a request

- can be run in parallel

- will not be rerun if possible (if multiple resolution requests require the same transform to be executed on the same artifacts, and the transform is cacheable, the transform will only be run once and the results fetched from the cache on each subsequent request)

| `TransformAction`s are only instantiated and run if input artifacts exist. If there are no artifacts present in an input variant to a transform, that transform will be skipped. This can happen in the middle of a chain of actions, resulting in all subsequent transforms being skipped.

## Debugging Artifact Transforms

You can use the `artifactTransforms` task to inspect the artifact transforms registered in your build. This task makes it easier to debug unexpected transform behavior, understand which transforms are applied, and see how artifacts flow through your build.

To generate the report, run:

    $ ./gradlew artifactTransforms

This will produce output listing all registered transforms, including details such as:

- The input and output artifact types for each transform

- Attributes required for the transform to apply

- Implementation details of custom transforms

This report is especially helpful if you encounter issues with artifact selection or resolution errors related to transforms.

It’s important to note that the `artifactTransforms` task operates on a single project. If you run it on a subproject, it will report the transforms registered only in that specific project.

For example, to view the artifact transforms in the `app` subproject, you would run:

    $ ./gradlew :app:artifactTransforms

    > Task :app:artifactTransforms

    --------------------------------------------------
    CopyTransform
    --------------------------------------------------
    Type: dagger.hilt.android.plugin.transform.CopyTransform
    Cacheable: No
    From Attributes:
        - artifactType = android-classes
    To Attributes:
        - artifactType = jar-for-dagger

    --------------------------------------------------
    CopyTransform
    --------------------------------------------------
    Type: dagger.hilt.android.plugin.transform.CopyTransform
    Cacheable: No
    From Attributes:
        - artifactType = directory
    To Attributes:
        - artifactType = jar-for-dagger

    --------------------------------------------------
    AggregatedPackagesTransform
    --------------------------------------------------
    Type: dagger.hilt.android.plugin.transform.AggregatedPackagesTransform
    Cacheable: Yes
    From Attributes:
        - artifactType = jar-for-dagger
    To Attributes:
        - artifactType = aggregated-jar-for-hilt

    Some artifact transforms are not cacheable.  This can have negative performance impacts.

Because the task only inspects the current project, running it on the root project, where no transforms are registered, will produce empty output:

    $ ./gradlew artifactTransforms

    > Task :artifactTransforms

    There are no transforms registered in root project 'gradle'.