Gradle 9 deprecation and optional dependencies

In Gradle 8.x my project produces the following message:

“The ‘optional’ feature was created using the main source set. This behavior has been deprecated. This will fail with an error in Gradle 9.0. The main source set is reserved for production code and should not be used for features.”

I’m using registerFeature as a means to create optional dependency entries in the published pom. The basis for this was taken from a blog post.

java {
  registerFeature('optional') {
    usingSourceSet sourceSets.main
  }
}

And for the dependencies:

dependencies {
  ...
  optionalImplementation "javax.cache:cache-api:${javaxCacheApiVersion}"
  optionalImplementation "org.ow2.asm:asm:${asmVersion}"
  ...
}

The pom will have the following entries:

<dependency>
  <groupId>javax.cache</groupId>
  <artifactId>cache-api</artifactId>
  <version>1.1.1</version>
  <scope>runtime</scope>
  <optional>true</optional>
</dependency>
<dependency>
  <groupId>org.ow2.asm</groupId>
  <artifactId>asm</artifactId>
  <version>[6.0,)</version>
  <scope>runtime</scope>
  <optional>true</optional>
</dependency>

Apparently starting with Gradle 9 this will not work anymore.

So, what’s the correct way to solve this problem?

https://docs.gradle.org/current/userguide/upgrading_version_8.html#changes_8.6

You should take a look at the updated documentation

Here’s how I handle it,This may be illegal, but it achieves the results I want

I created Please revert the deprecation to declare additional feature variants on the `main` source set · Issue #29455 · gradle/gradle · GitHub to revert the deprecation and Allow to declare multiple feature variants on the same non-`main` source set · Issue #29457 · gradle/gradle · GitHub to extend the functionality from main to custom source sets.

But in the meantime, or if the deprecation is not going to be reverted, this should work as expected (it is written in Kotlin DSL but you can translate it to Groovy DSL too):

plugins {
    `java-library`
    `maven-publish`
}

group = "showcase"
version = "0.1.0-SNAPSHOT"

publishing {
    val library by publications.registering(MavenPublication::class) {
        from(components["java"])
    }
}

val optional by sourceSets.creating
java {
    registerFeature("optional") {
        usingSourceSet(optional)
    }
}
val optionalImplementation by configurations.existing
val compileOnly by configurations.existing {
    extendsFrom(optionalImplementation.get())
}
dependencies {
    optionalImplementation("javax.cache:cache-api:$javaxCacheApiVersion")
    optionalImplementation("org.ow2.asm:asm:$asmVersion")
}

val apiElements by configurations.getting
val optionalApiElements by configurations.existing {
    outgoing.artifacts.clear()
    apiElements.artifacts.forEach(outgoing::artifact)
    extendsFrom(apiElements)
}
val runtimeElements by configurations.getting
val optionalRuntimeElements by configurations.existing {
    outgoing.artifacts.clear()
    runtimeElements.artifacts.forEach(outgoing::artifact)
    extendsFrom(runtimeElements)
}
1 Like

I just ran across this issue as well and the documentation around the deprecation is seriously deficient. Adding another source set changes the entire source and artifact layout of the project. When using features as

a (better) substitute for Maven optional dependencies

as is mentioned in the user manual, not just the aforementioned blog post, the suggested change breaks consumers of your library. Because the classes that require the optional dependency are now in an entirely separate jar - one that will not be pulled in by existing consumers.

IMO there needs to continue to be a way to generate a single jar that simply declares certain dependencies as optional in the metadata. The workaround from @Vampire above does work but is too verbose and unintuitive to achieve what I imagine is a pretty common use-case. As-is I’m wondering why I shouldn’t just “cheat” and declare optional dependencies as compile only.

I suggest you also voice your concerns in the issue I create even though it is closed right now.
That will get more Gradle folks attention than a comment here.

As-is I’m wondering why I shouldn’t just “cheat” and declare optional dependencies as compile only.

Because Gradle consumers cannot select the variant and get the dependencies but then again need to know about those optional dependencies and depend on them manually in their downstream project. Basically the blog post describes the reasons why the cheating is not so nice for Gradle consumers and semantic.

Thanks for the response. Fair point on variant selection but my consumer projects all use Maven so I think they are already directly declaring my optional dependencies.

But now that I’m looking more closely, I actually don’t think your suggestion is working for me. It does indeed produce a single jar, and the metadata for that jar does declare my optional dependency as optional, but the actual classes with that dependency are not included. Am I doing something wrong?

Well, yeah, if all consumers are only Maven, there is not too much gained from the consumer point of view.

Your classes are probably not included because you moved them to the optional source set.
The point was to have the classes in the main source set like with the original solution from the blog post.

Correct, I did because before I did that the project wouldn’t compile because the class in the main source set doesn’t see my optional dependency.

Then my snippet probably is incomplete

Yep, this was missing:

val compileOnly by configurations.existing {
    extendsFrom(optionalImplementation.get())
}
1 Like