Skip to content

Fix Grails Forge build on Gradle 9 by bumping Shadow to 8.3.11 - #16274

Merged
codeconsole merged 2 commits into
apache:8.0.xfrom
codeconsole:fix/codec-metaclass-compile-static-8.0.x
Aug 30, 2026
Merged

Fix Grails Forge build on Gradle 9 by bumping Shadow to 8.3.11#16274
codeconsole merged 2 commits into
apache:8.0.xfrom
codeconsole:fix/codec-metaclass-compile-static-8.0.x

Conversation

@codeconsole

@codeconsole codeconsole commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

How it broke

The Forge build fails at configuration time on 8.0.x:

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 8.3.6's ShadowApplicationPlugin configures startShadowScripts with
conventionMapping.map('mainClassName'). Gradle 9 removed the deprecated
CreateStartScripts.mainClassName in favour of the mainClass Property, so the task
cannot be created, shadowDistTar cannot resolve its dependencies, and the whole build
dies before any compilation happens.

grails-forge-web-netty only applies Shadow because the AWS Elastic Beanstalk packaging
arrived from 7.2.x in #16240 (fd8de78c0a). Forge builds on Gradle 8.14.5 on 7.2.x,
where mainClassName still 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 plugins block, which is why the symptom kept
changing:

commit Forge jobs
057ffcd812 pass no Shadow plugin, no awsElasticBeanstalk on 8.0.x
fd8de78c0a (#16240) fail — Task with name 'shadowJar' not found EB packaging merged from 7.2.x, without the plugin that registers shadowJar
42219d0581 (#16243) never ran (run cancelled) added id 'com.gradleup.shadow' version '8.3.6'
5ff4af702f (#16248) fail — shadowJar not found removed the plugin line again
6d1f200d09 fail — mainClassName re-added id 'com.gradleup.shadow' without a version, so the plugin finally applied and the Gradle 9 incompatibility surfaced

The version had to be dropped in 6d1f200d09 because grails-forge/buildSrc/build.gradle
already puts shadow-gradle-plugin:$shadowVersion on the build classpath — a
version-carrying request in the plugins block fails with "the plugin is already on the
classpath with an unknown version"
.

The fix

-shadowVersion=8.3.6
+shadowVersion=8.3.11

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 Transformer API are unchanged across 8.3.x, so no
source changes are needed — grails-cli, grails-cli-shadow and the three reproducibility
transformers in buildSrc are untouched.

./gradlew build --continue --stacktrace -PgrailsIndy=false from grails-forge (the
command CI runs) now succeeds with tests. startShadowScripts, shadowDistTar,
shadowDistZip and awsElasticBeanstalk all execute, and the Elastic Beanstalk zip is
produced correctly — app.jar, Procfile, start.sh, .platform/nginx/conf.d/proxy.conf,
every entry at the constant 1980-02-01 timestamp, 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 a
real migration in buildSrcTransformerResourceTransformer, context.is
context.inputStream, and TransformerContext.getEntryTimestamp
ShadowJar.CONSTANT_TIME_FOR_ZIP_ENTRIES. Left for a separate change.


Footnote on the first commit. CodecMetaClassSupport is moved to class-level
@CompileStatic. The Caffeine cache added in #15800 is built in a static initializer
through three dynamic call sites (Caffeine.newBuilder(), weakKeys(), build()). In a
GraalVM native image there is no method handle to link, so Groovy falls back to
IndyInterface.aotDispatch, which resolves through the metaclass — and that metaclass is
empty unless com.github.benmanes.caffeine.cache.Caffeine is registered for reflection. The static initializer throws MissingMethodException: No signature of static method: newBuilder,
codecLookup fails, and the application never starts. The native-image agent cannot catch
this: on the JVM those call sites link a MethodHandle directly and never consult the
metaclass, so a traced reachability-metadata.json comes back without the entry.
Static compilation turns them into invokestatic/invokevirtual, which need no metadata.
addMetaMethod keeps @CompileDynamic because it resolves a metamethod by GString property
name, which static compilation cannot express. No JVM-observable behaviour change.
Related: #16176.

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

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 54.7040%. Comparing base (2e88f24) to head (6703374).

Additional details and impacted files

Impacted file tree graph

@@                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                
Files with missing lines Coverage Δ
...vy/org/grails/encoder/CodecMetaClassSupport.groovy 80.9524% <ø> (-1.5873%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.
@codeconsole codeconsole changed the title Compile CodecMetaClassSupport statically Fix Grails Forge build on Gradle 9 by bumping Shadow to 8.3.11 Aug 30, 2026
@codeconsole
codeconsole merged commit 4309d5b into apache:8.0.x Aug 30, 2026
86 of 87 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant