This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch fix/java-compat-jvm-args in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit c0d58dfda8ba838195e2a59c19247efba09e947c Author: James Fredley <[email protected]> AuthorDate: Thu Feb 19 17:03:45 2026 -0500 fix: add Java 23+/24+ compatibility JVM args and upgrade commons-lang3 to 3.20.0 Add version-conditional JVM arguments in GrailsGradlePlugin to suppress warnings on modern JDKs: - --sun-misc-unsafe-memory-access=allow for Java 23+ (JEP 471/498, #15343) - --enable-native-access=ALL-UNNAMED for Java 24+ (JEP 472, #15216) Override commons-lang3 from 3.17.0 (Spring Boot managed) to 3.20.0 in the Grails BOM to fix LANG-1786 timezone warnings and CVE-2025-48924. Includes Gradle TestKit functional tests verifying the args are applied correctly based on toolchain version. Assisted-by: Claude Code <[email protected]> --- dependencies.gradle | 2 + .../gradle/plugin/core/GrailsGradlePlugin.groovy | 74 ++++++++++++++- .../core/GrailsGradlePluginJavaCompatSpec.groovy | 105 +++++++++++++++++++++ .../java-compat-no-toolchain/build.gradle | 21 +++++ .../java-compat-no-toolchain/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../java-compat-no-toolchain/settings.gradle | 1 + .../java-compat-toolchain-23/build.gradle | 29 ++++++ .../java-compat-toolchain-23/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../java-compat-toolchain-23/settings.gradle | 1 + .../java-compat-toolchain-24/build.gradle | 29 ++++++ .../java-compat-toolchain-24/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../java-compat-toolchain-24/settings.gradle | 1 + .../java-compat-toolchain-current/build.gradle | 27 ++++++ .../gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../java-compat-toolchain-current/settings.gradle | 1 + 19 files changed, 294 insertions(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index b45d8a5309..37e5fbe7c6 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -71,6 +71,7 @@ ext { 'bootstrap-icons.version' : '1.13.1', 'bootstrap.version' : '5.3.8', 'commons-codec.version' : '1.18.0', + 'commons-lang3.version' : '3.20.0', 'geb-spock.version' : '8.0.1', 'groovy.version' : '4.0.30', 'jackson.version' : '2.19.1', @@ -100,6 +101,7 @@ ext { 'bootstrap' : "org.webjars.npm:bootstrap:${bomDependencyVersions['bootstrap.version']}", 'bootstrap-icons' : "org.webjars.npm:bootstrap-icons:${bomDependencyVersions['bootstrap-icons.version']}", 'commons-codec' : "commons-codec:commons-codec:${bomDependencyVersions['commons-codec.version']}", + 'commons-lang3' : "org.apache.commons:commons-lang3:${bomDependencyVersions['commons-lang3.version']}", 'geb-spock' : "org.apache.groovy.geb:geb-spock:${bomDependencyVersions['geb-spock.version']}", // start - restate the groovy-bom includes here because the spring dependency management will pick the library from spring-boot-dependencies otherwise 'groovy' : "org.apache.groovy:groovy:${bomDependencyVersions['groovy.version']}", diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy index 3cddfc86f3..84e008c110 100644 --- a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy +++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy @@ -31,6 +31,7 @@ import org.apache.tools.ant.filters.EscapeUnicode import org.apache.tools.ant.filters.ReplaceTokens import org.gradle.api.DefaultTask import org.gradle.api.GradleException +import org.gradle.api.JavaVersion import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project @@ -45,6 +46,7 @@ import org.gradle.api.file.FileCollection import org.gradle.api.file.RegularFile import org.gradle.api.plugins.ExtraPropertiesExtension import org.gradle.api.plugins.GroovyPlugin +import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.provider.Provider import org.gradle.api.tasks.AbstractCopyTask import org.gradle.api.tasks.JavaExec @@ -140,6 +142,8 @@ class GrailsGradlePlugin extends GroovyPlugin { configureForkSettings(project, grailsVersion) + configureJavaCompatibilityArgs(project) + configureGrailsSourceDirs(project) configureApplicationCommands(project) @@ -585,7 +589,7 @@ class GrailsGradlePlugin extends GroovyPlugin { */ protected void configureToolchainForForkTasks(Project project) { project.afterEvaluate { - def javaExtension = project.extensions.findByType(org.gradle.api.plugins.JavaPluginExtension) + def javaExtension = project.extensions.findByType(JavaPluginExtension) if (javaExtension?.toolchain?.languageVersion?.isPresent()) { def toolchainService = project.extensions.getByType(JavaToolchainService) def launcher = toolchainService.launcherFor(javaExtension.toolchain) @@ -597,6 +601,74 @@ class GrailsGradlePlugin extends GroovyPlugin { } } + /** + * Configures JVM arguments required for compatibility with Java 23+. + * + * <p>Java 24 introduced restrictions on native access ({@code JEP 472}) that cause + * warnings from libraries such as hawtjni (used by JLine) and Netty that call + * {@code System.loadLibrary} or declare native methods. The + * {@code --enable-native-access=ALL-UNNAMED} flag suppresses these warnings and + * will become mandatory in a future JDK release when the default changes to deny.</p> + * + * <p>Java 23 began terminal deprecation of {@code sun.misc.Unsafe} memory-access + * methods ({@code JEP 471/498}). Netty 4.1.x uses {@code Unsafe.allocateMemory} + * for off-heap buffers. The {@code --sun-misc-unsafe-memory-access=allow} flag + * suppresses the resulting warnings until Netty migrates to {@code MemorySegment} + * APIs (Netty 4.2+).</p> + * + * <p>Both flags are only added when the target JVM version (from the configured + * toolchain, or the JVM running Gradle if no toolchain is set) is high enough to + * recognize them, avoiding {@code Unrecognized option} errors on older JDKs.</p> + * + * @param project the Gradle project + * @see <a href="https://github.com/apache/grails-core/issues/15216">#15216 - Java 25 native access warnings</a> + * @see <a href="https://github.com/apache/grails-core/issues/15343">#15343 - sun.misc.Unsafe deprecation warnings</a> + * @since 7.0.8 + */ + protected void configureJavaCompatibilityArgs(Project project) { + project.afterEvaluate { + int targetVersion = resolveTargetJavaVersion(project) + + List<String> compatArgs = [] + + // JEP 472: Prepare to Restrict the Use of JNI - suppress native access warnings + // from hawtjni (JLine 2.x) and Netty calling System.loadLibrary / native methods + if (targetVersion >= 24) { + compatArgs.add('--enable-native-access=ALL-UNNAMED') + } + + // JEP 471/498: sun.misc.Unsafe memory-access terminal deprecation - suppress + // warnings from Netty's PlatformDependent0 using Unsafe.allocateMemory + if (targetVersion >= 23) { + compatArgs.add('--sun-misc-unsafe-memory-access=allow') + } + + if (compatArgs) { + project.tasks.withType(Test).configureEach { Test task -> + task.jvmArgs(compatArgs) + } + project.tasks.withType(JavaExec).configureEach { JavaExec task -> + task.jvmArgs(compatArgs) + } + } + } + } + + /** + * Resolves the Java version that forked tasks will use. Checks the project's + * toolchain configuration first, falling back to the JVM running Gradle. + * + * @param project the Gradle project + * @return the major Java version number (e.g. 17, 21, 24, 25) + */ + private int resolveTargetJavaVersion(Project project) { + JavaPluginExtension javaExtension = project.extensions.findByType(JavaPluginExtension) + if (javaExtension?.toolchain?.languageVersion?.isPresent()) { + return javaExtension.toolchain.languageVersion.get().asInt() + } + return JavaVersion.current().majorVersion.toInteger() + } + protected void configureConsoleTask(Project project) { TaskContainer tasks = project.tasks if (!project.configurations.names.contains('console')) { diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginJavaCompatSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginJavaCompatSpec.groovy new file mode 100644 index 0000000000..260b577c11 --- /dev/null +++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginJavaCompatSpec.groovy @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.gradle.plugin.core + +/** + * Tests that {@link GrailsGradlePlugin} applies Java compatibility JVM arguments + * conditionally based on the target Java version. + * + * <p>The plugin adds {@code --enable-native-access=ALL-UNNAMED} for Java 24+ + * (JEP 472) and {@code --sun-misc-unsafe-memory-access=allow} for Java 23+ + * (JEP 471/498) to both {@code Test} and {@code JavaExec} tasks.</p> + * + * @since 7.0.8 + * @see GrailsGradlePlugin#configureJavaCompatibilityArgs + */ +class GrailsGradlePluginJavaCompatSpec extends GradleSpecification { + + // ---------------------------------------------------------------- + // No compat args on current JDK (17) without toolchain + // ---------------------------------------------------------------- + + def "no Java compat args added when no toolchain configured on JDK 17"() { + given: + setupTestResourceProject('java-compat-no-toolchain') + + when: + def result = executeTask('inspectCompatArgs') + + then: + result.output.contains('HAS_NATIVE_ACCESS=false') + result.output.contains('HAS_UNSAFE_ACCESS=false') + } + + // ---------------------------------------------------------------- + // No compat args when toolchain matches current JDK (< 23) + // ---------------------------------------------------------------- + + def "no Java compat args added when toolchain is set to current JDK"() { + given: + setupTestResourceProject('java-compat-toolchain-current') + + when: + def result = executeTask('inspectCompatArgs') + + then: + result.output.contains('HAS_NATIVE_ACCESS=false') + result.output.contains('HAS_UNSAFE_ACCESS=false') + } + + // ---------------------------------------------------------------- + // Java 23 toolchain: only --sun-misc-unsafe-memory-access=allow + // ---------------------------------------------------------------- + + def "Java 23 toolchain adds sun-misc-unsafe-memory-access arg to JavaExec and Test tasks"() { + given: + setupTestResourceProject('java-compat-toolchain-23') + + when: + def result = executeTask('inspectCompatArgs') + + then: "sun.misc.Unsafe memory access flag is present" + result.output.contains('HAS_UNSAFE_ACCESS=true') + result.output.contains('TEST_HAS_UNSAFE_ACCESS=true') + + and: "native access flag is NOT present (only for 24+)" + result.output.contains('HAS_NATIVE_ACCESS=false') + result.output.contains('TEST_HAS_NATIVE_ACCESS=false') + } + + // ---------------------------------------------------------------- + // Java 24 toolchain: both flags + // ---------------------------------------------------------------- + + def "Java 24 toolchain adds both native-access and sun-misc-unsafe-memory-access args"() { + given: + setupTestResourceProject('java-compat-toolchain-24') + + when: + def result = executeTask('inspectCompatArgs') + + then: "both flags are present on JavaExec tasks" + result.output.contains('HAS_NATIVE_ACCESS=true') + result.output.contains('HAS_UNSAFE_ACCESS=true') + + and: "both flags are present on Test tasks" + result.output.contains('TEST_HAS_NATIVE_ACCESS=true') + result.output.contains('TEST_HAS_UNSAFE_ACCESS=true') + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/build.gradle new file mode 100644 index 0000000000..dd24336253 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +tasks.register('dummyJavaExec', JavaExec) { + classpath = files() + mainClass = 'does.not.Matter' +} + +tasks.register('inspectCompatArgs') { + doLast { + def javaExecTask = tasks.named('dummyJavaExec', JavaExec).get() + def testTask = tasks.named('test', Test).get() + def execArgs = javaExecTask.jvmArgs.join(',') + def testArgs = testTask.jvmArgs.join(',') + println "EXEC_ARGS=${execArgs}" + println "TEST_ARGS=${testArgs}" + println "HAS_NATIVE_ACCESS=${execArgs.contains('--enable-native-access=ALL-UNNAMED')}" + println "HAS_UNSAFE_ACCESS=${execArgs.contains('--sun-misc-unsafe-memory-access=allow')}" + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/gradle.properties new file mode 100644 index 0000000000..35c332fb87 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/grails-app/conf/application.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/settings.gradle new file mode 100644 index 0000000000..c426fa21f9 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-no-toolchain/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/build.gradle new file mode 100644 index 0000000000..61082b113c --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(23) + } +} + +tasks.register('dummyJavaExec', JavaExec) { + classpath = files() + mainClass = 'does.not.Matter' +} + +tasks.register('inspectCompatArgs') { + doLast { + def javaExecTask = tasks.named('dummyJavaExec', JavaExec).get() + def testTask = tasks.named('test', Test).get() + def execArgs = javaExecTask.jvmArgs.join(',') + def testArgs = testTask.jvmArgs.join(',') + println "EXEC_ARGS=${execArgs}" + println "TEST_ARGS=${testArgs}" + println "HAS_NATIVE_ACCESS=${execArgs.contains('--enable-native-access=ALL-UNNAMED')}" + println "HAS_UNSAFE_ACCESS=${execArgs.contains('--sun-misc-unsafe-memory-access=allow')}" + println "TEST_HAS_NATIVE_ACCESS=${testArgs.contains('--enable-native-access=ALL-UNNAMED')}" + println "TEST_HAS_UNSAFE_ACCESS=${testArgs.contains('--sun-misc-unsafe-memory-access=allow')}" + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/gradle.properties new file mode 100644 index 0000000000..35c332fb87 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/grails-app/conf/application.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/settings.gradle new file mode 100644 index 0000000000..c426fa21f9 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-23/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/build.gradle new file mode 100644 index 0000000000..0659157056 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(24) + } +} + +tasks.register('dummyJavaExec', JavaExec) { + classpath = files() + mainClass = 'does.not.Matter' +} + +tasks.register('inspectCompatArgs') { + doLast { + def javaExecTask = tasks.named('dummyJavaExec', JavaExec).get() + def testTask = tasks.named('test', Test).get() + def execArgs = javaExecTask.jvmArgs.join(',') + def testArgs = testTask.jvmArgs.join(',') + println "EXEC_ARGS=${execArgs}" + println "TEST_ARGS=${testArgs}" + println "HAS_NATIVE_ACCESS=${execArgs.contains('--enable-native-access=ALL-UNNAMED')}" + println "HAS_UNSAFE_ACCESS=${execArgs.contains('--sun-misc-unsafe-memory-access=allow')}" + println "TEST_HAS_NATIVE_ACCESS=${testArgs.contains('--enable-native-access=ALL-UNNAMED')}" + println "TEST_HAS_UNSAFE_ACCESS=${testArgs.contains('--sun-misc-unsafe-memory-access=allow')}" + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/gradle.properties new file mode 100644 index 0000000000..35c332fb87 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/grails-app/conf/application.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/settings.gradle new file mode 100644 index 0000000000..c426fa21f9 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-24/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/build.gradle new file mode 100644 index 0000000000..184b49389c --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(__CURRENT_JDK__) + } +} + +tasks.register('dummyJavaExec', JavaExec) { + classpath = files() + mainClass = 'does.not.Matter' +} + +tasks.register('inspectCompatArgs') { + doLast { + def javaExecTask = tasks.named('dummyJavaExec', JavaExec).get() + def testTask = tasks.named('test', Test).get() + def execArgs = javaExecTask.jvmArgs.join(',') + def testArgs = testTask.jvmArgs.join(',') + println "EXEC_ARGS=${execArgs}" + println "TEST_ARGS=${testArgs}" + println "HAS_NATIVE_ACCESS=${execArgs.contains('--enable-native-access=ALL-UNNAMED')}" + println "HAS_UNSAFE_ACCESS=${execArgs.contains('--sun-misc-unsafe-memory-access=allow')}" + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/gradle.properties new file mode 100644 index 0000000000..35c332fb87 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/grails-app/conf/application.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/settings.gradle new file mode 100644 index 0000000000..c426fa21f9 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/java-compat-toolchain-current/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain'
