The first time I heard about Gradle configurations, I thought it'd be about writing build.gradle
files and configuring some DSL
and writing {}
blocks. Then I started writing plugins and realized that Tasks
have a configuration phase too that is run before execution.
Well all these are all configurations for sure... They also hide another type of configuration, which plays a center role in Gradle dependency management handling: the Configuration API. Everytime you add a new dependency to a project, you're actually using configurations behind the scenes:
https://gist.github.com/b086d26e81c38d720f3aded2abfd292b
The Gradle dependency management documentation is very detailed. It has a very detailed page about terminology and another one on resolvable vs consumable Configurations that I recommend reading. It's also a lot of information to process.
This article goes the other way and starts from the concrete example above to try to expose the different kinds of configurations in real life.
To understand what implementation()
does, we're going to start with an empty project. Create a new empty Gradle project with just a single build.gradle.kts
file containing:
https://gist.github.com/041abc77a9b19b5ae6be34bf1f9877a3
Running ./gradlew dependencies
should fail:
https://gist.github.com/e5967d2d2941132dc50e0d02b80445a1
That's because implementation
is not a regular method from the Gradle Core API, it is a generated accesor generated automatically by Gradle to make it easier to work with the DSL (Groovy has the same syntax although it's more dynamic and doesn't rely on generated accessors). You don't have to rely on the generated accessors though. Everything is Gradle is doable using plain JVM Gradle APIs:
https://gist.github.com/0bea0c84960c9fa36f00ef841ad72aea
Trying to run ./gradlew dependencies
should still fail:
https://gist.github.com/d8aa80cae8a3beee01b2275da751cbb9
Fair enough. Since we started from an empty build.gradle.kts
file, Gradle doesn't even know what we're trying to do. OkHttp
and implementation
are JVM concepts so it makes sense that Gradle doesn't force it by default. In fact, the Java plugin creates the implementation
configuration (see doc). Let's add it:
https://gist.github.com/60a9506a905931fac8610e03728877d1
Running ./gradlew dependencies
will now show a lot more information. The result is too long to be displayed here, but you should see something like this (test configurations omitted for clarity):
annotationProcessor
Annotation processors and their dependencies for source set 'main'.apiElements
- API elements for main. (n)archives
- Configuration for archive artifacts. (n)compileClasspath
- Compile classpath for source set 'main'.compileOnly
- Compile only dependencies for source set 'main'. (n)default
- Configuration for default artifacts. (n)implementation
- Implementation only dependencies for source set 'main'. (n)runtimeClasspath
- Runtime classpath of source set 'main'.runtimeElements
- Elements of runtime for main. (n)runtimeOnly
- Runtime only dependencies for source set 'main'. (n)
Pheewww, that's a lot! We won't be able to cover all of them in this article but we'll cover the most representative ones. Let's skip the default
configuration that is now deprecated and put aside the archives
and annotationProcessor
ones for now, that leaves us with apiElements
, compileClasspath
, compileOnly
, implementation
, runtimeClasspath
, runtimeElements
and runtimeOnly
.
Let's start with the ubiquitous one, implementation
.
implementation
is a "bucket of dependencies" Configuration. This is where you add dependencies like com.squareup.okhttp3:okhttp:4.9.0
. To get the list of dependencies (but not their files, more on that later), you can do things like:
https://gist.github.com/bc8eb175db51cc0428f9ebe034def01b
Run ./gradlew
to trigger the compilation and evaluation of your build.gradle.kts
script:
https://gist.github.com/2188adc7852ec2f193d46e087b70536c
So far so good! The dependency you just added has been registered. It's registered as a DefaultExternalModuleDependency
because it gets its file from an external repo (MavenCentral here), that's fair. Ultimately though, you want to resolve that dependency and get access to the okhttp
jar as well as its transitive dependencies: okio
and kotlin-stdlib
. The way this is usually done if by reading files directly from the Configuration
. Indeed, a Configuration extends from a FileCollection so it has a getFiles()
method. Let's try to display the files in our configuration:
https://gist.github.com/c856b4c26e48082a2effab499f3e6ce9
That shouldn't go too well:
https://gist.github.com/44f7d2a06409a1eb62b6f3ecb8c4ff05
💥 Damn, this is where things get fun... Indeed, if you remember the results of the first ./gradlew dependencies
, there was this line:
https://gist.github.com/1bcd8628dd8034e839ef7c5967a564ad
Getting the list of jar files contained in the implementation
configuration, i.e. resolving it, is not possible. If you dump configurations["implementation"].isCanBeResolved
, you will see it will indeed be false
. This configuration holds dependencies declarations but cannot be resolved itself. For this, you'll need resolvable configurations (see doc).
If you look at the earlier ./gradlew dependencies
output, you can find two resolvable configurations:
runtimeClasspath
compileClasspath
Both these configurations don't have a (n)
in front of them, meaning you can resolve them, Let's do this:
https://gist.github.com/8fd38dad7d9c452d30de4351120340d6
https://gist.github.com/e2cf6cea4134f0782cab55d1f7db60f1
Huge success! You just resolved your first configuration. In fact this is the same thing that the Java/Kotlin compiler will use to determine what jars to put on the compile classpath (hence the "compileClasspath"
name!). Whenever you need to compile against okhttp
, the compiler also needs okio
and kotlin-stdlib
. It needs okio
because okio
is in the okhttp
API. Function such as ResponseBody.source() expose a okio.BufferedSource so the compiler needs that symbol in the compile classpath (you can read more about api vs implementation here).
What about runtimeClasspath
then? Well in this specific case, it's going to be the same. This is because the exact same dependencies are needed both to compile the project and to run it. This isn't a general rule though. If okhttp
wrapped all the okio
types and did not expose them, okio
wouldn't be needed to compile the project.
In addition, the java
plugin creates 2 non-resolvable, implementation-like, "bucket of dependencies", configurations:
compileOnly
to add a dependency tocompileClasspath
only. This is typically what's used by Gradle plugins to compile against the Gradle API but not use it at runtime since it's provided by the Gradle instance that runs the plugin.runtimeOnly
to add a dependency toruntimeClasspath
only. This is used less often but is useful in cases where multiple implementations of the same API could be made available at runtime. For an example using ServiceLoader or another mechanism. This happens with logging frameworks like SLF4J. The project is compiled using an abstract logger. The actual implementation is being loaded at runtime but not needed during compilation.
This is made by using Configuration.extendsFrom(). When compileClasspath extends from compileOnly, all the files from compileOnly will be available in compileClasspath.
In practice, the java
plugin uses the following (from the doc):
- implementation (non resolvable)
- compileOnly (non resolvable)
- runtimeOnly (non resolvable)
- compileClasspath extends compileOnly, implementation
- runtimeClasspath extends runtimeOnly, implementation
The first three are where you add dependencies. The last two are used by the JavaCompile task and runners.
Note that there is no api
configuration in the list. This is because api
only make sense for library projects that can be consumed by another project. The api
configuration is added by the java-library
plugin (and not the java
one)
If you go back to the original list of configurations, we have covered compileClasspath
, compileOnly
, implementation
, runtimeClasspath
and runtimeOnly
.
So what are runtimeElements
and apiElements
?
runtimeElements
and apiElements
are consumable
configurations. Consumable configurations are meant to be used by other projects consuming this project. I know this is very close to "resolvable". In Gradle terminology:
- Resolvable is to read the files from a configuration inside a project
- Consumable is to expose files to consumers outside the project
It makes more sense for library projects. For some reason, it's also added for non-library projects. I'm guessing some project could consume the executable jar too. In all cases, you can get the consumable configurations with ./gradlew outgoingVariants
:
https://gist.github.com/28172017ab1d6210aa6460e03a84f4ef
The consumable configurations are used during variant-aware selection. If you have seen a message such as below, chances are that some consumable configuration exposes incompatible attributes.
https://gist.github.com/1b3c3a9d08713838d3bba75d19b6efbd
A consumable configuration can have attributes using the Configuration.attributes
API and then expose artifacts using the Project.artifacts
API. There's a lot in there and that'll certainly deserve a separate article.
The Configuration API is a corner stone of the dependency management in Gradle. Despite all of them being Configurations, bucket of dependencies, resolvable and consumable configuration are very different. I hope this simple example helps to understand what are the different types of Configurations. If you ever want to check, you can dump the values of isCanBeResolved
and isCanBeConsumed
:
https://gist.github.com/c4b0d04cb1fc21857632d33515dd4968
https://gist.github.com/6cec3ac53b64262d6b2d4c8c629c2360
In this article, we've seen the three different types of configurations:
-
Bucket of dependencies (
implementation
,runtimeOnly
,compileOnly
) are used by the user to declare dependencies. They are neither resolvable nor consumable... ...well, except forcompileOnly
that is both! I didn't expected that when I started writing this article. If anyone has an explanation, I'll take it. -
resolvable configurations (
runtimeClasspath
andcompileClasspath
) are the resolvable configurations to be used inside the project by tasks like compileJava and compileKotlin to get the actual jar files. -
consumable configurations (
apiElements
andruntimeElements
): are the consumable configurations to be consumed by other projects and used by variant aware selection. You can see them with./gradlew outgoingVariant
.
When in doubt, always refer to the official terminology doc which is super useful!
Happy configuring!