# How to share outputs between projects

version 8.13

A common pattern, in multi-project builds, is that one project consumes the artifacts of another project.

In general, the simplest consumption form in the Java ecosystem is that when `A` depends on `B`, then `A` would depend on the `jar` produced by project `B`.

## Considerations and possible solutions

A frequent anti-pattern to declare cross-project dependencies is:

    dependencies {
       // this is unsafe!
       implementation project(":other").tasks.someOtherJar
    }

This publication model is unsafe and can lead to non-reproducible and hard to parallelize builds.

| Don’t reference other project tasks directly!

You could define a configuration on the producer side which serves as an exchange of artifacts between the producer and the consumer.

consumer/build.gradle

    dependencies {
        instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
    }

consumer/build.gradle.kts

    dependencies {
        instrumentedClasspath(project(mapOf(
            "path" to ":producer",
            "configuration" to "instrumentedJars")))
    }

However, the consumer has to explicitly tell which configuration it depends on and this is not recommended. If you plan to publish the component which has this dependency, it will likely lead to broken metadata.

This section explains how to properly create cross-project boundaries by defining "exchanges" between projects by using variants.

## Variant-aware sharing of artifacts

Gradle’s variant model allows consumers to specify requirements using attributes, while producers provide appropriate outgoing variants using attributes as well.

For example, a single dependency declaration like `project(":myLib")` can select either the `arm64` or `i386` version of `myLib`, based on the architecture.

To achieve this, attributes must be defined on both the consumer and producer configurations.

| When configurations have attributes, they participate in variant-aware resolution. This means they become candidates for resolution whenever any dependency declaration, such as `project(":myLib")`, is used. Attributes on producer configurations must be consistent with other variants provided by the same project. Introducing inconsistent or ambiguous attributes can lead to resolution failures. In practice, the attributes you define will often depend on the ecosystem (e.g., Java, C++) because ecosystem-specific plugins typically apply different attribute conventions.

Consider an example of a Java Library project. Java libraries typically expose two variants to consumers: `apiElements` and `runtimeElements`. In this case, we are adding a third variant, `instrumentedJars`.

To correctly configure this new variant, we need to understand its purpose and set appropriate attributes. Here are the attributes on the `runtimeElements` configuration of the producer:

    $ .gradle outgoingVariants --variant runtimeElements

    Attributes
        - org.gradle.category            = library
        - org.gradle.dependency.bundling = external
        - org.gradle.jvm.version         = 11
        - org.gradle.libraryelements     = jar
        - org.gradle.usage               = java-runtime

This tells us that the `runtimeElements` configuration includes 5 attributes:

1. `org.gradle.category` indicates that this variant represents a library.

2. `org.gradle.dependency.bundling` specifies that dependencies are external jars (not repackaged inside the jar).

3. `org.gradle.jvm.version` denotes the minimum Java version supported, which is Java 11.

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

5. `org.gradle.usage` defines the variant as a Java runtime, suitable for both compilation and runtime.

To ensure that the `instrumentedJars` variant is used in place of `runtimeElements` when executing tests, we must attach similar attributes to this new variant.

The key attribute for this configuration is `org.gradle.libraryelements`, as it describes what the variant contains. We can set up the `instrumentedJars` variant accordingly:

producer/build.gradle

    configurations {
        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(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
                attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInteger())
                attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
            }
        }
    }

producer/build.gradle.kts

    val instrumentedJars by configurations.creating {
        isCanBeConsumed = true
        isCanBeResolved = false
        attributes {
            attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
            attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
            attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
            attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, JavaVersion.current().majorVersion.toInt())
            attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named("instrumented-jar"))
        }
    }

This ensures that the `instrumentedJars` variant is correctly identified as containing elements similar to a jar, allowing it to be selected appropriately.

| Selecting the right attributes is the most challenging part of this process, as they define the semantics of the variant. Before introducing new attributes, always consider whether an existing attribute already conveys the required semantics. If no suitable attribute exists, you can create a new one. However, be cautious—adding a new attribute may introduce ambiguity during variant selection. In many cases, adding an attribute requires applying it consistently across all existing variants.

We’ve introduced a new variant for runtime that provides instrumented classes instead of the normal ones. As a result, consumers now face a choice between two runtime variants:

1. `runtimeElements` - the default runtime variant provided by the `java-library` plugin.

2. `instrumentedJars` - the custom variant we’ve added.

If we want the instrumented classes to be included on the test runtime classpath, we can now declare the dependency on the consumer as a regular project dependency:

consumer/build.gradle

    dependencies {
        testImplementation 'junit:junit:4.13'
        testImplementation project(':producer')
    }

consumer/build.gradle.kts

    dependencies {
        testImplementation("junit:junit:4.13")
        testImplementation(project(":producer"))
    }

If we stop here, Gradle will still resolve the `runtimeElements` variant instead of the `instrumentedJars` variant.

This happens because the `testRuntimeClasspath` configuration requests a variant with the `libraryelements` attribute set to `jar`, and our `instrumented-jars` value does not match.

To fix this, we need to update the requested attributes to specifically target instrumented jars:

consumer/build.gradle

    configurations {
        testRuntimeClasspath {
            attributes {
                attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
            }
        }
    }

consumer/build.gradle.kts

    configurations {
        testRuntimeClasspath {
            attributes {
                attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, "instrumented-jar"))
            }
        }
    }

We can look at another report on the consumer side to view exactly what attributes of each dependency will be requested:

    $ .gradle resolvableConfigurations --configuration testRuntimeClasspath

    Attributes
        - org.gradle.category            = library
        - org.gradle.dependency.bundling = external
        - org.gradle.jvm.version         = 11
        - org.gradle.libraryelements     = instrumented-jar
        - org.gradle.usage               = java-runtime

The `resolvableConfigurations` report is the complement of the `outgoingVariants` report we ran previously.

By running both of these reports on the consumer and producer sides of a relationship, respectively, you can see exactly what attributes are involved in matching during dependency resolution and better predict the outcome when configurations are resolved.

At this point, we’re specifying that the test runtime classpath should resolve variants with instrumented classes.

However, there’s an issue: some dependencies, like `JUnit`, don’t provide instrumented classes. If we stop here, Gradle will fail, stating that no compatible variant of `JUnit` exists.

This happens because we haven’t told Gradle that it’s acceptable to fall back to the regular jar when an instrumented variant isn’t available. To resolve this, we need to define a compatibility rule:

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

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

We then declare this rule on the attributes schema:

consumer/build.gradle

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

consumer/build.gradle.kts

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

And that’s it! Now we have:

- Added a variant which provides instrumented jars.

- Specified that this variant is a substitute for the runtime.

- Defined that the consumer needs this variant only for test runtime.

Gradle provides a powerful mechanism for selecting the right variants based on preferences and compatibility. For more details, check out the variant aware plugins section of the documentation.

| By adding a value to an existing attribute or defining new attributes, we are extending the model. This means that all consumers must be aware of this extended model. For local consumers, this is usually not a problem because all projects share the same schema. However, if you need to publish this new variant to an external repository, external consumers must also add the same rules to their builds for them to work. This is generally not an issue for ecosystem plugins (e.g., the Kotlin plugin), where consumption is not possible without applying the plugin. However, it becomes problematic if you add custom values or attributes. Therefore, avoid publishing custom variants if they are intended for internal use only.