What if you want to adjust the JAR file of one of your dependencies before you use it?

Gradle has a built-in feature for this called Artifact Transforms. With Artifact Transforms, you can modify, extend, or reduce artifacts like JAR files before tasks or tools like the IDE use them.

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 configuration’s request attributes. If the component does not have a matching variant, resolution fails unless Gradle finds an Artifact Transform chain that can transform one of the component’s variants' artifacts to satisfy the request attributes (without changing its transitive dependencies).

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 transform 2

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.

artifact transform 1

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:

artifact transform 3
  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/type.

  3. Register a Transform: You register the transform in your build script using the registerTransform() method of the dependencies block. This method links the input attributes to the output attributes and associates them with the transform action class.

  4. Use the Transformed Artifacts: When a resolution requires an artifact matching the transform’s output attributes, Gradle automatically applies the registered transform to the input artifact and provides the transformed artifact as a result.

1. Implement a Transform

A transform is usually implemented as an abstract class. The class 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 is where you implement the work the transform should do when triggered. The method has the TransformOutputs as an argument that defines what 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")
    }
}

4. Use the Transformed Artifacts

During a build, Gradle uses registered transforms to produce a required artifact if it’s not directly available.

Understanding Artifact Transforms

Dependencies can have different variants, essentially different versions or forms of the same dependency. These variants can differ based on their use cases, such as when compiling code or running applications.

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

artifact transform 4

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 with 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 its transitive dependencies or in the artifact itself.

For example, the java-api and java-runtime variants of the Maven dependency only differ in the 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).

Sometimes, a dependency might not have the exact variant with the requested attributes. In such cases, Gradle can transform one variant into another without changing its transitive dependencies (other dependencies it relies on).

Gradle does not try to select 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.

Understanding Artifact Transforms Chains

When Gradle resolves a configuration and a dependency does not have a variant with the requested attributes, it attempts to find a chain of Artifact Transforms to create the desired variant. This process is called Artifact Transform selection:

artifact transform 5

Artifact Transform selection:

  1. Start with requested Attributes:

    • Gradle starts with the attributes specified in the configuration.

    • 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 the best chain of transforms based on specific rules:

  • If there is only one chain, it is selected.

  • If one chain is a suffix of another, the more specific chain is selected.

  • The shortest chain is preferred.

  • If multiple chains are equally suitable, the selection fails, and an error is reported.

Continuing from the minified example 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 minifies it.

  • Gradle executes the producer:jar task to produce the JAR and then minifies it.

  • These tasks are executed in parallel where possible.

To set up the minified attribute so that the above works, you need to register the new attribute in the schema, add it to all JAR artifacts, and request it on all resolvable configurations:

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.all {
    afterEvaluate {
        if (isCanBeResolved) {
            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.all {
    afterEvaluate {
        if (canBeResolved) {
            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 minified=true on all resolvable configurations
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:

$ gradle 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

The implementation of the artifact transform action is a class implementing TransformAction. You must implement the transform() method on the action, which converts an input artifact into zero, one, or multiple output artifacts.

Most Artifact Transforms are one-to-one, so the transform method will transform the input artifact 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 at the location of the provided workspace.

The output artifacts replace the input artifact in the transformed variant in the order they were registered. For example, if the configuration consists of the artifacts lib1.jar, lib2.jar, lib3.jar, and the transform action registers a minified output artifact <artifact-name>-min.jar for the input artifact, then the transformed configuration consists 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)                              (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)                              (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 the artifact to transform into the action. It 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 the classes to keep as 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 for their outputs.

To enable the build cache for 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 work by only processing changed files from 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

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

  • The from and to attributes are required.

  • 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
val artifactType = Attribute.of("artifactType", String::class.java)

dependencies {
    registerTransform(Unzip::class) {
        from.attribute(artifactType, "jar")
        to.attribute(artifactType, "java-classes-directory")
    }
}
build.gradle
def artifactType = Attribute.of('artifactType', String)

dependencies {
    registerTransform(Unzip) {
        from.attribute(artifactType, 'jar')
        to.attribute(artifactType, 'java-classes-directory')
    }
}

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 task input), the transform execution will happen inside the task.

It’s important to remember that Artifact Transforms:

  • can be run in parallel

  • are cacheable

  • are reusable (if separate resolutions used by different tasks require the same transform to be executed on the same artifacts, the transform results will be cached and shared)