[ 
https://issues.apache.org/jira/browse/GROOVY-12149?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18096016#comment-18096016
 ] 

ASF GitHub Bot commented on GROOVY-12149:
-----------------------------------------

Copilot commented on code in PR #2690:
URL: https://github.com/apache/groovy/pull/2690#discussion_r3574739160


##########
src/main/java/org/codehaus/groovy/vmplugin/v8/Java8.java:
##########
@@ -403,7 +399,7 @@ public void configureClassNode(final CompileUnit 
compileUnit, final ClassNode cl
                 setAnnotationMetaData(f.getAnnotations(), fn);
                 classNode.addField(fn);
             }
-            Method[] methods = clazz.getDeclaredMethods();
+            Method[] methods = ReflectionUtils.getDeclaredMethodsSorted(clazz);
             for (Method m : methods) {

Review Comment:
   `configureClassNode` now uses sorted reflection results for methods and 
constructors, but fields are still collected via `clazz.getDeclaredFields()` 
(nondeterministic order). If field order feeds into emitted class layout, this 
can still produce byte-different class files across JVM runs, undermining the 
reproducible-build goal described in the PR.



##########
src/main/java/org/codehaus/groovy/reflection/ReflectionUtils.java:
##########
@@ -135,6 +137,70 @@ public static List<Method> getDeclaredMethods(final 
Class<?> type, final String
         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;
+    }

Review Comment:
   The PR description mentions adding 
`ReflectionUtils#getDeclaredFieldsSorted(Class)` for deterministic field 
ordering, but this class currently only adds sorted variants for methods and 
constructors. Without a field-sorting helper, callers like 
`Java8#configureClassNode` cannot easily make field enumeration deterministic 
too.





> Nondeterministic reflection order flows into generated bytecode, breaking 
> reproducible builds
> ---------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12149
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12149
>             Project: Groovy
>          Issue Type: Bug
>            Reporter: James Daugherty
>            Priority: Major
>
> Follow-up to GROOVY-12146, which fixed nondeterministic ordering of 
> annotation members copied from precompiled classes. The same root cause — 
> Class.getDeclaredMethods()/getDeclaredFields()/getDeclaredConstructors() 
> returning members in an unspecified order that varies between HotSpot runs — 
> also affects Java8#configureClassNode, which populates ClassNodes for 
> precompiled classes via reflection. The enumeration order is preserved 
> through to bytecode generation (e.g. via the LinkedHashMap returned by 
> ClassNode#getDeclaredMethodsMap), so two compilations of identical sources on 
> the same JDK can still produce byte-different class files.
> Two manifestations were observed while verifying the reproducibility of the 
> Apache Grails 8.0.0-M3 release artifacts (same sources, same JDK, 
> containerized double-build):
> 1. MOP bridge methods: for classes extending a precompiled Groovy class, 
> MopWriter#getSuperMethods iterates the superclass's declared-methods map, so 
> the synthetic super$N$… methods are emitted in a different order per build.
> 2. Woven trait methods: methods copied from a precompiled trait (e.g. GORM's 
> DirtyCheckable#trackChanges/syncChangedProperties) are woven into 
> implementing classes in enumeration order, reordering the emitted methods and 
> the constant pool.
> In both cases the bytecode is semantically identical — decompiled sources 
> match exactly — only member emission order (and the constant-pool layout that 
> follows from it) differs.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to