How to share outputs between projects
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.
dependencies {
instrumentedClasspath(project(path: ":producer", configuration: 'instrumentedJars'))
}
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 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:
-
org.gradle.category
indicates that this variant represents a library. -
org.gradle.dependency.bundling
specifies that dependencies are external jars (not repackaged inside the jar). -
org.gradle.jvm.version
denotes the minimum Java version supported, which is Java 11. -
org.gradle.libraryelements
shows that this variant contains all elements typically found in a jar (classes and resources). -
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:
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'))
}
}
}
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:
-
runtimeElements
- the default runtime variant provided by thejava-library
plugin. -
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:
dependencies {
testImplementation 'junit:junit:4.13'
testImplementation project(':producer')
}
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:
configurations {
testRuntimeClasspath {
attributes {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, 'instrumented-jar'))
}
}
}
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:
abstract class InstrumentedJarsRule implements AttributeCompatibilityRule<LibraryElements> {
@Override
void execute(CompatibilityCheckDetails<LibraryElements> details) {
if (details.consumerValue.name == 'instrumented-jar' && details.producerValue.name == 'jar') {
details.compatible()
}
}
}
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:
dependencies {
attributesSchema {
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE) {
compatibilityRules.add(InstrumentedJarsRule)
}
}
}
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. |