Java ToolChain present in allProjects block is no applied to subproject unless "apply plugin: java" is in the block

I am working in multi project gradle build, and was tasked with modfying the project such that it supports both jdk 1.8 and jdk 17 at compile time and runtime, and binaries compiled in jdk 1.8 should be executable with jdk 17 runtime.

For this I introduced JVM toolchain (ref) in allprojects block in the root project, as shown in the below code snippet

allProjects {
java {
	println "Present in the java task which is running in the project = " + project.name + " with jdk version for compile = " + compileJavaVersion
	toolchain {
		// Set the language version, defaulting to JDK8.
		languageVersion = JavaLanguageVersion.of(compileJavaVersion)
		vendor = JvmVendorSpec.XYZ_ABC
             }
     }
}

and there was a unit test to check if the correct version of jvm is being used to execute the test or not

import org.junit.Assert;
import org.junit.Test;
import org.junit.experimental.categories.Category;

@Category(UnitTest.class)
public class JavaVersionTest {
@Test
public void JavaRuntimeVersionTest(){
	//Currently it should check if jdk17 is being used or not
	Assert.assertEquals("JDK version being used","17.0.6", System.getProperty("java.version"));
}
}

With the above setup the testcase was failing, implying that the wrong jdk version is being used.
When I moved the java extension block to a sub project where this test case was written and executed the sub project tests alone this test was successful.
I am unable to figure out why the above java extension is working in sub project but not the main project.
I tried applying the java plugin in the allProjects block, as shown in the snippet below

allProjects {
   apply plugin: 'java'   //change that was made for compilation in different verison to work
java {
	println "Present in the java task which is running in the project = " + project.name + " with jdk version for compile = " + compileJavaVersion
	toolchain {
		// Set the language version, defaulting to JDK8.
		languageVersion = JavaLanguageVersion.of(compileJavaVersion)
		vendor = JvmVendorSpec.XYZ_ABC
             }
     }
}

The correct version of java was being used, after making this change.
My question
Can anyone give inputs on why the compilation was not using the correct jdk version before but is using the correct jdk version after making the small change?
Additional information if required, and my thoughts
There are some sub projects where there are no java tasks and they are present for the purpose of aggregating mutliple files, is it possible that the java extension block has failed for these projects and hence the extension setting were failed to apply for all the other sub projects as well?

Any suggestions and help regarding this is deeply appreciated.
Thanks

is it possible that the java extension block has failed for these projects and hence the extension setting were failed to apply for all the other sub projects as well?

No, the problem is, that you only configure the java extension of the root project.
If you add:

println("FOO: ${System.identityHashCode(java)}")
allprojects {
    println("BAR: ${System.identityHashCode(java)}")
}

you will see that always the root extension is configured.
If you change it to:

println("FOO: ${System.identityHashCode(java)}")
allprojects {
    apply plugin: 'java'
    println("BAR: ${System.identityHashCode(java)}")
}

you see that the subprojects’ extensions are configured.
If you change it to (don’t do this in reality, just showcasing):

println("FOO: ${System.identityHashCode(java)}")
allprojects {
    afterEvaluate {
        println("BAR: ${System.identityHashCode(java)}")
    }
}

you also see that the subprojects’ extensions are configured.

At the time your extension configuration is done on the subprojects, the Java plugin is not applied yet and because of that, it configures the Java extension of the root project instead.
By applying the java plugin before or by delaying the execution (again, don’t do this) the extension in the subproject is already there and can be configured.

I have two recommendations for you.

  1. Consider switching from Groovy DSL to Kotlin DSL. This is by now the recommended default DSL, it immediately gives you type-safe build scripts, it gives you actually helpful error messages if you mess up the syntax, and you get amazingly better IDE support when using a proper IDE like IntelliJ IDEA.
  2. Do not use allprojects { ... }, subprojects { ... }, project(...) { ... } or similar. Those are bad practice and immediately introduce project coupling by doing cross-project configuration. This disturbs more sophisticated Gradle features and even prevents some upcoming features to work properly. Instead you should use convention plugins, for example in buildSrc or and included build, for example implemented as precompiled script plugins. Those you can then targetedly apply to the projects where their effect should be present, so that you for example only apply Java conventions to projects that are actually Java projects.

If you want to continue the bad practice of using allprojects { ... }, you could at least mitigate your problem by reacting to the java-base plugin being applied to the project in question which is the one that adds the Java extension like:

println("FOO: ${System.identityHashCode(java)}")
allprojects {
    pluginManager.withPlugin("java-base") {
        println("BAR: ${System.identityHashCode(java)}")
    }
}
1 Like

Thank You, Vampire for explaining and giving suggestions on how to procceed forward.
For now I dont think I would be able to follow the best practices you have suggested for now, would keep this in mind for future reference
Thank You

1 Like