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());