This chapter is primarily aimed towards plugin authors who want to understand better how to leverage the capabilities of the dependency resolution engine to support variant-aware dependency management. Users who simply want to understand what configuration attributes are will also find support here.

Different kinds of configurations

Historically, configurations have been at the root of dependency resolution in Gradle. In the end, what we want to make a diffence is between a consumer and a producer. For this purpose, configurations are used for at least 3 different aspects:

  1. to declare dependencies

  2. as a consumer, to resolve a set of dependencies to files

  3. as a producer, to expose artifacts for consumption by other projects

For example, if I want to express that my application app depends on library lib, we need at least one configuration:

Example 1. Configurations are used to declare dependencies
build.gradle
configurations {
    // declare a "configuration" named "someConfiguration"
    someConfiguration
}
dependencies {
    // add a project dependency to the "someConfiguration" configuration
    someConfiguration project(":lib")
}
build.gradle.kts
// declare a "configuration" named "someConfiguration"
val someConfiguration by configurations.creating

dependencies {
    // add a project dependency to the "someConfiguration" configuration
    someConfiguration(project(":lib"))
}

Configurations can extend other configuration, in order to inherit their dependencies. However, the code above doesn’t tell anything about the consumer. In particular, it doesn’t tell what is the use of the configuration. Let’s say that lib is a Java library: it can expose different things, such as its API, implementation or test fixtures. If we want to resolve the dependencies of app, we need to know what kind of task we’re performing (compiling against the API of lib, executing the application, compiling tests, …​). For this purpose, you’ll often find companion configurations, which are meant to unambiguously declare the usage:

Example 2. Configurations representing concrete dependency graphs
build.gradle
configurations {
    // declare a configuration that is going to resolve the compile classpath of the application
    compileClasspath.extendsFrom(someConfiguration)

    // declare a configuration that is going to resolve the runtime classpath of the application
    runtimeClasspath.extendsFrom(someConfiguration)
}
build.gradle.kts
configurations {
    // declare a configuration that is going to resolve the compile classpath of the application
    compileClasspath.extendsFrom(someConfiguration)

    // declare a configuration that is going to resolve the runtime classpath of the application
    runtimeClasspath.extendsFrom(someConfiguration)
}

At this stage, we have 3 different configurations, which already have different goals:

  • someConfiguration declares the dependencies of my application. It’s just a bucket where we declare a list of dependencies.

  • compileClasspath and runtimeClasspath are configurations meant to be resolved: when resolved they should contain respectively the compile classpath, and the runtime classpath of the application.

This is actually represented on the Configuration type by the canBeResolved flag. A configuration that can be resolved is a configuration for which we can compute a dependency graph, because it contains all the necessary information for resolution to happen. That is to say we’re going to compute a dependency graph, resolve the components in the graph, and eventually get artifacts. A configuration which has canBeResolved set to false is not meant to be resolved. Such a configuration is there only to declare dependencies. The reason is that depending on the usage (compile classpath, runtime classpath), it can resolve to different graphs. It is an error to try to resolve a configuration which has canBeResolved set to false. To some extend, this is similar to an abstract class (canBeResolved=false) which is not supposed to be instantiated, and a concrete class extending the abstract class (canBeResolved=true). A resolvable configuration will extend at least one non resolvable configuration (and may extend more than one).

On the other end, at the library project side (the producer), we also use configurations to represent what can be consumed. For example, the library may expose an API or a runtime, and we would attach artifacts to either one, the other, or both. Typically, to compile against lib, we need the API of lib, but we don’t need its runtime dependencies. So the lib project will expose an apiElements configuration, which is aimed for consumers looking for its API. Such a configuration is going to be consumable, but is not meant to be resolved. This is expressed via the canBeConsumed flag of a Configuration:

Example 3. Setting up configurations
build.gradle
configurations {
    // A configuration meant for consumers that need the API of this component
    exposedApi {
        // This configuration is an "outgoing" configuration, it's not meant to be resolved
        canBeResolved = false
        // As an outgoing configuration, explain that consumers may want to consume it
        canBeConsumed = true
    }
    // A configuration meant for consumers that need the implementation of this component
    exposedRuntime {
        canBeResolved = false
        canBeConsumed = true
    }
}
build.gradle.kts
configurations {
    // A configuration meant for consumers that need the API of this component
    create("exposedApi") {
        // This configuration is an "outgoing" configuration, it's not meant to be resolved
        isCanBeResolved = false
        // As an outgoing configuration, explain that consumers may want to consume it
        isCanBeConsumed = true
    }
    // A configuration meant for consumers that need the implementation of this component
    create("exposedRuntime") {
        isCanBeResolved = false
        isCanBeConsumed = true
    }
}

In short, a configuration role is determined by the canBeResolved and canBeConsumed flag combinations:

