Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build-logic/docs-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ sourceSets {
}
}

// docs-core does not apply org.apache.grails.buildsrc.compile. Keep the same
// Grails 8 default (indy off) as CompilePlugin. See #15293.
apply from: layout.projectDirectory.file('../../gradle/groovy-indy.gradle')

def docFilesJar = tasks.register('docFilesJar', Jar)
docFilesJar.configure {Jar it ->
it.description = 'Package up files used for generating documentation.'
Expand Down
4 changes: 4 additions & 0 deletions build-logic/plugins/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ 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.
apply from: layout.projectDirectory.file('../../gradle/groovy-indy.gradle')

gradlePlugin {
plugins {
register('compilePlugin') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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')
}

def "trims whitespace when parsing grailsIndy"() {
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()
}
}
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 3 additions & 1 deletion gradle/grails-extension-gradle-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ 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)
indy = project.property('grailsIndy').toString().trim().toBoolean()
}
}
34 changes: 34 additions & 0 deletions gradle/groovy-indy.gradle
Original file line number Diff line number Diff line change
@@ -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 = project.property('grailsIndy').toString().trim().toBoolean()
}

project.pluginManager.withPlugin('groovy') {
project.tasks.withType(org.gradle.api.tasks.compile.GroovyCompile).configureEach { compileTask ->
compileTask.groovyOptions.optimizationOptions.indy = grailsIndyEnabled
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import groovy.transform.CompileStatic
import groovy.transform.TypeCheckingMode
import groovy.xml.slurpersupport.GPathResult
import org.codehaus.groovy.reflection.CachedMethod
import org.codehaus.groovy.runtime.InvokerHelper

import grails.databinding.converters.FormattedValueConverter
import grails.databinding.converters.ValueConverter
Expand Down Expand Up @@ -430,12 +431,24 @@ class SimpleDataBinder implements DataBinder {
try {
instance = referencedType.getDeclaredConstructor().newInstance()
} catch (NoSuchMethodException | IllegalAccessException ignored) {
return referencedType.newInstance(values)
return newInstanceFromMapArguments(referencedType, values)
}
bind(instance, new SimpleMapDataBindingSource(values), listener)
instance
}

/**
* Invoke a {@code Map} constructor without calling Groovy's
* {@code Class.newInstance(Map)}. Under {@code @CompileStatic} with
* invokedynamic disabled that extension is not selected, so nested
* objects with only a Map constructor are left unbound.
*/
protected Object newInstanceFromMapArguments(Class referencedType, Map values) {
// Pass an Object[] so CompileStatic cannot treat the Map as named
// arguments or coerce it to a multi-arg constructor signature.
InvokerHelper.invokeConstructorOf(referencedType, new Object[] { values })
}

@CompileStatic(TypeCheckingMode.SKIP)
protected initializeArray(obj, String propertyName, Class arrayType, int index) {
Object[] array = obj[propertyName]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,15 +377,20 @@ class GormStaticApi<D> extends AbstractGormApi<D> implements GormAllOperations<D

@Override
Integer count() {
log.debug('GormStaticApi.count() called for {}', persistentClass.name)
// Capture the @Slf4j logger before entering the SessionCallback. With
// invokedynamic off (Grails 8 default), log.debug(...) inside that
// closure is dispatched through methodMissing as a dynamic finder on
// the persistent class (MissingMethodException: debug).
def logger = log
logger.debug('GormStaticApi.count() called for {}', persistentClass.name)
Integer result = execute({ Session session ->
def query = session.createQuery(persistentClass)
query.projections().count()
def res = query.singleResult()
log.debug('Query singleResult returned {}', res)
logger.debug('Query singleResult returned {}', res)
res instanceof Number ? ((Number)res).intValue() : 0
} as SessionCallback<Integer>)
log.debug('count() result is {}', result)
logger.debug('count() result is {}', result)
return result
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ class GormStaticApiSpec extends Specification {
api.executeQualified(ConnectionSource.DEFAULT, { Session session -> 'ran' }) == 'ran'
}

void "count() does not dispatch log.debug through methodMissing"() {
given:
def api = new GormStaticApi(GormStaticApiThing, datastore, [])

when:
Integer n = api.count()

then:
n == 0
}

void "getGormDynamicFinders returns the finders the api was constructed with"() {
given:
def finder = Stub(org.grails.datastore.gorm.finders.FinderMethod)
Expand Down
2 changes: 2 additions & 0 deletions grails-forge/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions grails-gradle/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2163,7 +2163,8 @@ class SecureMapConstructorValue implements Validateable {

SecureMapConstructorValue(Map values) {
name = values.name
admin = values.admin as boolean
// Groovy 5 without invokedynamic throws on `null as boolean`.
admin = Boolean.TRUE.equals(values.admin)
}

static constraints = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,8 @@ class GrailsWebDataBinder extends SimpleDataBinder {
if (value instanceof Map) {
if (isBindAllIncludeList(includeList) ||
!DataBindingUtils.isDenyByDefaultEnabled()) {
return referencedType.newInstance(filterUnbindableMapConstructorArguments(referencedType, (Map) value))
return newInstanceFromMapArguments(referencedType,
filterUnbindableMapConstructorArguments(referencedType, (Map) value))
}
if (DataBindingUtils.isGeneratedBindingIncludeList(bindingIncludeList.get())) {
warnAboutMissingNoArgConstructor(referencedType)
Expand Down
Loading