jamesfredley commented on code in PR #16114:
URL: https://github.com/apache/grails-core/pull/16114#discussion_r3741747943


##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -204,33 +199,63 @@ class GrailsGradlePlugin implements Plugin<Project> {
             if (grailsExtension != null) {
                 c.groovyOptions.forkOptions.jvmArgumentProviders.add(new 
GrailsCompileStaticArtefactsProvider(grailsExtension.compileStatic))
             }
-            Closure<String> userScriptGenerator = getGroovyCompilerScript(c, 
project)
-            c.doFirst {
-                // This isn't ideal - we're performing configuration at 
execution time, but the alternative would be having
-                // to maintain a clean / configuration task and then gradle 
would want to cache those tasks.  Since the inputs
-                // to those tasks would effectively be the runtimeClasspath, 
dependency problems can arise if another task
-                // changes the runtimeClasspath. To prevent having to add 
those tasks into the dependency chain, use doFirst
-                File combinedFile = groovyCompilerConfigFile.get().asFile
-                if (!combinedFile.exists()) {
-                    combinedFile.parentFile.mkdirs()
-                    combinedFile.createNewFile()
-                }
+        }
 
-                String configuredScript = null
-                if (c.groovyOptions.configurationScript) {
-                    configuredScript = 
c.groovyOptions.configurationScript.text?.trim() ?: null
-                }
-                String grailsScript = userScriptGenerator?.call()
+        // The combined compiler configuration script is produced by its own 
task rather than from a
+        // doFirst on the compile task. Gradle finalizes task properties 
before any task action runs,
+        // so assigning groovyOptions.configurationScript from doFirst fails 
from Gradle 9.7 on, where
+        // GroovyCompileOptions became a lazy property — "The value for task 
':compileGroovy' property
+        // 'groovyOptions.configurationScriptFile' is final and cannot be 
changed any further." Once
+        // the property is assigned during configuration, Gradle also treats 
the script as an input
+        // file that has to exist before the compile task runs, which a 
producing task guarantees
+        // across a `clean build` and a doFirst cannot.
+        //
+        // Wiring happens after evaluation so a configurationScript set by the 
build script is already
+        // in place and gets folded into the combined file rather than 
clobbered. Names are read via
+        // TaskCollection.names, which does not realize the tasks.
+        project.afterEvaluate {

Review Comment:
   This one-time snapshot regresses the previous live `configureEach` behavior. 
A `GroovyCompile` registered from a later `projectsEvaluated` callback is 
absent from `names` and receives no generator/configuration script; I 
reproduced this against this head (`LATE_GROOVY_UNCONFIGURED=true`, with no 
`generateLateGroovyGrailsCompilerConfig` task). Please retain live wiring 
without registering a generator from inside the task-container callback (for 
example, a deferred task rule plus string `dependsOn`) and add a late-task 
TestKit case.



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -204,33 +199,63 @@ class GrailsGradlePlugin implements Plugin<Project> {
             if (grailsExtension != null) {
                 c.groovyOptions.forkOptions.jvmArgumentProviders.add(new 
GrailsCompileStaticArtefactsProvider(grailsExtension.compileStatic))
             }
-            Closure<String> userScriptGenerator = getGroovyCompilerScript(c, 
project)
-            c.doFirst {
-                // This isn't ideal - we're performing configuration at 
execution time, but the alternative would be having
-                // to maintain a clean / configuration task and then gradle 
would want to cache those tasks.  Since the inputs
-                // to those tasks would effectively be the runtimeClasspath, 
dependency problems can arise if another task
-                // changes the runtimeClasspath. To prevent having to add 
those tasks into the dependency chain, use doFirst
-                File combinedFile = groovyCompilerConfigFile.get().asFile
-                if (!combinedFile.exists()) {
-                    combinedFile.parentFile.mkdirs()
-                    combinedFile.createNewFile()
-                }
+        }
 
-                String configuredScript = null
-                if (c.groovyOptions.configurationScript) {
-                    configuredScript = 
c.groovyOptions.configurationScript.text?.trim() ?: null
-                }
-                String grailsScript = userScriptGenerator?.call()
+        // The combined compiler configuration script is produced by its own 
task rather than from a
+        // doFirst on the compile task. Gradle finalizes task properties 
before any task action runs,
+        // so assigning groovyOptions.configurationScript from doFirst fails 
from Gradle 9.7 on, where
+        // GroovyCompileOptions became a lazy property — "The value for task 
':compileGroovy' property
+        // 'groovyOptions.configurationScriptFile' is final and cannot be 
changed any further." Once
+        // the property is assigned during configuration, Gradle also treats 
the script as an input
+        // file that has to exist before the compile task runs, which a 
producing task guarantees
+        // across a `clean build` and a doFirst cannot.
+        //
+        // Wiring happens after evaluation so a configurationScript set by the 
build script is already
+        // in place and gets folded into the combined file rather than 
clobbered. Names are read via
+        // TaskCollection.names, which does not realize the tasks.
+        project.afterEvaluate {
+            project.tasks.withType(GroovyCompile).names.each { String 
compileTaskName ->
+                TaskProvider<GroovyCompile> compileTask = 
project.tasks.named(compileTaskName, GroovyCompile)
+                // Use a task-specific config file to avoid overlapping 
outputs when multiple
+                // GroovyCompile tasks exist in the same project (e.g. 
compileGroovy, compileTestGroovy).
+                Provider<RegularFile> groovyCompilerConfigFile = 
project.layout.buildDirectory.file("grailsGroovyCompilerConfig-${compileTaskName}.groovy")
+                File[] userConfigurationScript = new File[1]
+
+                TaskProvider<Task> generateGroovyCompilerConfig = 
project.tasks.register("generate${compileTaskName.capitalize()}GrailsCompilerConfig")
 { Task t ->
+                    t.description = "Generates the Grails Groovy compiler 
configuration script for ${compileTaskName}"
+                    t.outputs.file(groovyCompilerConfigFile)
+                    // Generating the script needs the resolved compile 
classpath, and declaring that as
+                    // an input would pull the runtimeClasspath into this 
task's up-to-date check. Since
+                    // the inputs to this task would effectively be the 
runtimeClasspath, dependency
+                    // problems can arise if another task changes the 
runtimeClasspath. Generating the
+                    // script is cheap, so skip state tracking and regenerate 
on every build instead.
+                    t.doNotTrackState('Depends on the resolved compile 
classpath; cheap to regenerate')
+                    t.doLast {
+                        File combinedFile = 
groovyCompilerConfigFile.get().asFile
+                        combinedFile.parentFile.mkdirs()
+
+                        String configuredScript = null
+                        if (userConfigurationScript[0]?.exists()) {
+                            configuredScript = 
userConfigurationScript[0].text?.trim() ?: null
+                        }
+                        String grailsScript = 
getGroovyCompilerScript(compileTask.get(), project)?.call()
+
+                        String combinedScripts = """
+                            // Grails groovy compilation configuration to 
ensure ASTs are applied correctly
+
+                            ${grailsScript?.trim() ?: ''}
 
-                String combinedScripts = """
-                    // Grails groovy compilation configuration to ensure ASTs 
are applied correctly
-                    
-                    ${grailsScript?.trim() ?: ''}
+                            ${configuredScript?.trim() ?: ''}
+                        """
+                        combinedFile.write(combinedScripts)
+                    }
+                }
 
-                    ${configuredScript?.trim() ?: ''}
-                """
-                combinedFile.write(combinedScripts)
-                c.groovyOptions.configurationScript = combinedFile
+                compileTask.configure { GroovyCompile c ->
+                    userConfigurationScript[0] = 
c.groovyOptions.configurationScript

Review Comment:
   This capture/replace is not stable for later configuration. When a user 
assigns `configurationScript` from a later `projectsEvaluated` callback, that 
assignment overwrites the combined script after this action. Reproduced result: 
the user's `LocalDate` import worked, but the generated Grails `CreatedDate` 
import disappeared and compilation failed. Please capture the final configured 
script at a graph-safe point (or wire it provider-first) and cover a 
later-callback assignment in TestKit.



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -204,33 +199,63 @@ class GrailsGradlePlugin implements Plugin<Project> {
             if (grailsExtension != null) {
                 c.groovyOptions.forkOptions.jvmArgumentProviders.add(new 
GrailsCompileStaticArtefactsProvider(grailsExtension.compileStatic))
             }
-            Closure<String> userScriptGenerator = getGroovyCompilerScript(c, 
project)
-            c.doFirst {
-                // This isn't ideal - we're performing configuration at 
execution time, but the alternative would be having
-                // to maintain a clean / configuration task and then gradle 
would want to cache those tasks.  Since the inputs
-                // to those tasks would effectively be the runtimeClasspath, 
dependency problems can arise if another task
-                // changes the runtimeClasspath. To prevent having to add 
those tasks into the dependency chain, use doFirst
-                File combinedFile = groovyCompilerConfigFile.get().asFile
-                if (!combinedFile.exists()) {
-                    combinedFile.parentFile.mkdirs()
-                    combinedFile.createNewFile()
-                }
+        }
 
-                String configuredScript = null
-                if (c.groovyOptions.configurationScript) {
-                    configuredScript = 
c.groovyOptions.configurationScript.text?.trim() ?: null
-                }
-                String grailsScript = userScriptGenerator?.call()
+        // The combined compiler configuration script is produced by its own 
task rather than from a
+        // doFirst on the compile task. Gradle finalizes task properties 
before any task action runs,
+        // so assigning groovyOptions.configurationScript from doFirst fails 
from Gradle 9.7 on, where
+        // GroovyCompileOptions became a lazy property — "The value for task 
':compileGroovy' property
+        // 'groovyOptions.configurationScriptFile' is final and cannot be 
changed any further." Once
+        // the property is assigned during configuration, Gradle also treats 
the script as an input
+        // file that has to exist before the compile task runs, which a 
producing task guarantees
+        // across a `clean build` and a doFirst cannot.
+        //
+        // Wiring happens after evaluation so a configurationScript set by the 
build script is already
+        // in place and gets folded into the combined file rather than 
clobbered. Names are read via
+        // TaskCollection.names, which does not realize the tasks.
+        project.afterEvaluate {
+            project.tasks.withType(GroovyCompile).names.each { String 
compileTaskName ->
+                TaskProvider<GroovyCompile> compileTask = 
project.tasks.named(compileTaskName, GroovyCompile)
+                // Use a task-specific config file to avoid overlapping 
outputs when multiple
+                // GroovyCompile tasks exist in the same project (e.g. 
compileGroovy, compileTestGroovy).
+                Provider<RegularFile> groovyCompilerConfigFile = 
project.layout.buildDirectory.file("grailsGroovyCompilerConfig-${compileTaskName}.groovy")
+                File[] userConfigurationScript = new File[1]
+
+                TaskProvider<Task> generateGroovyCompilerConfig = 
project.tasks.register("generate${compileTaskName.capitalize()}GrailsCompilerConfig")
 { Task t ->
+                    t.description = "Generates the Grails Groovy compiler 
configuration script for ${compileTaskName}"
+                    t.outputs.file(groovyCompilerConfigFile)
+                    // Generating the script needs the resolved compile 
classpath, and declaring that as
+                    // an input would pull the runtimeClasspath into this 
task's up-to-date check. Since
+                    // the inputs to this task would effectively be the 
runtimeClasspath, dependency
+                    // problems can arise if another task changes the 
runtimeClasspath. Generating the
+                    // script is cheap, so skip state tracking and regenerate 
on every build instead.
+                    t.doNotTrackState('Depends on the resolved compile 
classpath; cheap to regenerate')

Review Comment:
   `doNotTrackState` suppresses validation but does not establish execution 
ordering. The `doLast` calls `getGroovyCompilerScript`, which scans 
`compileTask.classpath`, while this generator has no dependency on tasks 
producing those classpath entries. I forced a classpath producer to run after 
the generator on a clean build; the generator omitted `grails.gorm.annotation`, 
then `compileGroovy` failed with `unable to resolve class CreatedDate`. Please 
model the classpath/base script as inputs and depend on their producer tasks.



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