Propagate Java toolchain to JavaExec tasks#15403
Conversation
Gradle's JavaPlugin sets toolchain conventions on JavaCompile, Javadoc, and Test tasks but not on JavaExec tasks. This means forked JVM processes (dbm-* migration tasks, console, shell, application context commands) use the JDK running Gradle instead of the project's configured toolchain. Add configureToolchainForForkTasks() to GrailsGradlePlugin that propagates the project toolchain to all JavaExec tasks via javaLauncher.convention(). Uses convention() so individual tasks can still override via set(). Only activates when java.toolchain.languageVersion is explicitly configured, preserving existing behavior for users without toolchains. Test coverage: 8 JUnit Jupiter tests via Gradle TestKit covering toolchain propagation (JavaExec, Test, ApplicationContextCommandTask, convention override), backwards compatibility (no-toolchain), and fork settings preservation (system properties, heap sizes).
There was a problem hiding this comment.
Pull request overview
This PR updates the Grails Gradle plugin so JavaExec-based tasks (console/shell/dbm/application context commands) inherit the project’s configured Java toolchain, avoiding mismatches between the Gradle daemon JDK and the project toolchain.
Changes:
- Add
configureToolchainForForkTasks(Project)to apply the project toolchain as ajavaLauncherconvention for allJavaExectasks when a toolchain languageVersion is configured. - Add Gradle TestKit JUnit Jupiter tests validating toolchain propagation and preserving existing fork settings behavior.
- Update the plugins module build to support JUnit Jupiter tests (and apply shared test Gradle configuration).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy |
Propagates Java toolchain to JavaExec tasks via javaLauncher.convention(...). |
grails-gradle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java |
Adds TestKit coverage for toolchain propagation + fork settings preservation. |
grails-gradle/plugins/build.gradle |
Adds JUnit Jupiter dependencies and applies shared test configuration script. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...dle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java
Outdated
Show resolved
Hide resolved
| * Reads {@code projectVersion} from the root {@code gradle.properties} so the | ||
| * test stays in sync with the actual build and is not tied to a hardcoded version. | ||
| */ | ||
| private static String resolveGrailsVersion() { |
There was a problem hiding this comment.
Let's pass this value via the gradle configuration of the Test itself? No reason to be dynamically looking this up when it's available at the time of configuration in the test task.
There was a problem hiding this comment.
Done. projectVersion and currentJdk are now passed as system properties from the test task configuration in build.gradle, and read via System.getProperty() in the GradleSpecification base class. No more runtime discovery.
...dle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java
Outdated
Show resolved
Hide resolved
| Files.writeString(projectDir.resolve("gradle.properties"), "grailsVersion=" + GRAILS_VERSION + "\n"); | ||
| } | ||
|
|
||
| private BuildResult runBuild(String... args) { |
There was a problem hiding this comment.
There's a pretty good base class in the grails publish project that is meant to handle a lot of this boilerplate. Let's just copy it to grails-core
There was a problem hiding this comment.
Done. Added GradleSpecification base class adapted from grails-gradle-publish. It handles temp dir setup, GradleRunner configuration, and copying test project fixtures from src/test/resources/test-projects/ with placeholder replacement for __PROJECT_VERSION__ and __CURRENT_JDK__.
| @Test | ||
| @DisplayName("JavaExec tasks inherit project toolchain") | ||
| void javaExecInheritsToolchain() throws IOException { | ||
| Files.writeString(projectDir.resolve("build.gradle"), |
There was a problem hiding this comment.
If you copy the spec from grails publish, these can be actual files checked in that are easy to modify over time instead of being hard coded as strings
There was a problem hiding this comment.
Done. All 8 test scenarios are now file-based fixtures under src/test/resources/test-projects/ (toolchain-javaexec, toolchain-test, toolchain-command, toolchain-override, no-toolchain-javaexec, no-toolchain-web, fork-settings-defaults, fork-settings-custom). Each has its own build.gradle, settings.gradle, gradle.properties, and grails-app/conf/application.yml.
| * | ||
| * @since 7.0.8 | ||
| */ | ||
| class GrailsGradlePluginToolchainTest { |
There was a problem hiding this comment.
Done. Rewrote the entire test as GrailsGradlePluginToolchainSpec extending GradleSpecification. Resolved the Groovy conflict by excluding org.apache.groovy from the test classpath (see comment on build.gradle). All 8 tests pass as Spock specs.
grails-gradle/plugins/build.gradle
Outdated
|
|
||
| // Testing - Gradle TestKit is auto-added by java-gradle-plugin | ||
| // NOTE: Cannot use Spock here because grails-gradle-tasks transitively | ||
| // pulls org.apache.groovy:groovy:4.0.30 which conflicts with |
There was a problem hiding this comment.
why is grails-gradle-tasks using groovy 4? just exclude it since we target groovy 3 with gradle.
There was a problem hiding this comment.
grails-gradle-tasks/build.gradle line 42 explicitly declares implementation 'org.apache.groovy:groovy:4.0.30' because its source code uses Groovy 4 APIs (it's not a Gradle plugin itself, just utility classes consumed by the plugins module). Added an exclusion of the org.apache.groovy group from the test configurations in plugins/build.gradle so Spock runs against Gradle's bundled Groovy 3 without conflicts.
Address reviewer feedback: migrate from JUnit Jupiter to Spock, replace inline build scripts with file-based test project fixtures, add GradleSpecification base class for Gradle TestKit tests, pass projectVersion via Gradle test config, and exclude Groovy 4 from test classpath to resolve the Spock/Gradle Groovy conflict. Assisted-by: Claude Code <Claude@Claude.ai>
All comments addressed in latest commit
grails-gradle/plugins/build.gradle
Outdated
| } | ||
| } | ||
|
|
||
| tasks.withType(Test).configureEach { |
There was a problem hiding this comment.
I'd suggest we move this to test-config. The additional properties don't hurt and it' sbest to be consistent.
There was a problem hiding this comment.
Done. Moved projectVersion and currentJdk system properties into test-config.gradle and removed the standalone block from plugins/build.gradle. All 8 tests pass.
jdaugherty
left a comment
There was a problem hiding this comment.
Minor comment but otherwise LGTM
Move projectVersion and currentJdk system properties from the plugins build.gradle into the shared test-config.gradle for consistency across all grails-gradle modules. Assisted-by: Claude Code <Claude@Claude.ai>
Summary
GrailsGradlePlugin.configureForkSettings()configures allJavaExectasks with system properties, heap sizes, and JVM args, but does not propagate the project's Java toolchain. Gradle'sJavaPluginsets toolchain conventions onJavaCompile,Javadoc, andTesttasks but not onJavaExectasks. This means forked JVM processes spawned by Grails (dbm-* migration tasks, console, shell, and application context commands) use the JDK running Gradle instead of the project's configured toolchain.Bug Description
When a Grails project configures a Java toolchain:
java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }JavaCompileandTesttasks correctly use JDK 17, but allJavaExec-based tasks (includingApplicationContextCommandTasksubclasses like dbm-* migration commands, console, and shell) silently use whatever JDK is running the Gradle daemon.This causes
UnsupportedClassVersionErrorwhen the project targets a newer JDK than the one running Gradle, or silent runtime failures when targeting an older JDK with different API behavior.Reproduction
Affected Tasks
All
JavaExec-based tasks registered by Grails plugins:GrailsGradlePluginJavaExecGrailsWebGradlePluginApplicationContextCommandTaskApplicationContextCommandTaskApplicationContextCommandTaskRoot Cause
Gradle's
JavaPlugin(viaJavaPluginExtension.configureToolchain()) only callsconfigureToolchain()on:JavaCompiletasks -javaCompiler.convention(compiler)Javadoctasks -javadocTool.convention(javadocTool)Testtasks -javaLauncher.convention(launcher)JavaExectasks are not included, despite also having ajavaLauncherproperty. This is a known Gradle behavior (not a Gradle bug per se, but a gap that framework plugins need to fill).GrailsGradlePlugin.configureForkSettings()already iterates over allJavaExectasks to set system properties and heap sizes, making it the natural place to also propagate the toolchain.Fix
Added
configureToolchainForForkTasks(Project project)method toGrailsGradlePlugin, called at the end ofconfigureForkSettings():Design decisions
JavaExectasks -Testtasks are already handled by Gradle'sJavaPluginconvention()notset()- allows individual tasks to override viajavaLauncher.set(...)if neededlanguageVersion?.isPresent()- only activates when the user explicitly configures a toolchain; no-toolchain projects get zero behavior changeafterEvaluate- ensures the toolchain configuration is fully resolved before reading itJavaToolchainServicefrom project extensions - avoids changing the plugin constructor signatureFiles changed
grails-gradle/plugins/.../GrailsGradlePlugin.groovyconfigureToolchainForForkTasks()method (35 lines including Javadoc), called fromconfigureForkSettings()grails-gradle/plugins/build.gradlegrails-gradle/plugins/src/test/.../GrailsGradlePluginToolchainTest.javaTest Coverage
8 JUnit Jupiter tests via Gradle TestKit (cannot use Spock due to Groovy 3/4 capability conflict in the
pluginsmodule -grails-gradle-taskstransitively pullsorg.apache.groovy:groovy:4.0.30which conflicts with Gradle's built-inorg.codehaus.groovy:groovy:3.0.25):Toolchain propagation (4 tests)
javaExecInheritsToolchainJavaExectask getsjavaLauncherconvention from project toolchaintestTaskInheritsToolchainTesttask toolchain still works (regression check for Gradle's built-in behavior)applicationContextCommandTaskInheritsToolchainurlMappingsReport(ApplicationContextCommandTask extends JavaExec) inherits toolchain viagrails-webpluginconventionAllowsOverridejavaLauncher.set(...)on individual task overrides the conventionBackwards compatibility (2 tests)
javaExecWorksWithoutToolchainwebPluginWorksWithoutToolchainGrailsWebGradlePluginapplies without errors when no toolchain is configuredFork settings preservation (2 tests)
forkSettingsApplyDefaultsgrails.envsystem property and 768m heap defaults still applied toJavaExectaskscustomHeapSizesPreservedminHeapSize/maxHeapSizeon individual tasks not overridden by fork settingsExample Application
Repository: https://github.com/jamesfredley/grails-javaexec-toolchain-bug
A minimal Grails 7 app with a
checkToolchaintask that prints the JDK version and whether it matches the configured toolchain. Without the fix,matchesToolchain=falsewhen the Gradle daemon JDK differs from the toolchain target.Run instructions
git clone https://github.com/jamesfredley/grails-javaexec-toolchain-bug.git cd grails-javaexec-toolchain-bug ./gradlew checkToolchainEnvironment Information
Version
7.0.x