You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/09/09 21:10:40 UTC
(groovy) branch master updated: GROOVY-12382: VMPluginFactory: tolerate a runtime without Runtime.version()
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 1fcee581c0 GROOVY-12382: VMPluginFactory: tolerate a runtime without Runtime.version()
1fcee581c0 is described below
commit 1fcee581c093c08475cf5ea3d98c717e1d5b795f
Author: Paul King <pa...@asert.com.au>
AuthorDate: Wed Sep 9 16:22:23 2026 +1000
GROOVY-12382: VMPluginFactory: tolerate a runtime without Runtime.version()
---
.../groovy/control/CompilerConfiguration.java | 3 +-
.../groovy/vmplugin/JavaFeatureVersion.java | 78 ++++++++++++++++++++++
.../codehaus/groovy/vmplugin/VMPluginFactory.java | 12 +++-
.../groovy/vmplugin/VMPluginFactoryTest.groovy | 67 +++++++++++++++++++
4 files changed, 158 insertions(+), 2 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java
index efbc160dd2..605f1babb6 100644
--- a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java
+++ b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java
@@ -27,6 +27,7 @@ import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.io.NullWriter;
import org.codehaus.groovy.control.messages.WarningMessage;
+import org.codehaus.groovy.vmplugin.JavaFeatureVersion;
import org.objectweb.asm.Opcodes;
import java.io.File;
@@ -1371,7 +1372,7 @@ public class CompilerConfiguration {
* @since 4.0.0
*/
private static String defaultTargetBytecode() {
- String javaVersion = Integer.toString(Runtime.version().feature());
+ String javaVersion = Integer.toString(JavaFeatureVersion.current());
if (JDK_TO_BYTECODE_VERSION_MAP.containsKey(javaVersion)) {
return javaVersion;
}
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/JavaFeatureVersion.java b/src/main/java/org/codehaus/groovy/vmplugin/JavaFeatureVersion.java
new file mode 100644
index 0000000000..725dfaeb19
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/vmplugin/JavaFeatureVersion.java
@@ -0,0 +1,78 @@
+/*
+ * 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
+ *
+ * http://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.codehaus.groovy.vmplugin;
+
+/**
+ * The Java feature version of the running VM, e.g. {@code 17} (GROOVY-12382).
+ * <p>
+ * {@code Runtime.version()} is a Java 9 API that some runtimes built on the
+ * JDK class library do not provide (Android's ART, for one). Where it is
+ * missing, the {@code java.specification.version} property is parsed instead,
+ * and when that is unusable the lowest release Groovy supports is assumed.
+ * <p>
+ * Deliberately tiny and free of lambdas: it is consulted from static
+ * initialisers that can run while the system class loader is being
+ * instantiated ({@code -Djava.system.class.loader=groovy.lang.GroovyClassLoader}),
+ * where an {@code invokedynamic} bootstrap is not yet permitted, and it must
+ * not trigger the creation of the VM plugin.
+ */
+public final class JavaFeatureVersion {
+
+ /** The lowest Java release Groovy runs on. */
+ public static final int MINIMUM = 17;
+
+ private JavaFeatureVersion() {
+ }
+
+ /**
+ * Returns the feature version of the running VM.
+ *
+ * @return the feature version, never below {@link #MINIMUM}
+ */
+ public static int current() {
+ try {
+ return Runtime.version().feature();
+ } catch (Throwable ignore) {
+ // NoSuchMethodError on a runtime without Runtime.version()
+ }
+ return parse(System.getProperty("java.specification.version"), MINIMUM);
+ }
+
+ /**
+ * Parses a {@code java.specification.version} value ({@code "1.8"},
+ * {@code "17"}, {@code "0.9"} on Android) into a feature version.
+ *
+ * @param specificationVersion the property value, possibly {@code null}
+ * @param minimum the value returned when the property is missing, unparseable or below it
+ * @return the feature version
+ */
+ public static int parse(final String specificationVersion, final int minimum) {
+ if (specificationVersion == null) return minimum;
+ String spec = specificationVersion.trim();
+ if (spec.startsWith("1.")) spec = spec.substring(2);
+ int dot = spec.indexOf('.');
+ if (dot > 0) spec = spec.substring(0, dot);
+ try {
+ int version = Integer.parseInt(spec);
+ return Math.max(version, minimum);
+ } catch (NumberFormatException e) {
+ return minimum;
+ }
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/VMPluginFactory.java b/src/main/java/org/codehaus/groovy/vmplugin/VMPluginFactory.java
index 5795799e83..98ce52beb9 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/VMPluginFactory.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/VMPluginFactory.java
@@ -44,7 +44,7 @@ public class VMPluginFactory {
private static final VMPlugin PLUGIN = createPlugin();
private static VMPlugin createPlugin() {
- int specVer = Runtime.version().feature();
+ int specVer = featureVersion();
for (Map.Entry<Integer, Supplier<VMPlugin>> entry : PLUGIN_MAP.entrySet()) {
if (specVer >= entry.getKey()) {
try {
@@ -61,6 +61,16 @@ public class VMPluginFactory {
return null;
}
+ /**
+ * The Java feature version of the running VM; see {@link JavaFeatureVersion#current()}.
+ *
+ * @return the feature version, never below the lowest supported release
+ * @since 6.0.0
+ */
+ public static int featureVersion() {
+ return JavaFeatureVersion.current();
+ }
+
/**
* Returns the VM plugin selected for the current runtime.
*
diff --git a/src/test/groovy/org/codehaus/groovy/vmplugin/VMPluginFactoryTest.groovy b/src/test/groovy/org/codehaus/groovy/vmplugin/VMPluginFactoryTest.groovy
new file mode 100644
index 0000000000..ea2f13fdde
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/vmplugin/VMPluginFactoryTest.groovy
@@ -0,0 +1,67 @@
+/*
+ * 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
+ *
+ * http://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.codehaus.groovy.vmplugin
+
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertNotNull
+import static org.junit.jupiter.api.Assertions.assertSame
+
+/**
+ * GROOVY-12382: the feature version must be available on runtimes without
+ * {@code Runtime.version()}, such as Android's ART, where the specification
+ * version property is the only clue and may itself be meaningless ("0.9").
+ */
+final class VMPluginFactoryTest {
+
+ @Test
+ void featureVersionMatchesTheRuntimeOnAJvm() {
+ assertEquals(Runtime.version().feature(), VMPluginFactory.featureVersion())
+ assertEquals(Runtime.version().feature(), JavaFeatureVersion.current())
+ assertEquals(17, JavaFeatureVersion.MINIMUM)
+ assertEquals(Integer.toString(Runtime.version().feature()), CompilerConfiguration.DEFAULT_TARGET_BYTECODE)
+ }
+
+ @Test
+ void parsesModernAndLegacySpecificationVersions() {
+ assertEquals(17, JavaFeatureVersion.parse('17', 17))
+ assertEquals(21, JavaFeatureVersion.parse('21', 17))
+ assertEquals(25, JavaFeatureVersion.parse(' 25 ', 17))
+ assertEquals(19, JavaFeatureVersion.parse('19.0.1', 17))
+ assertEquals(8, JavaFeatureVersion.parse('1.8', 8))
+ }
+
+ @Test
+ void neverReportsBelowTheLowestSupportedRelease() {
+ assertEquals(17, JavaFeatureVersion.parse('1.8', 17))
+ assertEquals(17, JavaFeatureVersion.parse('0.9', 17)) // Android
+ assertEquals(17, JavaFeatureVersion.parse('', 17))
+ assertEquals(17, JavaFeatureVersion.parse('banana', 17))
+ assertEquals(17, JavaFeatureVersion.parse(null, 17))
+ }
+
+ @Test
+ void pluginIsSelected() {
+ VMPlugin plugin = VMPluginFactory.getPlugin()
+ assertNotNull(plugin)
+ assertSame(plugin, VMPluginFactory.getPlugin())
+ }
+}