Table 1. Configuration roles

Configuration role

can be resolved

can be consumed

Bucket of dependencies

false

false

Resolve for certain usage

true

false

Exposed to consumers

false

true

Legacy, don’t use

true

true

For backwards compatibility, those flags have both true as the default value, but as a plugin author, you should always determine the right values for those flags, or you might accidentally introduce resolution errors.

Configuration attributes

We have explained that we have 3 configuration roles, and explained that we may want to resolve the compile and runtime classpath differently, but there’s nothing in what we’ve written which allows explaining the difference. This is where attributes come into play. The role of attributes is to perform the selection of the right variant of a component. In our example, the lib library exposes 2 variants: its API (via exposedApi) and its runtime (via exposedRuntime). There’s no restriction on the number of variants a component can expose. We may, for example, want to expose the test fixtures of a component too. But then, the consumer needs to explain what configuration to consume, and this is done by setting attributes on both the consumer and producer ends.

Attributes consist of a name and a value pair. Gradle comes with a standard attribute named org.gradle.usage specifically to deal with the concept of selecting the right variant of a component based on the usage of the consumer (compile, runtime …​). It is however possible to define an arbitrary number of attributes. As a producer, I can express that a consumable configuration represents the API of a component by attaching the (org.gradle.usage,JAVA_API) attribute to the configuration. As a consumer, I can express that I need the API of the dependencies of a resolvable configuration by attaching the (org.gradle.usage,JAVA_API) attribute to it. Now Gradle has a way to automatically select the appropriate variant by looking at the configuration attributes: - the consumer wants org.gradle.usage=JAVA_API - the dependent project exposes 2 different variants. One with org.gradle.usage=JAVA_API, the other with org.gradle.usage=JAVA_RUNTIME. - Gradle selects the org.gradle.usage=JAVA_API variant

In other words: attributes are used to perform the selection based on the values of the attributes. It doesn’t matter what the names of the configurations are: only the attributes matter.

Declaring attributes

Attributes are typed. An attribute can be created via the Attribute<T>.of method:

Example 4. Define attributes
build.gradle
// An attribute of type `String`
def myAttribute = Attribute.of("my.attribute.name", String)
// An attribute of type `Usage`
def myUsage = Attribute.of("my.usage.attribute", Usage)
build.gradle.kts
// An attribute of type `String`
val myAttribute = Attribute.of("my.attribute.name", String::class.java)
// An attribute of type `Usage`
val myUsage = Attribute.of("my.usage.attribute", Usage::class.java)

Currently, only attribute types of String, or anything extending Named is supported. Attributes must be declared in the attribute schema found on the dependencies handler:

Example 5. Registering attributes on the attributes schema
build.gradle
dependencies.attributesSchema {
    // registers this attribute to the attributes schema
    attribute(myAttribute)
    attribute(myUsage)
}
build.gradle.kts
dependencies.attributesSchema {
    // registers this attribute to the attributes schema
    attribute(myAttribute)
    attribute(myUsage)
}

Then configurations can be configured to set values for attributes:

Example 6. Setting attributes on configurations
build.gradle
configurations {
    myConfiguration {
        attributes {
            attribute(myAttribute, 'my-value')
        }
    }
}
build.gradle.kts
configurations {
    create("myConfiguration") {
        attributes {
            attribute(myAttribute, "my-value")
        }
    }
}

For attributes which type extends Named, the value of the attribute must be created via the object factory:

Example 7. Named attributes
build.gradle
configurations {
    myConfiguration {
        attributes {
            attribute(myUsage, project.objects.named(Usage, 'my-value'))
        }
    }
}
build.gradle.kts
configurations {
    "myConfiguration" {
        attributes {
            attribute(myUsage, project.objects.named(Usage::class.java, "my-value"))
        }
    }
}

Attribute compatibility rules

Attributes let the engine select compatible variants. However, there are cases where a provider may not have exactly what the consumer wants, but still something that it can use. For example, if the consumer is asking for the API of a library, there’s a possibility that the producer doesn’t have such a variant, but only a runtime variant. This is typical of libraries published on external repositories. In this case, we know that even if we don’t have an exact match (API), we can still compile against the runtime variant (it contains more than what we need to compile but it’s still ok to use). To deal with this, Gradle provides attribute compatibilty rules. The role of a compatibility rule is to explain what variants are compatible with what the consumer asked for.

Attribute compatibility rules have to be registered via the attribute matching strategy that you can obtain from the attributes schema.

Attribute disambiguation rules

Because multiple values for an attribute can be compatible with the requested attribute, Gradle needs to choose between the candidates. This is done by implementing an attribute disambiguation rule.

Attribute disambiguation rules have to be registered via the attribute matching strategy that you can obtain from the attributes schema.