Fix Grails Forge build on Gradle 9 by bumping Shadow to 8.3.11 - #16274
Merged
codeconsole merged 2 commits intoAug 30, 2026
Merged
Conversation
The static initializer added by apache#15800 builds its Caffeine cache through three dynamic call sites: ldc class com/github/benmanes/caffeine/cache/Caffeine invokedynamic invoke:(Ljava/lang/Class;)Ljava/lang/Object; // newBuilder() invokedynamic invoke:(Ljava/lang/Object;)Ljava/lang/Object; // weakKeys() invokedynamic invoke:(Ljava/lang/Object;)Ljava/lang/Object; // build() In a GraalVM native image there is no method handle to link, so Groovy falls back to IndyInterface.aotDispatch and resolves through the metaclass, which is built from Caffeine.getDeclaredMethods() -- empty unless the class is registered for reflection. The class initializer fails with No signature of static method: newBuilder for class: com.github.benmanes.caffeine.cache.Caffeine and codecLookup, gspTagLibraryLookup and groovyPagesTemplateEngine fail with it, so the application never starts. Six of the ten methods already carried @CompileStatic. Moving it to the class turns those three call sites into invokestatic/invokevirtual and needs no reachability metadata at all. addMetaMethod stays dynamic: it resolves a metamethod by GString property name, which is the one thing static compilation cannot express.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16274 +/- ##
==================================================
- Coverage 54.7109% 54.7040% -0.0069%
+ Complexity 20422 20420 -2
==================================================
Files 2101 2101
Lines 100978 100978
Branches 17907 17907
==================================================
- Hits 55246 55239 -7
- Misses 37866 37873 +7
Partials 7866 7866
🚀 New features to boost your workflow:
|
Shadow 8.3.6's ShadowApplicationPlugin configures the startShadowScripts
task through conventionMapping.map('mainClassName'). Gradle 9 removed the
deprecated CreateStartScripts.mainClassName in favour of the mainClass
Property, so the task can no longer be created and the Forge build fails
at configuration time:
Could not determine the dependencies of task
':grails-forge-web-netty:shadowDistTar'.
> Could not create task ':grails-forge-web-netty:startShadowScripts'.
> You can't map a property that does not exist: propertyName=mainClassName
Shadow is only applied to grails-forge-web-netty because the AWS Elastic
Beanstalk packaging came over from 7.2.x, which builds Forge on Gradle
8.14.5 where mainClassName still exists. Shadow 8.3.7 backported Gradle 9
support and 8.3.11 is the latest 8.3.x; the 8.x line is maintenance-only
upstream, so Forge's own build should follow the generated applications to
Shadow 9 (already 9.2.2 in the Forge dependency catalog) in a later change,
which needs the Transformer -> ResourceTransformer migration in buildSrc.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
How it broke
The Forge build fails at configuration time on
8.0.x:Shadow 8.3.6's
ShadowApplicationPluginconfiguresstartShadowScriptswithconventionMapping.map('mainClassName'). Gradle 9 removed the deprecatedCreateStartScripts.mainClassNamein favour of themainClassProperty, so the taskcannot be created,
shadowDistTarcannot resolve its dependencies, and the whole builddies before any compilation happens.
grails-forge-web-nettyonly applies Shadow because the AWS Elastic Beanstalk packagingarrived from 7.2.x in #16240 (
fd8de78c0a). Forge builds on Gradle 8.14.5 on 7.2.x,where
mainClassNamestill exists, and on Gradle 9.6.0 on 8.0.x, where it does not.The feature was written and validated against Gradle 8 and merged onto a Gradle 9 build.
Three commits then churned the same
pluginsblock, which is why the symptom keptchanging:
057ffcd812awsElasticBeanstalkon 8.0.xfd8de78c0a(#16240)Task with name 'shadowJar' not foundshadowJar42219d0581(#16243)id 'com.gradleup.shadow' version '8.3.6'5ff4af702f(#16248)shadowJar not found6d1f200d09mainClassNameid 'com.gradleup.shadow'without a version, so the plugin finally applied and the Gradle 9 incompatibility surfacedThe version had to be dropped in
6d1f200d09becausegrails-forge/buildSrc/build.gradlealready puts
shadow-gradle-plugin:$shadowVersionon the build classpath — aversion-carrying request in the
pluginsblock fails with "the plugin is already on theclasspath with an unknown version".
The fix
Shadow 8.3.7 backported Gradle 9 support (GradleUp/shadow#1470) and 8.3.11 is the latest of
that line. The package layout and the
TransformerAPI are unchanged across 8.3.x, so nosource changes are needed —
grails-cli,grails-cli-shadowand the three reproducibilitytransformers in
buildSrcare untouched../gradlew build --continue --stacktrace -PgrailsIndy=falsefromgrails-forge(thecommand CI runs) now succeeds with tests.
startShadowScripts,shadowDistTar,shadowDistZipandawsElasticBeanstalkall execute, and the Elastic Beanstalk zip isproduced correctly —
app.jar,Procfile,start.sh,.platform/nginx/conf.d/proxy.conf,every entry at the constant
1980-02-01timestamp, so reproducibility is preserved.Known limitation: this keeps Forge's own build on a line upstream considers
maintenance-only. Generated applications are already on Shadow 9.2.2
(
grails-forge-core/src/main/resources/pom.xml), and Forge should follow, but that needs areal migration in
buildSrc—Transformer→ResourceTransformer,context.is→context.inputStream, andTransformerContext.getEntryTimestamp→ShadowJar.CONSTANT_TIME_FOR_ZIP_ENTRIES. Left for a separate change.Footnote on the first commit.
CodecMetaClassSupportis moved to class-level@CompileStatic. The Caffeine cache added in #15800 is built in a static initializerthrough three dynamic call sites (
Caffeine.newBuilder(),weakKeys(),build()). In aGraalVM native image there is no method handle to link, so Groovy falls back to
IndyInterface.aotDispatch, which resolves through the metaclass — and that metaclass isempty unless
com.github.benmanes.caffeine.cache.Caffeineis registered for reflection. The static initializer throwsMissingMethodException: No signature of static method: newBuilder,codecLookupfails, and the application never starts. The native-image agent cannot catchthis: on the JVM those call sites link a
MethodHandledirectly and never consult themetaclass, so a traced
reachability-metadata.jsoncomes back without the entry.Static compilation turns them into
invokestatic/invokevirtual, which need no metadata.addMetaMethodkeeps@CompileDynamicbecause it resolves a metamethod by GString propertyname, which static compilation cannot express. No JVM-observable behaviour change.
Related: #16176.