Skip to content

feat: keep published artifacts on invokedynamic, let applications choose - #16171

Closed
codeconsole wants to merge 2 commits into
apache:8.0.xfrom
codeconsole:feat/indy-app-only
Closed

feat: keep published artifacts on invokedynamic, let applications choose#16171
codeconsole wants to merge 2 commits into
apache:8.0.xfrom
codeconsole:feat/indy-app-only

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

Alternative to #16170. Same problem, opposite conclusion: instead of publishing every artifact twice, publish each one once — always as invokedynamic.

Why

Call-site caching links call sites at runtime, which ahead-of-time native compilation cannot do. A native image needs every class on the classpath to be the invokedynamic flavour, so one dependency compiled the other way rules out a native image for every application that depends on it.

That makes the flavour of a published artifact not really its own build's decision. Today a plugin author can set grails { indy = false } and silently strip native compatibility from every application using that plugin.

What changes

A project applying org.apache.grails.gradle.grails-plugin always compiles its published classes with invokedynamic and ignores grails.indy. Plugin authors need to change nothing; a plugin build that sets indy can drop it.

grails.indy still applies to applications, which sit at the end of the classpath and may choose for their own sources. It now defaults to true, following Groovy's own default, rather than false. An application setting it to false opts itself out of native compilation but still runs correctly against the invokedynamic artifacts of the framework and its plugins — the two flavours interoperate on the same classpath.

This also resolves an existing incoherence: the framework currently ships mixed bytecode (grails-controllers indy-on, grails-gsp indy-off) purely from which modules happened to adopt grails-plugin. Every published artifact is now invokedynamic.

Compared to #16170

#16170 (dual artifacts) this PR
Published artifacts 2 per module 1 per module
Diff 2 new plugin classes + test projects 73 insertions, 10 deletions, 6 files
Groovy compilation ~doubles unchanged
Release surface doubles (signing, SBOM, scanning) unchanged
Native compilation possible unless the app opts out possible unless the app opts out
App can use call-site caching yes, for its own code and its dependencies yes, for its own code

The noindy artifacts that #16170 publishes can never be used in a native build, so they double the release surface to serve only the non-native case — which this PR still supports for an application's own code at no artifact cost.

Verification

4 functional tests in GrailsIndyAppOnlySpec: a plugin that explicitly sets indy = false still emits PLUGIN_BYTECODE=indy=true,callsite=false; no second-flavour tasks exist; an application can still choose call-site caching for itself; an application configuring nothing follows Groovy's default.

On a real module: grails-cache applies grails-plugin and compiled with zero invokedynamic instructions on 8.0.x. On this branch, 17 of its 37 classes carry them.

Suite: 96/99 passing. The 3 GroovyPageToolchainSpec failures are environmental — they need a JDK 21 toolchain that is not installed on the machine I ran them on, and they fail identically on unmodified 8.0.x. codeStyle clean.

Reviewer notes

  • Full ./gradlew build has not been run; one framework module was built and inspected locally.
  • The 29 modules here that apply grails-plugin change from indy-off to indy-on, which is the intended unification but is the change most likely to show in a performance comparison against 7.x.

Callsite caching links call sites at runtime and cannot be compiled ahead
of time, so a native image needs every class on the classpath to be the
invokedynamic flavour. One dependency compiled the other way rules out a
native image for every application that uses it.

A project applying the grails-plugin plugin therefore always compiles its
published classes with invokedynamic and ignores grails.indy, so a plugin
build can no longer strip native compatibility from its consumers. Plugin
authors need to change nothing; a plugin build that sets indy can drop it.

grails.indy still applies to applications, which sit at the end of the
classpath and may choose for their own sources, and now defaults to true
following Groovy's own default rather than false. An application setting
it to false opts itself out of native compilation but still runs correctly
against the invokedynamic artifacts of the framework and its plugins.

The -PgrailsIndy toggle used by CI now applies only to the
application-type projects in this repository.
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #16171        +/-   ##
==================================================
+ Coverage     54.7109%   55.9298%   +1.2189%     
+ Complexity      20422      19897       -525     
==================================================
  Files            2101       2020        -81     
  Lines          100978      96242      -4736     
  Branches        17907      17104       -803     
==================================================
- Hits            55246      53828      -1418     
+ Misses          37866      34815      -3051     
+ Partials         7866       7599       -267     

see 91 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.

@jdaugherty

jdaugherty commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

I would 100% support using a classifier to let end users chose. I do not support having indy be the default for Groovy 5 though. The performance isn't there yet and it's enough that we already decided to remove it. I'm a -1 on making it the default until we can get the performance to be on par (which I think Groovy 6 does)

# Conflicts:
#	grails-doc/src/en/guide/upgrading/upgrading80x.adoc
@testlens-app

testlens-app Bot commented Aug 30, 2026

Copy link
Copy Markdown

🚨 TestLens detected 3 failed tests 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

CI / Build Gradle Plugins (21) > :grails-gradle-plugins:test [grails-gradle]

Test Runs Flakiness
GrailsNativeImageDefaultsSpec > an application that builds no image is left as it was 0% 🟢

CI / Build Gradle Plugins (25) > :grails-gradle-plugins:test [grails-gradle]

