matrei commented on PR #16180:
URL: https://github.com/apache/grails-core/pull/16180#issuecomment-5634376130

   # Review Findings
   
   Head `2fb4179e56` on `8.0.x`, merge base `43510784cf`. #16179 has merged, so 
the diff is now only the `end-to-end/` module (14 files, 477 additions). `git 
merge-tree` is clean.
   
   CI on this head is green, including "End to End Tests (end-to-end build 
only)", which runs `./gradlew check` in `end-to-end` and therefore the new 
`i18nCheckOnJvm` task.
   
   I also ran the native half, which the PR says nobody has. Setup followed the 
README exactly: Oracle GraalVM for JDK 21 (21.0.12) as `JAVA_HOME` and 
`GRAALVM_HOME`, `grails-gradle` and the framework republished to 
`build/local-maven` with `-PgrailsIndy=true` (verified: 0 of 300 sampled 
`grails-core` classes carry a `CallSiteArray`, 168 contain `invokedynamic`), 
then `./gradlew :native-i18n:check -PnativeTests`. The JVM half passed. The 
native half fails, twice over, for reasons that are upstream of this PR but 
that the PR's README presents as solved. Details in the first finding.
   
   The PR is no longer marked draft, but the description still says "the reason 
this is a draft" and "the native half has not been executed yet". The second 
statement is accurate and should stay; the draft flag should come back until 
the first finding is resolved.
   
   ## Findings
   
   ### High: the native half cannot pass on 8.0.x as pinned, and the README 
recipe does not produce a working image
   
   What happens when the README is followed:
   
   | Step | GraalVM for JDK 21 (21.0.12) | Oracle GraalVM 25.3.4 (native-image 
25.0.4.1) |
   |---|---|---|
   | `nativeCompile` | fails in 4s at "Initializing": `Could not find target 
method: ... 
Target_org_codehaus_groovy_vmplugin_v7_IndyInterface_invalidateSwitchPoints.invalidateSwitchPoints()`
 | succeeds, 5m 34s, 157 MiB binary |
   | `i18nCheckOnNative` | not reached | binary dies before Spring starts: 
`BootstrapMethodError: NullPointerException` at 
`IndyInterface.makeBootHandle(IndyInterface.java:247)` from 
`Application.<clinit>` |
   
   The first failure is GraalVM's bundled Groovy substitution targeting a 
method Groovy 4+ no longer has (oracle/graal #10200, #13096; fixed on master in 
April 2026 and present in the 25 line, not in 21). So the README's "a GraalVM 
JDK" has to say "GraalVM for JDK 25 or later", and the JDK-matching advice in 
the same section needs rewording, since the framework is then published on 21 
and the image built on 25.
   
   The second failure is the real blocker. Line 247 passes 
`FROM_CACHE_HANDLE_METHOD`, a `static final` handle that 
`IndyInterface.<clinit>` assigns, and it is null: the bootstrap method ran 
before its declaring class was initialised. This is not Grails-specific and not 
a metadata gap. I reproduced the identical exception with a ten-line Groovy 
hello-world, no Spring, no Grails, metadata from the tracing agent, against 
Groovy 5.1.2 and against Groovy 4.0.27. Registering every `IndyInterface` 
method for reflection changes nothing; `--exact-reachability-metadata` raises 
no missing-registration error; `--initialize-at-build-time` for the class fails 
the build on image-heap objects, as Groovy's own tracker says it will. The 
class-initialisation report confirms `IndyInterface` is `RUN_TIME`.
   
   Groovy knows about it. `IndyInterface.java` in 6.0.0-RC-1 carries this 
comment above a new `ensureInitialized()` guard:
   
   > Guards against a GraalVM native-image gap: the runtime invokedynamic 
linkage invokes a bootstrap method without running its declaring class's 
`<clinit>` first (observed on GraalVM CE 25.2.4; on HotSpot the bootstrap's 
`DirectMethodHandle` carries a class-initialization barrier).
   
   The same hello-world against 6.0.0-RC-1 fails at that guard with `BUG! 
unreachable: LOOKUP read before initialization`, so even the Groovy 6 
workaround does not hold on Oracle GraalVM 25.3.4. The wider work is all on the 
Groovy 6 line: GROOVY-12234 "Indy: AOT link mode so dynamic Groovy dispatch 
works in GraalVM native images" (6.0.0-beta-2), GROOVY-12227, and in the 
unreleased 6.0.0-RC-2 GROOVY-12364, GROOVY-12365 (ship reachability metadata in 
the groovy jar) and GROOVY-12366 (ship `native-image.properties`). 
GROOVY-12234's description states the status plainly: before it, dynamic Groovy 
cannot run in an image at all, indy or not, and the only workaround was 
compiling *everything* non-indy with `groovy-callsite`.
   
   Grails 8.0.x pins `groovy.version` 5.1.2 in `dependencies.gradle`. None of 
