Skip to content

Commit 63beaf3

Browse files
authored
Merge pull request #15403 from jamesfredley/fix/javaexec-toolchain-inheritance
Propagate Java toolchain to JavaExec tasks
2 parents dd033e5 + 5d7c861 commit 63beaf3

File tree

37 files changed

+504
-0
lines changed

37 files changed

+504
-0
lines changed

grails-gradle/gradle/test-config.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ tasks.withType(Test).configureEach {
4040
}
4141

4242
useJUnitPlatform()
43+
systemProperty 'projectVersion', projectVersion
44+
systemProperty 'currentJdk', JavaVersion.current().majorVersion
4345
jvmArgs += java17moduleReflectionCompatibilityArguments
4446
testLogging {
4547
events('passed', 'skipped', 'failed')

grails-gradle/plugins/build.gradle

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ dependencies {
5656
implementation 'org.springframework.boot:spring-boot-gradle-plugin'
5757
implementation 'org.springframework.boot:spring-boot-loader-tools'
5858
implementation 'io.spring.gradle:dependency-management-plugin'
59+
60+
// Testing - Gradle TestKit is auto-added by java-gradle-plugin
61+
testImplementation('org.spockframework:spock-core') { transitive = false }
62+
testImplementation 'org.codehaus.groovy:groovy-test-junit5'
63+
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
64+
}
65+
66+
configurations {
67+
testCompileClasspath.exclude group: 'org.apache.groovy', module: 'groovy'
68+
testRuntimeClasspath.exclude group: 'org.apache.groovy', module: 'groovy'
5969
}
6070

6171
gradlePlugin {
@@ -131,4 +141,5 @@ tasks.withType(Copy) {
131141

132142
apply {
133143
from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle')
144+
from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
134145
}

grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import org.gradle.api.tasks.TaskContainer
5454
import org.gradle.api.tasks.TaskProvider
5555
import org.gradle.api.tasks.compile.GroovyCompile
5656
import org.gradle.api.tasks.testing.Test
57+
import org.gradle.jvm.toolchain.JavaToolchainService
5758
import org.gradle.language.jvm.tasks.ProcessResources
5859
import org.gradle.process.JavaForkOptions
5960
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
@@ -556,6 +557,44 @@ class GrailsGradlePlugin extends GroovyPlugin {
556557
String grailsEnvSystemProperty = System.getProperty(Environment.KEY)
557558
tasks.withType(Test).configureEach(systemPropertyConfigurer.curry(grailsEnvSystemProperty ?: Environment.TEST.getName()))
558559
tasks.withType(JavaExec).configureEach(systemPropertyConfigurer.curry(grailsEnvSystemProperty ?: Environment.DEVELOPMENT.getName()))
560+
561+
configureToolchainForForkTasks(project)
562+
}
563+
564+
/**
565+
* Configures {@link JavaExec} tasks to inherit the project's Java toolchain.
566+
*
567+
* <p>Gradle's {@code JavaPlugin} already sets toolchain conventions on
568+
* {@code JavaCompile}, {@code Javadoc}, and {@code Test} tasks, but does
569+
* <strong>not</strong> set them on {@code JavaExec} tasks. This means forked
570+
* JVM processes (dbm-* migration tasks, console, shell, and application
571+
* context commands) use the JDK running Gradle instead of the project's
572+
* configured toolchain. When the project targets a different JDK version
573+
* than the one running Gradle, this causes {@code UnsupportedClassVersionError}
574+
* or silent runtime failures.</p>
575+
*
576+
* <p>This method only acts when the user has explicitly configured a toolchain
577+
* via {@code java.toolchain.languageVersion}. When no toolchain is configured,
578+
* behavior is unchanged - tasks use the JDK running Gradle as before.</p>
579+
*
580+
* <p>Uses {@code convention()} so that individual tasks can still override
581+
* the launcher via {@code javaLauncher.set(...)} if needed.</p>
582+
*
583+
* @param project the Gradle project
584+
* @since 7.0.8
585+
*/
586+
protected void configureToolchainForForkTasks(Project project) {
587+
project.afterEvaluate {
588+
def javaExtension = project.extensions.findByType(org.gradle.api.plugins.JavaPluginExtension)
589+
if (javaExtension?.toolchain?.languageVersion?.isPresent()) {
590+
def toolchainService = project.extensions.getByType(JavaToolchainService)
591+
def launcher = toolchainService.launcherFor(javaExtension.toolchain)
592+
593+
project.tasks.withType(JavaExec).configureEach { JavaExec task ->
594+
task.javaLauncher.convention(launcher)
595+
}
596+
}
597+
}
559598
}
560599

561600
protected void configureConsoleTask(Project project) {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.grails.gradle.plugin.core
20+
21+
import org.gradle.testkit.runner.BuildResult
22+
import org.gradle.testkit.runner.GradleRunner
23+
import org.gradle.testkit.runner.TaskOutcome
24+
import spock.lang.Specification
25+
26+
import java.nio.file.FileVisitResult
27+
import java.nio.file.Files
28+
import java.nio.file.Path
29+
import java.nio.file.SimpleFileVisitor
30+
import java.nio.file.attribute.BasicFileAttributes
31+
32+
/**
33+
* Base class for Gradle plugin functional tests using TestKit.
34+
*
35+
* <p>Adapted from the {@code GradleSpecification} in the
36+
* {@code apache/grails-gradle-publish} project. Handles temp directory
37+
* management, GradleRunner setup, test resource project copying, and
38+
* common build assertions.</p>
39+
*
40+
* @since 7.0.8
41+
*/
42+
abstract class GradleSpecification extends Specification {
43+
44+
private static Path basePath
45+
private static GradleRunner gradleRunner
46+
47+
/** Project version injected by Gradle test config. */
48+
protected static final String PROJECT_VERSION = System.getProperty('projectVersion')
49+
50+
/** Current JDK major version injected by Gradle test config. */
51+
protected static final int CURRENT_JDK = Integer.parseInt(System.getProperty('currentJdk'))
52+
53+
void setupSpec() {
54+
basePath = Files.createTempDirectory('gradle-projects')
55+
Path testKitDir = Files.createDirectories(basePath.resolve('.gradle'))
56+
gradleRunner = GradleRunner.create()
57+
.withPluginClasspath()
58+
.withTestKitDir(testKitDir.toFile())
59+
}
60+
61+
void cleanup() {
62+
basePath?.toFile()?.listFiles()?.each {
63+
if (it.name == '.gradle') {
64+
return
65+
}
66+
it.deleteDir()
67+
}
68+
}
69+
70+
void cleanupSpec() {
71+
basePath?.toFile()?.deleteDir()
72+
}
73+
74+
/**
75+
* Sets up a test project from resource files under
76+
* {@code src/test/resources/test-projects/{projectName}}.
77+
*
78+
* <p>Files are copied to a temp directory. Any occurrence of
79+
* {@code __CURRENT_JDK__} in {@code .gradle} files is replaced
80+
* with the actual current JDK version, and {@code __PROJECT_VERSION__}
81+
* is replaced with the actual project version.</p>
82+
*/
83+
protected GradleRunner setupTestResourceProject(String projectName) {
84+
Path destination = basePath.resolve(projectName)
85+
Files.createDirectories(destination)
86+
87+
Path source = Path.of("src/test/resources/test-projects/${projectName}")
88+
copyDirectory(source, destination)
89+
90+
gradleRunner.withProjectDir(destination.toFile())
91+
}
92+
93+
/**
94+
* Executes a Gradle task and returns the build result.
95+
*/
96+
protected BuildResult executeTask(String taskName, List<String> otherArgs = []) {
97+
List<String> args = [taskName, '--stacktrace']
98+
args.addAll(otherArgs)
99+
gradleRunner.withArguments(args).forwardOutput().build()
100+
}
101+
102+
/**
103+
* Asserts that the given task succeeded.
104+
*/
105+
protected void assertTaskSuccess(String taskName, BuildResult result) {
106+
def task = result.tasks.find { it.path.endsWith(":${taskName}") }
107+
assert task != null : "Task '${taskName}' not found in build result"
108+
assert task.outcome == TaskOutcome.SUCCESS : "Task '${taskName}' outcome was ${task.outcome}"
109+
}
110+
111+
private void copyDirectory(Path source, Path destination) {
112+
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
113+
@Override
114+
FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
115+
Files.createDirectories(destination.resolve(source.relativize(dir)))
116+
FileVisitResult.CONTINUE
117+
}
118+
119+
@Override
120+
FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
121+
Path target = destination.resolve(source.relativize(file))
122+
if (file.toString().endsWith('.gradle') || file.toString().endsWith('.properties')) {
123+
String content = Files.readString(file)
124+
.replace('__CURRENT_JDK__', String.valueOf(CURRENT_JDK))
125+
.replace('__PROJECT_VERSION__', PROJECT_VERSION)
126+
Files.writeString(target, content)
127+
} else {
128+
Files.copy(file, target)
129+
}
130+
FileVisitResult.CONTINUE
131+
}
132+
})
133+
}
134+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.grails.gradle.plugin.core
20+
21+
/**
22+
* Tests that {@link GrailsGradlePlugin} propagates the project's Java toolchain
23+
* to JavaExec tasks via {@code javaLauncher.convention()}.
24+
*
25+
* <p>Without the fix, JavaExec tasks spawned by Grails (dbm-* migration
26+
* commands, console, shell, application context commands) use the JDK
27+
* running Gradle instead of the project's configured toolchain.</p>
28+
*
29+
* @since 7.0.8
30+
* @see GrailsGradlePlugin#configureToolchainForForkTasks
31+
*/
32+
class GrailsGradlePluginToolchainSpec extends GradleSpecification {
33+
34+
// ----------------------------------------------------------------
35+
// Toolchain propagation
36+
// ----------------------------------------------------------------
37+
38+
def "JavaExec tasks inherit project toolchain"() {
39+
given:
40+
setupTestResourceProject('toolchain-javaexec')
41+
42+
when:
43+
def result = executeTask('checkToolchain')
44+
45+
then:
46+
result.output.contains("TOOLCHAIN_VERSION=${CURRENT_JDK}")
47+
}
48+
49+
def "Test tasks inherit project toolchain"() {
50+
given:
51+
setupTestResourceProject('toolchain-test')
52+
53+
when:
54+
def result = executeTask('checkTestToolchain')
55+
56+
then:
57+
result.output.contains("TEST_TOOLCHAIN_VERSION=${CURRENT_JDK}")
58+
}
59+
60+
def "ApplicationContextCommandTask inherits toolchain via grails-web plugin"() {
61+
given:
62+
setupTestResourceProject('toolchain-command')
63+
64+
when:
65+
def result = executeTask('checkCommandToolchain')
66+
67+
then:
68+
result.output.contains("CMD_TOOLCHAIN_VERSION=${CURRENT_JDK}")
69+
}
70+
71+
def "convention allows individual task override via set()"() {
72+
given:
73+
setupTestResourceProject('toolchain-override')
74+
75+
when:
76+
def result = executeTask('checkOverride')
77+
78+
then:
79+
result.output.contains("OVERRIDE_VERSION=${CURRENT_JDK}")
80+
}
81+
82+
// ----------------------------------------------------------------
83+
// Backwards compatibility (no toolchain configured)
84+
// ----------------------------------------------------------------
85+
86+
def "JavaExec tasks work without errors when no toolchain configured"() {
87+
given:
88+
setupTestResourceProject('no-toolchain-javaexec')
89+
90+
when:
91+
def result = executeTask('checkToolchain')
92+
93+
then:
94+
result.output.contains('HAS_LAUNCHER=')
95+
}
96+
97+
def "GrailsWebGradlePlugin works without errors when no toolchain configured"() {
98+
given:
99+
setupTestResourceProject('no-toolchain-web')
100+
101+
when:
102+
def result = executeTask('checkNoError')
103+
104+
then:
105+
result.output.contains('WEB_PLUGIN_OK=true')
106+
}
107+
108+
// ----------------------------------------------------------------
109+
// Fork settings preservation
110+
// ----------------------------------------------------------------
111+
112+
def "configureForkSettings applies system properties and default heap sizes"() {
113+
given:
114+
setupTestResourceProject('fork-settings-defaults')
115+
116+
when:
117+
def result = executeTask('inspectSysProps')
118+
119+
then:
120+
result.output.contains('HAS_ENV=true')
121+
result.output.contains('MIN_HEAP=768m')
122+
result.output.contains('MAX_HEAP=768m')
123+
}
124+
125+
def "custom heap sizes are not overridden by fork settings"() {
126+
given:
127+
setupTestResourceProject('fork-settings-custom')
128+
129+
when:
130+
def result = executeTask('inspectHeap')
131+
132+
then:
133+
result.output.contains('MIN_HEAP=512m')
134+
result.output.contains('MAX_HEAP=2g')
135+
}
136+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
plugins {
2+
id 'org.apache.grails.gradle.grails-app'
3+
}
4+
5+
tasks.register('customHeap', JavaExec) {
6+
classpath = files()
7+
mainClass = 'does.not.Matter'
8+
minHeapSize = '512m'
9+
maxHeapSize = '2g'
10+
}
11+
12+
tasks.register('inspectHeap') {
13+
doLast {
14+
def task = tasks.named('customHeap', JavaExec).get()
15+
println "MIN_HEAP=${task.minHeapSize}"
16+
println "MAX_HEAP=${task.maxHeapSize}"
17+
}
18+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
grailsVersion=__PROJECT_VERSION__

grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/grails-app/conf/application.yml

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'test-toolchain'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
plugins {
2+
id 'org.apache.grails.gradle.grails-app'
3+
}
4+
5+
tasks.register('checkSysProps', JavaExec) {
6+
classpath = files()
7+
mainClass = 'does.not.Matter'
8+
}
9+
10+
tasks.register('inspectSysProps') {
11+
doLast {
12+
def task = tasks.named('checkSysProps', JavaExec).get()
13+
def sysProps = task.systemProperties
14+
println "HAS_ENV=${sysProps.containsKey('grails.env')}"
15+
println "MIN_HEAP=${task.minHeapSize}"
16+
println "MAX_HEAP=${task.maxHeapSize}"
17+
}
18+
}

0 commit comments

Comments
 (0)