# How to Share Artifacts Between Projects with Gradle

version 8.14.4

Gradle provides a mechanism to share artifacts between projects in a flexible and maintainable way using variant-aware sharing. This allows consuming projects to select the appropriate artifact based on defined attributes, ensuring compatibility and correctness.

## Why Use Variant-Aware Sharing?

Unlike simple project dependencies, variant-aware sharing provides:

- Better encapsulation by exposing only intended artifacts.

- Fine-grained control over artifact selection.

- Support for multiple variants of the same artifact (e.g., debug vs. release builds).

## Step 1: Configuring the Producer Project

To get started, you should check the existing variants of the producer project by running the `outgoingVariants` task:

    $ ./gradlew :producer:outgoingVariants --variant runtimeElements

    > Task :producer:outgoingVariants
    --------------------------------------------------
    Variant runtimeElements
    --------------------------------------------------
    Runtime elements for the 'main' feature.

    Capabilities
    - variant-sharing-example:producer:unspecified (default capability)
    Attributes
    - org.gradle.category            = library
    - org.gradle.dependency.bundling = external
    - org.gradle.jvm.version         = 17
    - org.gradle.libraryelements     = jar
    - org.gradle.usage               = java-runtime
    Artifacts
    - build/libs/producer.jar (artifactType = jar)

What it tells us is that this Java Library plugin produces variants with 5 attributes:

1. `org.gradle.category` - this variant represents a library

2. `org.gradle.dependency.bundling` - the dependencies of this variant are found as jars (they are not, for example, repackaged inside the jar)

3. `org.gradle.jvm.version` - the minimum Java version this library supports is Java 11

4. `org.gradle.libraryelements` - this variant contains all elements found in a jar (classes and resources)

5. `org.gradle.usage` - this variant is a Java runtime, therefore suitable for a Java compiler but also at runtime

The producer project defines an artifact (e.g., an instrumented JAR) that other projects can consume based on attributes. If we want the instrumented classes to be used in place of the existing variant when executing tests, we need to attach similar attributes to this variant. In fact, the attribute we care about is `org.gradle.libraryelements` which explains what the variant contains.

First, the following task defines a custom JAR artifact with an instrumented classifier (e.g., `producer-instrumented.jar)`:

producer/build.gradle.kts

    plugins {
        id("java-library")
    }

    val instrumentedJar by tasks.registering(Jar::class) {
        archiveClassifier.set("instrumented")
        from(sourceSets.main.get().output)
        // Additional instrumentation processing could go here
    }

producer/build.gradle

    plugins {
        id("java-library")
    }

    def instrumentedJar = tasks.register("instrumentedJar", Jar) {
        archiveClassifier.set("instrumented")
        from(sourceSets.main.output)
        // Additional instrumentation processing could go here
    }

Second, a configuration is created which:

- Provides an artifact variant for consumption.

- This variant is a `runtime-library` artifact with the element type `instrumented-jar`.

This allows other projects (consumers) to discover and request this specific artifact based on attributes:

producer/build.gradle.kts

    configurations {
        create("instrumentedJars") {
            isCanBeConsumed = true
            isCanBeResolved = false
            attributes {
                attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
                attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
                attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("instrumented-jar"))
            }
        }
    }

    artifacts {
        add("instrumentedJars", instrumentedJar)
    }

producer/build.gradle

    configurations {
        create("instrumentedJars") {
            canBeConsumed = true
            canBeResolved = false
            attributes {
                attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
                attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
                attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, "instrumented-jar"))
            }
        }
    }

    artifacts {
        add("instrumentedJars", instrumentedJar)
    }

This configuration ensures that only the correct artifacts are exposed and prevents accidental dependencies on internal tasks.

## Step 2: Configuring the consumer project

The consumer project requests the instrumented JAR by defining matching attributes.

First, the consumer explicitly requests a runtime usage artifact with the variant marked as `instrumented-jar`:

consumer/build.gradle.kts

    plugins {
        id("application")
    }

    configurations {
        create("instrumentedRuntime") {
            isCanBeConsumed = false
            isCanBeResolved = true
            attributes {
                attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
                attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("instrumented-jar"))
            }
        }
    }

consumer/build.gradle

    plugins {
        id("application")
    }

    configurations {
        create("instrumentedRuntime") {
            canBeConsumed = false
            canBeResolved = true
            attributes {
                attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
                attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, "instrumented-jar"))
            }
        }
    }

Then, a dependency is added on the producer:

consumer/build.gradle.kts

    dependencies {
        add("instrumentedRuntime", project(":producer"))
    }

consumer/build.gradle

    dependencies {
        add("instrumentedRuntime", project(":producer"))
    }

Finally, we use the artifact variant in a task. This task runs the application using the resolved instrumented JAR from the producer:

consumer/build.gradle.kts

    tasks.register<JavaExec>("runWithInstrumentation") {
        classpath = configurations["instrumentedRuntime"]
        mainClass.set("com.example.Main")
    }

consumer/build.gradle

    tasks.register("runWithInstrumentation",JavaExec) {
        classpath = configurations["instrumentedRuntime"]
        mainClass.set("com.example.Main")
    }

This setup ensures that the consumer resolves the correct variant without requiring knowledge of the producer’s implementation details.

A great way to check everything is working is by running the `resolvableConfigurations` task on the consumer side:

    $ ./gradlew consumer:resolvableConfigurations --configuration instrumentedRuntime

    > Task :consumer:resolvableConfigurations
    --------------------------------------------------
    Configuration instrumentedRuntime
    --------------------------------------------------

    Attributes
        - org.gradle.libraryelements = instrumented-jar
        - org.gradle.usage           = java-runtime

## Step 3: Setting up Defaults

To ensure that Gradle doesn’t fail when resolving dependencies without an instrumented variant, we need to define a fallback. Without this fallback, Gradle would complain about missing variants for dependencies that do not provide instrumented classes. The fallback explicitly tells Gradle that it’s acceptable to use the regular JAR when an instrumented variant isn’t available.

This is done using a compatibility rule:

consumer/build.gradle.kts

    abstract class InstrumentedJarsRule: AttributeCompatibilityRule<LibraryElements> {
        override fun execute(details: CompatibilityCheckDetails<LibraryElements>) = details.run {
            if (consumerValue?.name == "instrumented-jar" && producerValue?.name == "jar") {
                compatible()
            }
        }
    }

consumer/build.gradle

    abstract class InstrumentedJarsRule implements AttributeCompatibilityRule<LibraryElements> {

        @Override
        void execute(CompatibilityCheckDetails<LibraryElements> details) {
            if (details.consumerValue.name == 'instrumented-jar' && details.producerValue.name == 'jar') {
                details.compatible()
            }
        }
    }

Which we declare on the attributes schema:

consumer/build.gradle.kts

    dependencies {
        attributesSchema {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
                compatibilityRules.add(InstrumentedJarsRule::class.java)
            }
        }
    }

consumer/build.gradle

    dependencies {
        attributesSchema {
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
                compatibilityRules.add(InstrumentedJarsRule)
            }
        }
    }

## Step 4: Troubleshooting

If the consumer fails to resolve the artifact, check:

- The attributes in the consumer match those in the producer.

- The producer project properly declares the artifact.

- There are no conflicting configurations with different attributes.

## Summary

Variant-aware sharing enables clean and flexible artifact sharing between projects. It avoids hardcoded task dependencies and improves build maintainability.