This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch fix/disable-groovy-indy-build in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit d9a19f508b92b7b3e6059dbb9b090f48b0ce6d53 Author: James Fredley <[email protected]> AuthorDate: Wed Aug 19 19:41:25 2026 -0400 build: disable Groovy invokedynamic for the Grails 8 compile Groovy 5 defaults indy on. Only modules that apply the Grails Gradle plugin inherited grails.indy=false, so published framework artifacts were mixed. Centralize indy=false in CompilePlugin and apply gradle/groovy-indy.gradle from the grails-core, grails-gradle, and grails-forge builds. CI can still opt in with -PgrailsIndy=true. See #15293 Assisted-by: Sisyphus:grok-4.6 --- build-logic/docs-core/build.gradle | 7 ++ build-logic/plugins/build.gradle | 7 ++ .../apache/grails/buildsrc/CompilePlugin.groovy | 7 ++ .../grails/buildsrc/CompilePluginSpec.groovy | 93 ++++++++++++++++++++++ build.gradle | 2 + gradle/grails-extension-gradle-config.gradle | 2 + gradle/groovy-indy.gradle | 34 ++++++++ grails-forge/build.gradle | 2 + grails-gradle/build.gradle | 2 + 9 files changed, 156 insertions(+) diff --git a/build-logic/docs-core/build.gradle b/build-logic/docs-core/build.gradle index d3459b3331..c52c7515df 100644 --- a/build-logic/docs-core/build.gradle +++ b/build-logic/docs-core/build.gradle @@ -71,6 +71,13 @@ sourceSets { } } +// docs-core does not apply org.apache.grails.buildsrc.compile. Keep the same +// Grails 8 default (indy off) as CompilePlugin. See #15293. +tasks.withType(GroovyCompile).configureEach { + groovyOptions.optimizationOptions.indy = project.hasProperty('grailsIndy') && + Boolean.parseBoolean(project.property('grailsIndy') as String) +} + def docFilesJar = tasks.register('docFilesJar', Jar) docFilesJar.configure {Jar it -> it.description = 'Package up files used for generating documentation.' diff --git a/build-logic/plugins/build.gradle b/build-logic/plugins/build.gradle index e6d687e1f8..b7c64b86a3 100644 --- a/build-logic/plugins/build.gradle +++ b/build-logic/plugins/build.gradle @@ -57,6 +57,13 @@ tasks.named('test') { useJUnitPlatform() } +// This project compiles CompilePlugin itself, so it cannot apply that plugin. +// Keep the same Grails 8 default (indy off) as CompilePlugin. See #15293. +tasks.withType(GroovyCompile).configureEach { + groovyOptions.optimizationOptions.indy = project.hasProperty('grailsIndy') && + Boolean.parseBoolean(project.property('grailsIndy') as String) +} + gradlePlugin { plugins { register('compilePlugin') { diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy index 38c31e0b12..5275928991 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy @@ -35,6 +35,7 @@ import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.javadoc.Javadoc import org.gradle.external.javadoc.StandardJavadocDocletOptions +import static org.apache.grails.buildsrc.GradleUtils.lookupProperty import static org.apache.grails.buildsrc.GradleUtils.lookupPropertyByType @CompileStatic @@ -107,6 +108,12 @@ class CompilePlugin implements Plugin<Project> { it.groovyOptions.encoding = StandardCharsets.UTF_8.name() // Preserve method parameter names in Groovy/Java classes for IDE parameter hints & bean reflection metadata. it.groovyOptions.parameters = true + // Grails 8 keeps invokedynamic off. Groovy 5's compiler default is indy=true, + // which is a large runtime regression for dynamic Groovy (see #15293). Modules + // that do not apply the Grails Gradle plugin would otherwise inherit that + // default. Grails 9 / Groovy 6 can flip this. CI can still opt in with + // -PgrailsIndy=true (same property as grails-extension-gradle-config.gradle). + it.groovyOptions.optimizationOptions.put('indy', lookupProperty(project, 'grailsIndy', false)) // encoding needs to be the same since it's different across platforms it.options.encoding = StandardCharsets.UTF_8.name() it.options.fork = true diff --git a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginSpec.groovy b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginSpec.groovy new file mode 100644 index 0000000000..04b707a2ae --- /dev/null +++ b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginSpec.groovy @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.grails.buildsrc + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import spock.lang.Specification +import spock.lang.TempDir + +import java.nio.file.Path + +class CompilePluginSpec extends Specification { + + @TempDir + Path testProjectDir + + def setup() { + testProjectDir.resolve('settings.gradle').toFile().text = '' + testProjectDir.resolve('.asf.yaml').toFile().text = '' + def configScript = testProjectDir.resolve('gradle/groovy-compile-configscript.groovy').toFile() + configScript.parentFile.mkdirs() + configScript.text = '' + testProjectDir.resolve('build.gradle').toFile().text = """ + plugins { + id 'groovy' + id 'org.apache.grails.buildsrc.compile' + } + + ext { + javaVersion = 21 + grailsVersion = '8.0.0-SNAPSHOT' + formattedBuildDate = '2026-01-01' + } + + repositories { + mavenCentral() + } + + tasks.register('printIndy') { + def compileTask = tasks.named('compileGroovy', org.gradle.api.tasks.compile.GroovyCompile) + def testCompileTask = tasks.named('compileTestGroovy', org.gradle.api.tasks.compile.GroovyCompile) + doLast { + println "MAIN_INDY=\${compileTask.get().groovyOptions.optimizationOptions.indy}" + println "TEST_INDY=\${testCompileTask.get().groovyOptions.optimizationOptions.indy}" + } + } + """ + } + + def "disables invokedynamic on GroovyCompile tasks by default"() { + when: + def result = runPrintIndy() + + then: + result.task(':printIndy').outcome == TaskOutcome.SUCCESS + result.output.contains('MAIN_INDY=false') + result.output.contains('TEST_INDY=false') + } + + def "enables invokedynamic when grailsIndy is true"() { + when: + def result = runPrintIndy('-PgrailsIndy=true') + + then: + result.task(':printIndy').outcome == TaskOutcome.SUCCESS + result.output.contains('MAIN_INDY=true') + result.output.contains('TEST_INDY=true') + } + + private def runPrintIndy(String... extraArgs) { + GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments(['printIndy', '--stacktrace'] + (extraArgs as List)) + .withPluginClasspath() + .build() + } +} diff --git a/build.gradle b/build.gradle index 61f25361f5..b8fa7f63e5 100644 --- a/build.gradle +++ b/build.gradle @@ -109,6 +109,8 @@ final class ActiveProcessorCountArgumentProvider implements CommandLineArgumentP subprojects { + apply from: rootProject.layout.projectDirectory.file('gradle/groovy-indy.gradle') + tasks.withType(Test).configureEach { testTask -> testTask.jvmArgumentProviders.add(new ActiveProcessorCountArgumentProvider( Runtime.runtime.availableProcessors(), gradle.startParameter.maxWorkerCount)) diff --git a/gradle/grails-extension-gradle-config.gradle b/gradle/grails-extension-gradle-config.gradle index d491c3f2e5..bb3cc52e28 100644 --- a/gradle/grails-extension-gradle-config.gradle +++ b/gradle/grails-extension-gradle-config.gradle @@ -35,6 +35,8 @@ grails { // Allow CI to toggle Groovy invokedynamic (indy) via -PgrailsIndy=true // This enables testing functional tests with both indy enabled and disabled. // See: https://github.com/apache/grails-core/issues/15321 + // Framework modules that do not apply this plugin inherit the same default + // from org.apache.grails.buildsrc.compile (CompilePlugin). if (project.hasProperty('grailsIndy')) { indy = Boolean.parseBoolean(project.property('grailsIndy') as String) } diff --git a/gradle/groovy-indy.gradle b/gradle/groovy-indy.gradle new file mode 100644 index 0000000000..33fc608be8 --- /dev/null +++ b/gradle/groovy-indy.gradle @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Grails 8 keeps invokedynamic off. Groovy 5's compiler default is indy=true, +// which is a large runtime regression for dynamic Groovy (see #15293). +// Applied from each independent build (grails-core, grails-gradle, grails-forge) +// so modules that never apply the Grails Gradle plugin still inherit this. +// Grails 9 / Groovy 6 can flip the default. CI can still opt in with -PgrailsIndy=true. +boolean grailsIndyEnabled = false +if (project.hasProperty('grailsIndy')) { + grailsIndyEnabled = Boolean.parseBoolean(project.property('grailsIndy') as String) +} + +project.pluginManager.withPlugin('groovy') { + project.tasks.withType(org.gradle.api.tasks.compile.GroovyCompile).configureEach { compileTask -> + compileTask.groovyOptions.optimizationOptions.indy = grailsIndyEnabled + } +} diff --git a/grails-forge/build.gradle b/grails-forge/build.gradle index a149ee6e11..81570aaa0b 100644 --- a/grails-forge/build.gradle +++ b/grails-forge/build.gradle @@ -83,6 +83,8 @@ allprojects { } subprojects { + apply from: rootProject.layout.projectDirectory.file('../gradle/groovy-indy.gradle') + configurations.configureEach { resolutionStrategy { def cacheHours = isCiBuild || isReproducibleBuild ? 0 : 24 diff --git a/grails-gradle/build.gradle b/grails-gradle/build.gradle index bc42c184e2..43b1ee10b4 100644 --- a/grails-gradle/build.gradle +++ b/grails-gradle/build.gradle @@ -72,6 +72,8 @@ final class ActiveProcessorCountArgumentProvider implements CommandLineArgumentP } subprojects { + apply from: rootProject.layout.projectDirectory.file('../gradle/groovy-indy.gradle') + tasks.withType(Test).configureEach { testTask -> testTask.jvmArgumentProviders.add(new ActiveProcessorCountArgumentProvider( Runtime.runtime.availableProcessors(), gradle.startParameter.maxWorkerCount))