Test Runs Flakiness
GrailsNativeImageDefaultsSpec > an application that builds no image is left as it was 0% 🟢

CI / Functional Tests (Java 21, indy=false) > :grails-test-examples-scaffolding:integrationTest

Test Runs Flakiness
UserControllerSpec > User list 3% 🟡

🏷️ Commit: c2c4c69
▶️ Tests: 68989 executed
⚪️ Checks: 89/89 completed

Test Failures

GrailsNativeImageDefaultsSpec > an application that builds no image is left as it was (:grails-gradle-plugins:test [grails-gradle] in CI / Build Gradle Plugins (21))
Condition not satisfied:

result.output.contains('INDY=false')
|      |      |
|      |      false
|       
|      > Configure project :
|      Could not fully resolve CLI companion probe dependencies for project native-defaults-off; some companion -cli artifacts (and their per-command Gradle tasks) may not be auto-provisioned. Cause: Cannot resolve external dependency org.apache.grails:grails-bom:8.0.0-SNAPSHOT because no repositories are defined.
|      Required by:
|          root project 'native-defaults-off'
|       
|      > Task :inspectDefaults
|      INDY=true
|      HAS_NATIVE_EXTENSION=false
|       
|      [Incubating] Problems report is available at: file:///tmp/gradle-projects10171980740616637220/native-defaults-off/build/reports/problems/problems-report.html
|       
|      Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.
|       
|      You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
|       
|      For more on this, please refer to https://docs.gradle.org/9.6.0/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
|       
|      BUILD SUCCESSFUL in 28s
|      1 actionable task: 1 executed
<org.gradle.testkit.runner.internal.FeatureCheckBuildResult@382d47a5 delegateBuildResult=org.gradle.testkit.runner.internal.DefaultBuildResult@13a7d728 outputFeatureCheck=org.gradle.testkit.runner.internal.feature.BuildResultOutputFeatureCheck@4578b8cb>

	at org.grails.gradle.plugin.core.GrailsNativeImageDefaultsSpec.an application that builds no image is left as it was(GrailsNativeImageDefaultsSpec.groovy:45)
GrailsNativeImageDefaultsSpec > an application that builds no image is left as it was (:grails-gradle-plugins:test [grails-gradle] in CI / Build Gradle Plugins (25))
Condition not satisfied:

result.output.contains('INDY=false')
|      |      |
|      |      false
|       
|      > Configure project :
|      Could not fully resolve CLI companion probe dependencies for project native-defaults-off; some companion -cli artifacts (and their per-command Gradle tasks) may not be auto-provisioned. Cause: Cannot resolve external dependency org.apache.grails:grails-bom:8.0.0-SNAPSHOT because no repositories are defined.
|      Required by:
|          root project 'native-defaults-off'
|       
|      > Task :inspectDefaults
|      INDY=true
|      HAS_NATIVE_EXTENSION=false
|       
|      [Incubating] Problems report is available at: file:///tmp/gradle-projects17161651197222466318/native-defaults-off/build/reports/problems/problems-report.html
|       
|      Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.
|       
|      You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
|       
|      For more on this, please refer to https://docs.gradle.org/9.6.0/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
|       
|      BUILD SUCCESSFUL in 24s
|      1 actionable task: 1 executed
<org.gradle.testkit.runner.internal.FeatureCheckBuildResult@2eba1b7 delegateBuildResult=org.gradle.testkit.runner.internal.DefaultBuildResult@10aa5f84 outputFeatureCheck=org.gradle.testkit.runner.internal.feature.BuildResultOutputFeatureCheck@56827682>

	at org.grails.gradle.plugin.core.GrailsNativeImageDefaultsSpec.an application that builds no image is left as it was(GrailsNativeImageDefaultsSpec.groovy:45)
UserControllerSpec > User list (:grails-test-examples-scaffolding:integrationTest in CI / Functional Tests (Java 21, indy=false))
geb.waiting.WaitTimeoutException: condition did not pass in 30 seconds (failed with exception)
	at geb.waiting.Wait.waitFor(Wait.groovy:128)
	at geb.waiting.DefaultWaitingSupport.doWaitFor(DefaultWaitingSupport.groovy:55)
	at geb.waiting.DefaultWaitingSupport.waitFor(DefaultWaitingSupport.groovy:41)
	at geb.Page.waitFor(Page.groovy:120)
	at com.example.pages.LoginPage.login(LoginPage.groovy:39)
	at com.example.UserControllerSpec.User list(UserControllerSpec.groovy:48)
Caused by: Assertion failed: 

title != pageTitle && $('input', name: 'username').empty
|     |  |         |
|     |  |         false
|     |  'Please sign in'
|     false
'Please sign in'

	at com.example.pages.LoginPage.login_closure1(LoginPage.groovy:39)
	at com.example.pages.LoginPage.login_closure1(LoginPage.groovy)
	at geb.waiting.Wait.waitFor(Wait.groovy:117)
	... 5 more

Rerun Controls

Select tests to mute in this pull request:

  • GrailsNativeImageDefaultsSpec > an application that builds no image is left as it was
  • UserControllerSpec > User list

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

Learn more about TestLens at testlens.app/docs.

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.

2 participants