the above is in any 5.1.x release.
   
   Consequences for this PR:
   
   - The README section "Native image verification" describes a recipe (GraalVM 
JDK, indy on both sides) as if following it yields an image that runs the 
check. It does not, on any released Groovy the BOM can use. The section should 
say what the actual precondition is (a Groovy 6 with the AOT link mode, and a 
GraalVM on which the class-init gap is closed) and that neither exists in a 
release today, so `-PnativeTests` is expected to fail until then. Otherwise the 
next person loses a day the way I nearly did.
   - The two build comments in `native-i18n/build.gradle` about 16 threads and 
six minutes, and the README's `UnsupportedFeatureError: Tried to define class` 
paragraph, are observations from some earlier build. Whichever toolchain and 
Groovy they came from should be named, because they are not reproducible with 
what 8.0.x pins.
   - "Released Grails artifacts are built without indy, so no released Grails 
can produce a working image" (`README.md:103`) is true but misleading in 
isolation: with Groovy 5 no build of Grails can, indy or not. The sentence 
should say so.
   - The claim in the description that this module lets a missing hint 
"actually be falsified" is not yet true. The JVM half is a fixture guard; the 
native half needs the Groovy 6 line before it can assert anything. That is fine 
for a first landing as long as the module and README say it, and `#16176` is 
updated with the two upstream blockers.
   
   What would make the native half useful sooner: the snapshot canary workflow 
already rebuilds Grails against a Groovy branch by rewriting `groovy.version`. 
Running this module in that job, on GraalVM 25, once GROOVY-12366 ships in 
RC-2, is the cheapest way to learn when the upstream gap closes. I did not 
attempt a Groovy 6 rebuild of the framework here; it is a different review.
   
   ### Medium: the "bundle never registered" diagnostic can never fire under 
this configuration
   
   References:
   
   - `end-to-end/native-i18n/grails-app/conf/application.yml:27` sets 
`use-code-as-default-message: true`
   - 
`end-to-end/native-i18n/grails-app/init/nativei18n/Application.groovy:96-99` 
catches an exception from `getMessage` and reports "its bundle was never 
registered"
   - `end-to-end/README.md:60` and the class Javadoc say a missing hint shows 
up "as a `NoSuchMessageException`"
   
   With `useCodeAsDefaultMessage` on, `AbstractMessageSource.getMessage(code, 
args, locale)` returns the code itself instead of throwing, so a bundle that is 
missing from the image produces `actual == code`, and the failure surfaces 
through the *other* branch as `'native.plugin.greeting' for fr resolved to 
'native.plugin.greeting', expected '...'`. The check still fails, so this is 
not a false pass, but the catch block is dead code, its message is the one a 
reader would want, and the README and Javadoc describe an exception that this 
application is configured never to see.
   
   Suggested fix: keep the property (the unknown-code assertion needs it), but 
make `check` treat `actual == code` as "bundle not registered" and say so, and 
drop or reword the `NoSuchMessageException` sentence in the README and the 
Javadoc.
   
   ### Low: leftover debugging in the build script
   
   Reference: `end-to-end/native-i18n/build.gradle:28-31`
   
   ```groovy
   // Applied the legacy way: the Spring Boot plugin is on this build's 
classpath through the
   // Grails plugin, so the id resolves, but it carries no version for the 
plugins block.
   // TEMP probe: disabled
   //apply plugin: 'org.springframework.boot.aot'
   ```
   
   Should go. It is also unnecessary: Spring Boot's Gradle plugin applies 
`org.springframework.boot.aot` itself when it sees 
`org.graalvm.buildtools.native`, so the native path already runs `processAot` 
and the JVM path deliberately does not. If the point was to run AOT processing 
on the JVM too, that is a separate decision worth a sentence, not a 
commented-out line.
   
   ### Low: `grails { indy = true }` duplicates a convention the Grails Gradle 
plugin already sets
   
   References:
   
   - `end-to-end/native-i18n/build.gradle:36-41`
   - 
`grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:1066-1071`
 (`configureNativeImage` sets `indy.convention(true)` when the GraalVM plugin 
is applied)
   
   Not wrong, and setting it unconditionally means the JVM run is compiled the 
same way as the image, which is a reasonable choice. Note that with Groovy 5 it 
also makes no difference to the outcome, see the first finding. But the comment 
presents it as something the application has to do, when the plugin does it for 
any application that builds an image. Either drop the setting and let the 
convention prove itself (arguably more end-to-end), or keep it and say that it 
is there so the JVM half matches the native half rather than because an image 
needs it.
   
   ### Low: the README's justification for the JDK requirement does not match 
the build
   
   Reference: `end-to-end/README.md:87-90`: "`grails-gradle` pins no release 
target, so it stamps whatever JDK ran it onto its class files".
   
   `grails-gradle` includes `build-logic`, and its four modules apply 
`org.apache.grails.buildsrc.compile`, whose `configureJavaVersion` sets 
`options.release` on every `JavaCompile`. What is true is that `GroovyCompile` 
has no `release` option, so the Groovy half of those modules targets the 
bytecode level of the JDK running the compiler. The advice (export the GraalVM 
JDK before publishing) is right; the reason given is not. And as the first 
finding shows, the GraalVM has to be the 25 line, so the README's implied "one 
JDK for everything" does not survive contact either: publish on 21, build the 
image with `GRAALVM_HOME` on 25, which the GraalVM plugin honours with 
`toolchainDetection = false`. Say "the Groovy sources in `grails-gradle` are 
compiled for the running JDK" or similar.
   
   ### Low: the GraalVM plugin version is pinned in a new place, one step behind
   
   Reference: `end-to-end/gradle.properties:27`, 
`graalvmBuildtoolsVersion=1.1.7`
   
   Nothing else in the repository pins `org.graalvm.buildtools` (checked 
`dependencies.gradle` and `grails-gradle`), so a property is a fair place for 
it. Maven Central has 1.1.12 as the latest. Not blocking, but since this is the 
first pin it may as well start current, and a one-line comment saying why it 
lives in `end-to-end/gradle.properties` rather than `dependencies.gradle` would 
save the next person the search.
   
   ### Nits
   
   - `end-to-end/native-i18n/grails-app/init/nativei18n/Application.groovy:88`: 
`System.exit(0)` is only on the success path. On failure the exception leaves 
`main` and the JVM exits non-zero only once every non-daemon thread has 
stopped. The context is closed in `finally`, so this works today, but a `catch` 
that prints and calls `System.exit(1)` would make the exit code independent of 
thread cleanup, which matters more for the native binary than the jar.
   - `end-to-end/native-i18n/build.gradle:91`: `commandLine` resolves the 
binary path eagerly with `.get()` at configuration time. Wiring it from 
`nativeCompile`'s output (`tasks.named('nativeCompile').flatMap { it.outputFile 
}` or the binary's `outputDirectory`) keeps the path and the dependency in one 
place.
   - `end-to-end/native-i18n/build.gradle:84`: the comment on 
`toolchainDetection = false` explains why a failed image is a failure, not what 
the setting does (use `GRAALVM_HOME`/`JAVA_HOME` instead of a Gradle 
toolchain). Worth one clause.
   - `end-to-end/native-i18n/grails-app/i18n/messages_fr.properties:19`: "de l 
application" reads as a typo. Spring's `ResourceBundleMessageSource` does not 
pass a message with no arguments through `MessageFormat`, so `l'application` 
renders as written; if the apostrophe was dropped to be safe, `l''application` 
is the conventional spelling.
   - Neither new module applies 
`org.apache.grails.buildsrc.vulnerability-scan`, which `legacy-commands`, 
`legacy-commands-plugin` and `spring-dependency-management` all do. Probably 
deliberate for a fixture with two dependencies, but consistency is cheap.
   - README table rows are inserted between `spring-dependency-management` and 
`taglib-index-incremental`; the table was alphabetical.
   
   ## Verified as correct
   
   - The plugin fixture does exercise the multi-word matching it claims: 
`NativeMessagesGrailsPlugin` yields `native-messages` via 
`GrailsNameUtils.getPluginName` in the descriptor, the runtime reports 
`nativeMessages`, and `EffectiveI18nDescriptors` normalises both through 
`PluginUtils.normalizePluginName`. The base name `native-messages` also passes 
`GenerateI18nDescriptorTask.validatePluginNamespace`.
   - The dotted base name is covered by 
`I18nRuntimeHintsProcessor.toResourcePath`, which is exactly the conversion the 
third case tests, and `I18nEnvironmentPostProcessor.compose` keeps an 
application-declared base name ahead of the discovered ones, so 
`config.i18n.custom` survives the merge.
   - `fallback-to-system-locale: false` is what makes the `Locale.ENGLISH` 
assertions land on the base bundle regardless of the machine's default locale. 
Good.
   - Apache license headers are present on every new file, including the 
bundles.
   
   ## Beyond this PR
   
   - **Native image support in Grails 8 depends on Groovy 6.** Everything above 
reduces to this. `#16176` should record it, and whoever owns the AOT work 
(#16094) should decide whether native image is an 8.0 claim or an 
8.x-with-Groovy-6 claim, because the upgrade guide and the Gradle plugin's 
`configureNativeImage` convention currently imply the former.
   - **Indy default for releases.** `README.md:103` is right that 
`CompilePlugin` defaults `grailsIndy` to `false`, so even once Groovy is fixed 
a released Grails cannot be imaged. That needs an issue or a decision before 
8.0.0.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to