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())
+    }
+}