Skip to content

Instantly share code, notes, and snippets.

@martinbonnin
Created May 30, 2021 21:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save martinbonnin/ce863591a39ef6a073aae8125f636089 to your computer and use it in GitHub Desktop.
Save martinbonnin/ce863591a39ef6a073aae8125f636089 to your computer and use it in GitHub Desktop.

Actual footage of different kinds of Gradle Configurations

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.

A concrete example

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.

"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:

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

Resolvable Configurations

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

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?

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:

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.

Conclusion

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 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment