feat!: make callsite caching the default and publish invokedynamic under a classifier - #16170
feat!: make callsite caching the default and publish invokedynamic under a classifier#16170codeconsole wants to merge 5 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16170 +/- ##
===================================================
- Coverage 53.6425% 27.1621% -26.4803%
+ Complexity 19783 471 -19312
===================================================
Files 2086 78 -2008
Lines 99630 4521 -95109
Branches 17594 772 -16822
===================================================
- Hits 53444 1228 -52216
+ Misses 38542 3084 -35458
+ Partials 7644 209 -7435
🚀 New features to boost your workflow:
|
|
Rebuilt on a different mechanism after CI failed. The original approach was not fixable. Why it failedPublishing the Every unmatched line reads "but the consumer didn't ask for it", including No attribute rule repairs this. A disambiguation rule receives only the values candidates declare, so the variant without the attribute never appears among them — instrumenting the rule prints Worth noting the one existing precedent here, What replaces itThe Selection is assembled on the consuming side. An application setting grails {
indy = false
noindyModules = ['org.apache.grails:grails-core', 'com.example:my-plugin']
}That variant exists only inside the opted-in build, whose schema also carries the disambiguation rule needed to choose between it and the main artifact. Verified across five scenarios, including the two that broke CI: an artifact view constraining nothing, and an ad-hoc configuration with no attributes. Both now resolve a single artifact. A dependency publishing only the main artifact still resolves under either setting. Two limitations, both inherent
Standing recommendation unchangedThis makes the approach viable, not necessarily right. |
Groovy compiles dynamic calls either as invokedynamic or as call-site caching bytecode. Which is faster depends on the application, but the choice was previously made once at compile time and could not reach the framework and plugin jars an application already depends on. Framework modules and Grails plugins now publish their classes twice: a default artifact using Groovy's own invokedynamic default, and a `noindy` classifier artifact compiled without it. An application sets `grails.indy` and the choice propagates across the whole dependency graph. Selection goes through Gradle Module Metadata rather than the classifier alone. A Maven classifier shares the POM of the main artifact, so asking for one by classifier would pull the default flavour of everything it depends on. The noindy artifact is published as a secondary variant of apiElements and runtimeElements, which inherits their dependencies, so one request applies transitively. Only the noindy variants declare the attribute; the default variants leave it absent. A consumer that never asks for it -- any plain Gradle project that does not apply a Grails plugin -- sees a single candidate and resolves the default artifact as before. Declaring it on both would fail every such build with a variant ambiguity error. grails.indy no longer governs how a plugin compiles its own sources, since a plugin builds both flavours and the application resolving it chooses. Applications are unaffected: the default is still false.
grails.indy defaulted to false, so an application that never configured it compiled with callsite caching. It now defaults to true, matching Groovy's own default, and applications wanting the previous behaviour set it to false explicitly. The setting continues to select which flavour of the framework and plugin artifacts the application resolves, so the default artifact set -- the one almost every application receives -- is the one Groovy would produce. The -PgrailsIndy toggle used by CI now applies only to the application-type projects in this repository; modules applying the grails-plugin plugin build both flavours regardless of it.
Call-site caching bytecode links call sites at runtime and cannot be compiled ahead of time, so a native image needs every artifact on the classpath to be the invokedynamic flavour. Setting indy = false is therefore a choice against native compilation, which the guide did not say.
…shed variant Publishing the noindy jar as a secondary variant of apiElements and runtimeElements made every module ambiguous to a configuration that requests no attributes at all -- the shape the tck and CLI companion probe configurations use -- and CI failed across the board on it. No attribute rule can repair that. A disambiguation rule is handed only the values candidates declare, so the variant without the attribute never appears among them and cannot be preferred; instrumenting the rule shows `consumer=null candidates=[false]`. Declaring the attribute on both variants instead would resolve it here but break every plain consumer of a Grails artifact, which is where this started. The noindy jar is now published as an ordinary Maven classifier and adds nothing to the module's metadata, so no consumer's resolution changes. Selection is assembled on the consuming side: an application that sets indy = false registers a component metadata rule deriving a variant from the classifier for the modules it lists in grails.noindyModules. That variant exists only inside the build that opted in, whose schema carries the rules needed to choose, so the ambiguity is unreachable rather than merely fixed. The list is required because a rule applied to a module that publishes no such classifier -- or to a platform, which publishes no jar -- fails when the missing file is fetched. Modules advertise theirs through the Grails-Noindy-Artifact manifest attribute, mirroring Grails-Cli-Artifact, so the list can later be discovered rather than written. Component metadata rules do not apply to project dependencies, so a composite or multi-project build always resolves the main artifact.
…ssifier
Grails 8 compiles applications without invokedynamic, as 7.x did, and every
module and plugin publishes its main artifact the same way. The invokedynamic
flavour is published alongside it under the `indy` classifier, so an
application that wants it can have it throughout rather than only in its own
classes -- which is what ahead-of-time native compilation requires, since
callsite caching links call sites at runtime and cannot be compiled into a
native image.
An application opts in by enabling indy and naming the dependencies that
publish the classifier:
grails {
indy = true
indyModules = ['org.apache.grails:grails-core']
}
Dependencies left off the list keep their main artifact. That is safe on the
JVM, where the two flavours interoperate on the same classpath, but a native
image needs the indy flavour of everything, so an application targeting one
lists every Grails module and plugin it uses. Listing a module that publishes
no classifier -- or a platform such as grails-bom, which publishes no jar --
fails when the missing file is fetched, so modules advertise theirs through
the Grails-Indy-Artifact manifest attribute.
grails.indy no longer governs how a plugin compiles its own sources: a plugin
builds both flavours through the new indyJar task and the resolving
application chooses.
The indy jar is assembled by unpacking the finished main jar and layering the
invokedynamic classes over it. Enumerating the source directories instead
omitted the AST classes a plugin copies in from a separate source set along
with its staged command and template resources, and reusing the main jar's
copy spec imported that task's duplicatesStrategy with it. Unpacking keeps
the two artifacts identical by construction, whatever the build contributed.
b9ed461 to
3ce9e88
Compare
🚨 TestLens detected 128 failed tests 🚨Here is what you can do:
Test SummaryCI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 0) > :grails-test-examples-app1:integrationTest
CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 0) > :grails-test-examples-mongodb-hibernate5:test
CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 0) > :grails-test-suite-persistence:test
CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1) > :grails-data-mongodb-core:test
CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1) > :grails-data-mongodb-core:test (first 40 of 99)
CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1) > :grails-test-examples-mongodb-hibernate5:integrationTest
CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 2) > :grails-data-mongodb-spring-boot:test
CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 2) > :grails-data-mongodb-spring-data:test
CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 2) > :grails-test-examples-mongodb-base:test
CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 2) > :grails-test-examples-mongodb-database-per-tenant:integrationTest
CI / Build Grails-Core (Ubuntu JDK 21) > :grails-test-suite-persistence:test
CI / Build Grails-Core (Ubuntu JDK 25) > :grails-test-suite-persistence:test
CI / Build Grails-Core (Windows JDK 25 shard 0) > :grails-test-suite-persistence:test
CI / Build Grails-Core (macOS JDK 21) > :grails-test-suite-persistence:test
CI / Build Grails-Core Rerunning all Tasks (Ubuntu JDK 21 shard 0) > :grails-test-suite-persistence:test
🏷️ Commit: 3ce9e88 Test Failures (first 10 of 128)BookFongoSpec > Test GORM access (:grails-test-examples-mongodb-hibernate5:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 0))GrailsWebDataBinderSpec > Test Map constructor fallback binds unconfigured properties but not bindable false by default (:grails-test-suite-persistence:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 0))GrailsWebDataBinderSpec > Test Map constructor fallback remains permissive for explicit bind-all (:grails-test-suite-persistence:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 0))GrailsWebDataBinderSpec > Test Map constructor fallback remains permissive in compatibility mode (:grails-test-suite-persistence:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 0))AssignedIdentifierSpec > Test that entities can be saved, retrieved and updated with assigned ids (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1))AssignedIdentifierSpec > Test that saving a second object with an assigned identifier produces an error (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1))BasicCollectionTypeSpec > Test persist basic collection types (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1))BatchUpdateDeleteSpec > Test that batch delete works (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1))BatchUpdateDeleteSpec > Test that batch update works (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1))BatchUpdateDeleteSpec > Test that batch update works with domain properties (:grails-data-mongodb-core:test in CI - Groovy Joint Validation Build / Build Grails with Groovy snapshot (shard 1))Rerun Controls (first 20 of 131)Select tests to mute in this pull request:
Reuse successful test results:
Click the checkbox to trigger a rerun:
Learn more about TestLens at testlens.app/docs. |
|
@codeconsole #16178 and this PR have a lot of overlap for Grails 8 on Groovy 5. Since Groovy 5 Indy is not fast enough in Grails I am not sure we need to publish the extra modules. Then on Groovy 6, we will flip everything to Indy, which will be the first time Grails is on Indy, with the exception of the narrow window at the beginning of Grails 7. |
Rebased onto latest
8.0.x(d0c68dc, which brings Groovy 5.1.0) and reworked to the design the Grails team asked for: the main artifact is the callsite-caching flavour, andinvokedynamicships under theindyclassifier.What an application sees
Nothing, unless it opts in.
grails.indydefaults tofalse, exactly as 7.x behaved, and every Grails module and plugin publishes its main artifact compiled that way.An application that wants
invokedynamicgets it for its own classes and its dependencies:grails { indy = true indyModules = ['org.apache.grails:grails-core', 'com.example:my-plugin'] }Dependencies left off the list keep their main artifact. That is safe on the JVM — the two flavours interoperate on the same classpath — but a native image needs the
indyflavour of everything, so an application targeting one lists every Grails module and plugin it uses.Plugins publish both
A project applying
org.apache.grails.gradle.grails-pluginbuilds both flavours through the newindyJartask, so any application that wants indy can have it.grails.indyno longer affects how a plugin compiles its own sources; plugin authors need to change nothing, and a plugin build that setsindycan drop it.Why a classifier and not a variant
The previous revision of this PR published the
indyjar as a secondary Gradle variant, and CI failed across the board:A configuration that requests no attributes at all — the shape
tckand the CLI companion probe use, here and in other people's builds — finds every variant equally good. No attribute rule repairs it: a disambiguation rule only receives the values candidates declare, so the variant without the attribute never appears among them (consumer=null candidates=[false]), and declaring it on both variants breaks every plain consumer instead.A plain Maven classifier adds nothing to the published metadata, so the ambiguity is unreachable rather than fixed. Selection is assembled on the consuming side: an application that opts in registers a
ComponentMetadataRulederiving a variant from the classifier for the modules it lists. That variant exists only inside the opted-in build, whose schema also carries the rules needed to choose.Verification
10 functional tests in
GrailsIndyVariantsSpec, including the two shapes that broke CI last time — an artifact view constraining nothing, and a configuration with no attributes — both now resolving a single artifact.On
grails-cache, a real plugin module with anastsource set:invokedynamic-indyEntry lists identical; only the Groovy call-site bytecode differs. (The 1 in the main jar is
makeConcatWithConstantsin a.javasource file — javac's string concatenation, not a Groovy call site.)Suite: 188/191. The 3
GroovyPageToolchainSpecfailures need a JDK 21 toolchain that is not installed on the machine I ran them on and fail identically on unmodified8.0.x.codeStyleclean.Known limitations
indyModulesmust be listed explicitly. An unguarded rule fails hard trying to fetch e.g.groovy-5.1.0-indy.jar, and platforms fail the same way —grails-bomis a platform every application depends on — so there is no safe group-wide default. Modules advertise their classifier throughGrails-Indy-Artifact(mirroringGrails-Cli-Artifact) so the list can later be discovered by a probe rather than written; that discovery is not implemented here.Reviewer notes
Full
./gradlew buildhas not been run;grails-cachewas built and inspected locally. TheindyJartask assembles by unpacking the finished main jar rather than listing source directories — enumerating them omitted the AST classes and staged command/template resources, and reusing the main jar's copy spec imported itsduplicatesStrategy. Both were caught only by building a real module, not by the functional tests.