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 0558aef6cb GROOVY-12149: Nondeterministic reflection order flows into 
generated bytecode, breaking reproducible builds
0558aef6cb is described below

commit 0558aef6cb41761551b8248e092fe2f30a0ce968
Author: James Daugherty <[email protected]>
AuthorDate: Sat Jul 11 12:58:14 2026 -0400

    GROOVY-12149: Nondeterministic reflection order flows into generated 
bytecode, breaking reproducible builds
    
    (cherry picked from commit 7aabc479f6bc03a6a6c5ecd31fc2c66d2cfb8e21)
---
 .../groovy/reflection/ReflectionUtils.java         | 66 ++++++++++++++++++++++
 .../org/codehaus/groovy/vmplugin/v8/Java8.java     | 10 +---
 2 files changed, 69 insertions(+), 7 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/reflection/ReflectionUtils.java 
b/src/main/java/org/codehaus/groovy/reflection/ReflectionUtils.java
index 0bccdec6d4..5f7a94e912 100644
--- a/src/main/java/org/codehaus/groovy/reflection/ReflectionUtils.java
+++ b/src/main/java/org/codehaus/groovy/reflection/ReflectionUtils.java
@@ -27,8 +27,10 @@ import java.lang.invoke.MethodHandles;
 import java.lang.invoke.MethodType;
 import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedList;
@@ -134,6 +136,70 @@ public class ReflectionUtils {
         return doGetMethods(type, name, parameterTypes, 
Class::getDeclaredMethods);
     }
 
+    /**
+     * Returns the declared methods of a class in a deterministic order.
+     * <p>
+     * {@link Class#getDeclaredMethods()} does not guarantee an order, and
+     * HotSpot's varies between JVM runs. When members of a precompiled class
+     * are enumerated at compile time (annotation members copied to generated
+     * code, trait methods woven into implementing classes, MOP {@code super$}
+     * bridge methods, etc.), that order flows into the generated bytecode, so
+     * a nondeterministic order produces byte-different class files from
+     * identical sources. Callers whose output depends on member order should
+     * use this variant so builds are reproducible.
+     *
+     * @param type the class to introspect
+     * @return the declared methods, sorted by name then signature
+     * @since 5.0.8
+     */
+    public static Method[] getDeclaredMethodsSorted(final Class<?> type) {
+        Method[] methods = type.getDeclaredMethods();
+        Arrays.sort(methods, ReflectionUtils::compareMethods);
+        return methods;
+    }
+
+    /**
+     * Returns the declared constructors of a class in a deterministic order.
+     * <p>
+     * {@link Class#getDeclaredConstructors()} does not guarantee an order, and
+     * a nondeterministic order can flow into generated bytecode; see
+     * {@link #getDeclaredMethodsSorted(Class)}.
+     *
+     * @param type the class to introspect
+     * @return the declared constructors, sorted by parameter types
+     * @since 5.0.8
+     */
+    public static Constructor<?>[] getDeclaredConstructorsSorted(final 
Class<?> type) {
+        Constructor<?>[] constructors = type.getDeclaredConstructors();
+        // name is always <init> and the return type always void, so the 
parameters decide
+        Arrays.sort(constructors, (c1, c2) ->
+            compareParameterTypes(c1.getParameterTypes(), 
c2.getParameterTypes()));
+        return constructors;
+    }
+
+    /**
+     * Orders by name, then parameter types, then return type. This is a total 
order:
+     * the JVM forbids two methods in one class sharing a name and descriptor, 
and the
+     * descriptor is exactly the parameter types plus the return type.
+     */
+    private static int compareMethods(final Method m1, final Method m2) {
+        int c = m1.getName().compareTo(m2.getName());
+        if (c != 0) return c;
+        c = compareParameterTypes(m1.getParameterTypes(), 
m2.getParameterTypes());
+        if (c != 0) return c;
+        return 
m1.getReturnType().getName().compareTo(m2.getReturnType().getName());
+    }
+
+    private static int compareParameterTypes(final Class<?>[] p1, final 
Class<?>[] p2) {
+        int c = Integer.compare(p1.length, p2.length);
+        if (c != 0) return c;
+        for (int i = 0; i < p1.length; i += 1) {
+            c = p1[i].getName().compareTo(p2[i].getName());
+            if (c != 0) return c;
+        }
+        return 0;
+    }
+
     public static List<Method> getMethods(final Class<?> type, final String 
name, final Class<?>... parameterTypes) {
         return doGetMethods(type, name, parameterTypes, Class::getMethods);
     }
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java 
b/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java
index ed551a2419..124aa94fbd 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java
@@ -67,7 +67,6 @@ import java.lang.reflect.Type;
 import java.lang.reflect.TypeVariable;
 import java.lang.reflect.WildcardType;
 import java.util.Arrays;
-import java.util.Comparator;
 import java.util.List;
 
 /**
@@ -284,10 +283,7 @@ public class Java8 implements VMPlugin {
         } else {
             Method[] declaredMethods;
             try {
-                declaredMethods = 
annotation.annotationType().getDeclaredMethods();
-                // getDeclaredMethods does not guarantee an order, and 
HotSpot's varies between JVM
-                // runs; sort so the annotation is emitted deterministically 
for reproducible builds
-                Arrays.sort(declaredMethods, 
Comparator.comparing(Method::getName));
+                declaredMethods = 
ReflectionUtils.getDeclaredMethodsSorted(annotation.annotationType());
             } catch (SecurityException se) {
                 declaredMethods = EMPTY_METHOD_ARRAY;
             }
@@ -356,7 +352,7 @@ public class Java8 implements VMPlugin {
                 setAnnotationMetaData(f.getAnnotations(), fn);
                 classNode.addField(fn);
             }
-            Method[] methods = clazz.getDeclaredMethods();
+            Method[] methods = ReflectionUtils.getDeclaredMethodsSorted(clazz);
             for (Method m : methods) {
                 ClassNode rt = makeClassNode(compileUnit, 
m.getGenericReturnType(), m.getReturnType());
                 Parameter[] params = makeParameters(compileUnit, 
m.getGenericParameterTypes(), m.getParameterTypes(), 
m.getParameterAnnotations(), m);
@@ -371,7 +367,7 @@ public class Java8 implements VMPlugin {
                 mn.setSynthetic(m.isSynthetic());
                 classNode.addMethod(mn);
             }
-            Constructor<?>[] constructors = clazz.getDeclaredConstructors();
+            Constructor<?>[] constructors = 
ReflectionUtils.getDeclaredConstructorsSorted(clazz);
             for (Constructor<?> c : constructors) {
                 Parameter[] params = makeParameters(compileUnit, 
c.getGenericParameterTypes(), c.getParameterTypes(), 
getConstructorParameterAnnotations(c), c);
                 ClassNode[] exceptions = makeClassNodes(compileUnit, 
c.getGenericExceptionTypes(), c.getExceptionTypes());

Reply via email to