Skip to content

Gradle 9.7.0 Upgrade - Generate the Groovy compiler config script in its own task (fixes Gradle 9.7) - #16114

Open
codeconsole wants to merge 3 commits into
apache:8.0.xfrom
codeconsole:fix/gradle-97-groovy-config-script
Open

Gradle 9.7.0 Upgrade - Generate the Groovy compiler config script in its own task (fixes Gradle 9.7)#16114
codeconsole wants to merge 3 commits into
apache:8.0.xfrom
codeconsole:fix/gradle-97-groovy-config-script

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

Problem

GrailsGradlePlugin.configureGroovyCompiler assigns groovyOptions.configurationScript from a doFirst on each GroovyCompile task. Gradle finalizes task properties before any task action runs, so from Gradle 9.7 — where GroovyCompileOptions became a lazy property — every Groovy compilation in every Grails project fails:

* What went wrong:
Execution failed for task ':compileGroovy' (registered by plugin class 'org.gradle.api.plugins.GroovyBasePlugin').
> The value for task ':compileGroovy' property 'groovyOptions.configurationScriptFile' is final and cannot be changed any further.
at org.gradle.api.tasks.compile.GroovyCompileOptions.setConfigurationScript(GroovyCompileOptions.java:216)
at org.grails.gradle.plugin.core.GrailsGradlePlugin$_configureGroovyCompiler_closure4$_closure37.doCall(GrailsGradlePlugin.groovy:233)

This blocks Grails 8 on Gradle 9.7 entirely — there is no user-side workaround, and no Gradle opt-out flag.

Fix

The combined compiler configuration script is now produced by a dedicated task that the compile task depends on.

Assigning the property during configuration is necessary but not sufficient: Gradle then treats the script as an input file that must exist before the compile task runs, which fails on a clean build because doFirst runs after input validation. A producing task is what makes the file exist at the right moment.

The generator task is marked doNotTrackState, which preserves the property the original doFirst was protecting — the existing comment noted that generating the script needs the resolved compile classpath, and declaring that as an input would pull the runtimeClasspath into the task's up-to-date check. The script is cheap to build, so it is regenerated on every build instead.

Wiring moves into afterEvaluate so a configurationScript set by the build script is already in place and gets folded into the combined script rather than clobbered — preserving the merge the old execution-time read performed. Task names are read via TaskCollection.names, which does not realize the tasks.

Testing

  • :grails-gradle-plugins:test passes.
  • Verified against a multi-project Grails 8 application (~20 subprojects, Groovy + GSP + plugins) on both Gradle 9.6.1 and 9.7.0:
    • full bootJar succeeds on both;
    • clean plus compilation in a single invocation succeeds (the case a doFirst cannot satisfy);
    • the generated grailsGroovyCompilerConfig-<task>.groovy still carries the expected withConfig(configuration) { inline(phase: 'CONVERSION') ... } metadata (projectVersion, projectName, isPlugin).

Without this change, that same application fails on Gradle 9.7.0 at the first compileGroovy.

`configureGroovyCompiler` assigned `groovyOptions.configurationScript` from a
`doFirst` on each GroovyCompile task. Gradle finalizes task properties before
any task action runs, so from Gradle 9.7 — where GroovyCompileOptions became a
lazy property — every Groovy compilation fails:

    Execution failed for task ':compileGroovy'.
    > The value for task ':compileGroovy' property
      'groovyOptions.configurationScriptFile' is final and cannot be changed
      any further.

Assigning the property during configuration is not enough on its own: Gradle
then treats the script as an input file that must exist before the compile task
runs, which fails on a clean build because `doFirst` runs after input
validation. So the combined script is now produced by a dedicated task that the
compile task depends on.

That task is marked `doNotTrackState`, which keeps the property that motivated
the original `doFirst`: generating the script needs the resolved compile
classpath, and declaring it as an input would pull the runtimeClasspath into the
task's up-to-date check. The script is cheap to build, so it is simply
regenerated on every build.

Wiring moves to `afterEvaluate` so a `configurationScript` set by the build
script is already in place and gets folded into the combined script rather than
clobbered — the merge the old execution-time read performed. Task names are read
via `TaskCollection.names`, which does not realize the tasks.

Verified against a multi-project Grails 8 application on both Gradle 9.6.1 and
9.7.0: full `bootJar`, and `clean` plus compilation in a single invocation.
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.3350%. Comparing base (a1e526f) to head (17f81fe).
⚠️ Report is 19 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
...rails/gradle/plugin/core/GrailsGradlePlugin.groovy 0.0000% 25 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #16114        +/-   ##
==================================================
- Coverage     52.3431%   52.3350%   -0.0081%     
  Complexity      18296      18296                
==================================================
  Files            2036       2036                
  Lines           96347      96381        +34     
  Branches        16829      16835         +6     
==================================================
+ Hits            50431      50441        +10     
- Misses          38492      38514        +22     
- Partials         7424       7426         +2     
Files with missing lines Coverage Δ
...rails/gradle/plugin/core/GrailsGradlePlugin.groovy 0.0000% <0.0000%> (ø)

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

@codeconsole codeconsole changed the title Generate the Groovy compiler config script in its own task (fixes Gradle 9.7) Gradle 9.7.0 Upgrade - Generate the Groovy compiler config script in its own task (fixes Gradle 9.7) Aug 7, 2026
Moves grails-core's own build onto 9.7.0: `.sdkmanrc`, `gradleToolingApiVersion`,
and the wrapper for every Gradle build in the repository — root, build-logic,
grails-gradle, grails-forge, end-to-end — plus the grails-shell-cli gradle-sample
fixture.

The shared wrappers were regenerated with `gradle -p gradle-bootstrap`; the
gradle-sample fixture, which bootstrap does not reach, was refreshed from the
same output so the launcher scripts and wrapper jar stay byte-identical
everywhere.

`end-to-end/legacy-g7-command-plugin` deliberately stays on Gradle 8.14.5, the
version Grails 7 pins for that fixture.

The wrapper task regenerates the properties files from scratch, so the
"keep this synced" checklist comments were restored afterwards.
Applications created by the forge and by the profile CLIs now get a 9.7.0
wrapper instead of 9.6.0.

For the forge, that means the `gradleWrapperProperties` template plus the three
binaries `Gradle.java` copies onto the generated project — `gradlew`,
`gradlew.bat`, and `gradle/wrapper/gradle-wrapper.jar`.

For the profile CLIs, only the `base` and `profile` skeletons carry wrapper
assets; `web`, `rest-api`, and `plugin` inherit them from `base` through
`profileRuntimeApi`, and `web-plugin` / `rest-api-plugin` chain through those,
so every application type picks the new wrapper up.

Generated applications can only move to 9.7 together with the compiler config
script fix earlier in this branch — without it every Groovy compilation in a
Grails project fails on Gradle 9.7.
@testlens-app

testlens-app Bot commented Aug 7, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 17f81fe
▶️ Tests: 59137 executed
⚪️ Checks: 60/60 completed


Learn more about TestLens at testlens.app.

@codeconsole
codeconsole requested review from borinquenkid, jamesfredley, jdaugherty, matrei and sbglasius and removed request for jdaugherty and matrei August 7, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant