Problem with compileClasspath after gradle 8 update

We have a project with many modules where we have centralized versions in a java platform module.

In gradle 7.6 this worked fine:

dependencies {
	compileClasspath enforcedPlatform(project(":project-dependencies"))
	compileOnly enforcedPlatform(project(":project-dependencies"))

	compileOnly 'org.mapstruct:mapstruct'

(Note: This is simplified: The enforcedPlatform dependencies are inside an allProjects block in the main build.gradle)

In gradle 7.6 this worked fine (without any deprecation warnings).

In gradle 8.0.1 this results in

An exception occurred applying plugin request [id: 'java']
> Failed to apply plugin 'org.gradle.java'.
   > Dependencies can not be declared against the `compileClasspath` configuration.

When I remove the compileClasspath enforcedPlatform(project(":project-dependencies")) this is what get:

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':sub-project:compileJava'.
> Could not resolve all files for configuration ':sub-project:compileClasspath'.
   > Could not find org.mapstruct:mapstruct:.
     Required by:
         project :sub-project

which is also strange, because the mapstruct dependency is in compileOnly configuration for which the platform dependency is present.

Also: I have not found any notes about “It is now impossible to define dependency against the compileClasspath configuration” in the update notes.

1 Like

If I try to delcare a dependency on compileClasspath with 7.6 like compileClasspath(enforcedPlatform("org.spockframework:spock-bom:2.3-groovy-4.0")), I get:

The compileClasspath configuration has been deprecated for dependency declaration. This will fail with an error in Gradle 8.0. Please use the implementation or api or compileOnly configuration instead. Consult the upgrading guide for further information: https://docs.gradle.org/7.6/userguide/upgrading_version_5.html#dependencies_should_no_longer_be_declared_using_the_compile_and_runtime_configurations
	at Build_gradle$1.invoke(build.gradle.kts:33)
	(Run with --stacktrace to get the full stack trace of this deprecation warning.)

Thanks, you are right, I missed that warning. Shame on me.

Yet, that does not help with my problem:
I declare a dependency to my java platform (which defines the mapstruct version) in the compileOnly configuration.
But still, when declaring the mapstruct dependency in the compileOnly configuration, gradle complains about missing mapstruct version in the compileClasspath configuration.

I just tried with

compileOnly(platform("org.spockframework:spock-bom:2.3-groovy-4.0"))
compileOnly("org.spockframework:spock-core")

and gw dependencies --configuration compileClasspath says for me

> Task :dependencies

------------------------------------------------------------
Root project 'showcase'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.spockframework:spock-bom:2.3-groovy-4.0
|    \--- org.spockframework:spock-core:2.3-groovy-4.0 (c)
\--- org.spockframework:spock-core -> 2.3-groovy-4.0
     +--- org.apache.groovy:groovy:4.0.4
     |    \--- org.apache.groovy:groovy-bom:4.0.4
     |         \--- org.apache.groovy:groovy:4.0.4 (c)
     +--- org.junit:junit-bom:5.9.0
     |    +--- org.junit.platform:junit-platform-engine:1.9.0 (c)
     |    \--- org.junit.platform:junit-platform-commons:1.9.0 (c)
     +--- org.junit.platform:junit-platform-engine -> 1.9.0
     |    +--- org.junit:junit-bom:5.9.0 (*)
     |    +--- org.opentest4j:opentest4j:1.2.0
     |    +--- org.junit.platform:junit-platform-commons:1.9.0
     |    |    +--- org.junit:junit-bom:5.9.0 (*)
     |    |    \--- org.apiguardian:apiguardian-api:1.1.2
     |    \--- org.apiguardian:apiguardian-api:1.1.2
     \--- org.hamcrest:hamcrest:2.2

Can you provide an MCVE where it does not work?

Trying to come up with an MVE was (of course) providing more insight.

Quite some time ago, we switched from the spring-dependency-management plugin to gradle’s Java platform (because the Spring plugin was impacting the build performance massively).
A nice feature of the Spring dependency management plugin is, that you can easily specify versions for dependencies in all configurations.

I had not found out how to do that with gradle’s platform mechanism (and still don’t know).
When trying to add the platform dependencies to specific configurations I was going nuts. There are a gazillion configurations all over our project and some need the platform dependency while other configurations break if they the platform dependency (for example “checkstyle” - no idea why).

I even asked about an easy way to add a platform to all sensible configurations (like the spring dependency management did), but got no answer. For some reason beyond me, the stackoverflow community bot deleted my question. You need to be me or have 10k reps to see it: https://stackoverflow.com/questions/66062899/how-to-add-versions-specified-in-a-java-platform-to-all-relevant-configurations

So the solution of “clever” me back then was to have a list of configurations that break when getting the platform dependency and then do:

allprojects {
	plugins.with {
		withType(JavaPlugin) {
			configurations.all { conf ->
					if (conf.canBeResolved && !(conf.name in excludedConfigurations)) {
						add(conf.name, enforcedPlatform(project(":project-dependencies")))
					}
				}

That did no longer work in gradle 8, because compileClasspath is resolvable (but you can no longer add dependencies to it) but implementationis not. Clearly I misunderstood what resolvable means.

I now added the platform explicitly to 29 configurations and it seems to work. (Well it doesn’t, actually, but that seems to be caused by a plugin that does not support gradle 8).

My question still stands:
What is the best way to mimic the spring-dependency-management plugin and add a platform dependency to all configurations that need it?

Trying to come up with an MVE was (of course) providing more insight.

Always part of the hope when asking for one :slight_smile:

because the Spring plugin was impacting the build performance massively

And because even the maintainers of the plugin tell not to use it but instead use Gradle built-in functionality instead. :slight_smile:

For some reason beyond me, the stackoverflow community bot deleted my question.

Here is explained why: The Community user deleted my question! What gives? - Help Center - Stack Overflow
You have now a second undelete vote, but you need a third one to get the question restored.

So the solution of “clever” me back then

Some immediate remarks:

  • allprojects { ... } and subprojects { ... } are evil and should be avoided, you should strongly consider using convention plugins instead.
  • the JavaDoc of project.plugins (actually project.getPlugins()) tells you not to use it, you should use pluginManager.withPlugin("java") { ... } instead, or maybe even pluginManager.withPlugin("java-base") { ... }.

Clearly I misunderstood what resolvable means.

Resolvable means it can be resolved.
Besides legacy configurations that can be consumed and resolved, there are three types.
Configurations that are just buckets for declaring dependencies like implementation, api, compileOnly, …
Configurations that are meant to be consumed from downstream projects like apiElements, runtimeElements, …
And configurations that are meant for local resolution like compileClasspath, runtimeClasspath, …

Not all plugins follow this strategy, but at least the Gradle built-in configurations should follow this quite consistently.

I now added the platform explicitly to 29 configurations and it seems to work.

… but … why?
Shouldn’t it be enough to e. g. declare it on implementation?
I think this way it should work on all necessary configurations.
compileClasspath, runtimeClasspath, testCompileClasspath, testRuntimeClasspath, …

Thanks a lot for the answer. I’ll have a look into our usage of allprojects/subprojects and project.plugins.

… but … why?

Unfortunately, there are a lot of configurations added by plugins or even by our own build (for example for JavaExec tasks). Some of these need the platform, but some break (with strange errors) when the platform gets added to them.

Here’s a list of the configurations; I currently use:

acmeBeanGenerator, annotationProcessor, bootArchives, codenarc, compileOnly, acmeWar, coverageDataElementsForTest, developmentOnly, acmePeStandard, filesystemResources, implementation, jaxb, loader, mainPmdAuxClasspath, mainSourceElements, overlay, pmdAux, productionRuntimeClasspath, providedCompile, providedRuntime, runtimeOnly, tar, testAnnotationProcessor, testCompileOnly, testImplementation, testPmdAuxClasspath, testResultsElementsForTest, testRuntimeOnly, war

Probably this list can be reduced if I figure out which configurations extend which others.
Also some of these configurations probably do not really need the platform, but it is very time consuming to figure that out (e.g. codenarc needs it, but pmd breaks the build if the platform is added to it).

Just to illustrate the point: Here are a couple of configurations that cause errors, when I add the platform to is:

  • checkstyle: ClassNotFoundException: com.puppycrawl.tools.checkstyle.ant.CheckstyleAntTask
  • jacocoAgent: IllegalStateException: Expected configuration ‘:claims:jacocoAgent’ to contain exactly one file, however, it contains no files.
  • pmd: ClassNotFoundException: net.sourceforge.pmd.PMD

Also, canBeResolved and canBeConsumed are no help in deciding whether to add the platform:
implementation is neither consumable nor resolvable, but codenarc is both (and also really needs the platform).

Also, canBeResolved and canBeConsumed are no help in deciding whether to add the platform:
implementation is neither consumable nor resolvable, but codenarc is both (and also really needs the platform).

As I said, the ones that are neither consumable nor resolvable are usually the ones where dependencies are declared on, ones that have one of the two set are the ones that just extend one of the former and are used for resolving or providing for consumption, and one where both are true are legacy. That the codenarc configuration has both true is probably just for backwards compatibility I guess, but I don’t know.

As I said, the ones that are neither consumable nor resolvable are usually the ones where dependencies are declared on,

Some of own configurations, are used like this:

configurations {
	acmeBeanGenerator
}

dependencies {
	acmeBeanGenerator 'com.acme.models:acme-bean-generator'
	...
}

task generateAcmeBeans(type: JavaExec) {
	classpath = configurations.acmeBeanGenerator
	...

I guess I can declare that configuration as not consumable, but it must be resolvable to be used in the classpathfor the JavaExec, right?

Exactly.
The even more idiomatic would be to have two configurations, one with both false to declare the dependency and one with consumable false that extends from it.

1 Like