Actual footage of different kinds of Gradle Configurations

Martin Bonnin
6 min readMay 31, 2021

--

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 they have a configuration phase too that is run before execution.

Well these are all configurations for sure… They also hide another type of configuration, which plays a center role in Gradle dependency management: the Configuration API.

Elephant by flowcomm

Everytime you add a new dependency to a project, you’re actually using configurations behind the scenes:

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 go up and expose the three different kinds of configurations in real life.

A concrete example

To understand what implementation() does, let’s start from scratch! Create a new empty Gradle project with a single build.gradle.kts file containing:

Running ./gradlew dependencies should fail:

That’s because implementation is not a regular method from the Gradle Core API, it is a generated accessor, 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 the plain Gradle APIs:

Running ./gradlew dependencies should still fail:

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:

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.

“Bucket of dependencies” Configurations

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:

Run ./gradlew to trigger the compilation and evaluation of your build.gradle.kts script:

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 get access to the okhttp jar as well as its transitive dependencies: okio and kotlin-stdlib. The way this is usually done is 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, this is called resolving the configuration:

That shouldn’t go too well:

💥 Damn, this is where things get fun… Indeed, if you remember the results of the first ./gradlew dependencies, there was this line:

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).

Resolvable Configurations

If you look at the earlier ./gradlew dependencies output, you can find two resolvable configurations:

Both these configurations don’t have a (n) in front of them, meaning you can resolve them, Let's do this:

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 to the above resolvable configurations, the java plugin creates 2 non-resolvable, implementation-like, "bucket of dependencies", configurations:

  • compileOnly to add a dependency to compileClasspath 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 to runtimeClasspath 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.

Using Configuration.extendsFrom(), Gradle can make dependencies from these configurations available to the resolvable configurations. 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?

Consumable Configurations

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:

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.

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.

Conclusion

The Configuration API is a corner stone of the dependency management in Gradle. Despite all of them being Configurations, the “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 double check, you can always dump the values of isCanBeResolved and isCanBeConsumed:

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 for compileOnly 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 and compileClasspath) 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 and runtimeElements): 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!

*: I got an explanation from Cedric Champeau! compileOnly was present before runtimeOnly and has both flags set to true for backward compatibility reasons. And actually, both flags are set to false by default starting with Gradle 7.

--

--

Martin Bonnin
Martin Bonnin

No responses yet