Gradle has a lot of subtle assumptions or requirments it makes. Some of them are documented, but some are only implied through how gradle executes, and many are not obvious to someone unfamiliar with gradle. Here, I've made my best attempt to document some of these requirements/suggestions, provide some reasoning, and provide sources where they exist. In general, I've split stuff up into two categories: things that developers using gradle for their projects should know, and things that those authoring gradle plugins should know. Note that those authoring gradle plugins should also be aware of the first category.
Gradle -- in part due to how much it's changed since early versions -- often has many different ways to do the same thing. Generally speaking, the newer approaches are more declarative and less imperative than the older approaches, and these approaches tend to better support modern gradle features in general, or just be less annoying to fuss around with. To consider some examples, see how to add to the classpaths of a source set or using capabilities instead of classifiers.
Consider the following scenario: you have an api
source set and a main
source set, and you'd like everything from the
classpath of the api
source set to also be on the classpath of the main
source set. A common way of doing this (that
should be avoided!) that I've quite commonly seen goes as follows:
sourceSets {
api {}
main {
runtimeClasspath += sourceSets.api.runtimeClasspath
compileClasspath += sourceSets.api.compileClasspath
}
}
To see why this is not a good idea, consider the following case: the main
source set has a dependency implementation 'a:a:1.0.0'
, and the api souce set has a dependency implementation 'b:b:1.2.0'
. Now, it turns out that a
also depends on
b
, but it publishes a transitive dependency on version 1.0.0
. With the code above, both versions 1.0.0
and 1.2.0
of b
will end up on the classpath -- and if there's further transitive dependencies this gets messy even faster! A better approach
to this is to let gradle handle it all at the level of dependency resolution:
configurations {
compileClasspath.extendsFrom apiCompileClasspath
runtimeClasspath.extendsFrom apiRuntimeClasspath
}
And the compile classpath will inherit the dependencies used by the api compile classpath, instead of just inherting the resolved files.
Let's expand on that last example of a separate api
source set a bit more: generally, if you're using such a setup it's because you're publishing a separate api jar. You might create a jar with a different classifier, include its contents in both
the main jar and the normal jar, and then publish both by manually adding the artifact to the publication. A complete build script implementing this that way -- which is not the best option for a number of reasons -- looks as follows:
sourceSets {
api {}
}
configurations {
compileClasspath.extendsFrom apiCompileClasspath
runtimeClasspath.extendsFrom apiRuntimeClasspath
}
tasks.register('apiJar', Jar) {
from sourceSets.api.output
archiveClassifier = 'api'
}
jar {
from sourceSets.api.output
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact tasks.apiJar
}
}
}
At first glance, this seems like a good way to go about this. However, several problems arise quickly:
- someone depending on the main jar or someone depending on the
api
jar will get the exact same transitive dependencies -- which is not optimal; there may, for instance, be compile-only dependencies of the main jar that you wish to expose ascompileOnlyApi
for use by other projects internally, but which should not be exposed to consumers of theapi
- the creation of the
apiJar
task, its addition to the publication, and the inheritance of source sets is all done imperatively -- this leaves it rather fragile, and somewhat confusing to read. Looking at the build script at a glance, we might immediately tell that it's making and publishing another jar -- but it's not obvious that what its doing is isolating certain portions of our project into their own chunk, which themain
component of the project extends from. - Build scripts are meant to declare the structure of a project, as opposed to declaring a series of steps. With the approach
above, someone writing this has to have some understanding of how the
Jar
task type works, how gradle sets up task dependencies, etc -- all of which are more like implementation details of declaring the desired structure.
Ideally, there'd be a way of telling gradle about the structure we want, and having it figure out all the tasks needed, linking it up to publication, and all the rest without having to manually specify it. And indeed there is! It's called library features. An example of the previous buildscript, adapted to use feature variants, might look like:
sourceSets {
api {}
}
java.registerFeature('api') {
usingSourceSet(sourceSets.api)
}
configurations {
compileClasspath.extendsFrom apiCompileClasspath
runtimeClasspath.extendsFrom apiRuntimeClasspath
apiElements.extendsFrom apiApiElements
runtimeElements.extendsFrom apiRuntimeElements
}
jar {
from sourceSets.api.output
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}
Registering the feature automatically sets up a number of components -- notably, the jar task for the feature, and publishing.
In fact, you can tell it to set up javadoc or sources jars for a feature in a similar way as you would for the main feature!
Additionally, the api
jar now can have its own transitive dependencies when published -- and we can express that the main
artifact's transitive dependencies should extend from the api jar's with the apiElements.extendsFrom apiApiElements
and
runtimeElements.extendsFrom apiRuntimeElements
lines. When depending on this jar, instead of specifying the classifier, you specify the capability:
implementation('org.example:example:1.0.0') {
capabilities {
requireCapability('org.example:example-api')
}
}
Now, there's still one or two things left to be desired with this setup -- namely, having to explicitly define that the various
main
configurations depend on the equivalent api
configurations, and that the main
jar should contain the sources from
the api
jar. However, if we're working with some sort of plugin to bundle dependencies, such as the shadow plugin, or the
jar-in-jar features of plugins like loom or neogradle, this can all be expressed quite nicely:
sourceSets {
api {}
}
java.registerFeature('api') {
usingSourceSet(sourceSets.api)
}
dependencies {
include(implementation(project(':')) {
capabilities {
requireCapability('org.example:example-api')
}
})
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}