You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ma...@apache.org on 2026/09/18 07:35:46 UTC

(grails-core) 01/01: Call the asset classpath configuration statically

This is an automated email from the ASF dual-hosted git repository.

matrei pushed a commit to branch fix/web-plugin-daemon-reuse-16347
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 0ac295582d7214f1e77240b89515711a76361060
Author: Mattias Reichel <ma...@gmail.com>
AuthorDate: Fri Sep 18 09:30:15 2026 +0200

    Call the asset classpath configuration statically
    
    configureAssetCompilation() was compiled dynamically and called the
    private configureAssetsOnTheClasspath() of the same class. Once the
    buildProperties task has executed, Gradle replaces the meta class of
    the applied plugin class with CallInterceptingMetaClass. Under the
    Groovy 3 runtime of Gradle 8 a dynamic call through that meta class
    does not find a private method of the superclass when the applied
    plugin is GrailsWebGradlePlugin, so every later build on the same
    daemon failed while applying the plugin with a MissingMethodException.
    
    Only the access to the asset pipeline's extension and task needs
    dynamic dispatch, so that part moves into its own method and
    configureAssetCompilation() is statically compiled.
    
    Adds a functional test that applies the web plugin twice on one daemon
    with buildProperties executed in between.
    
    Fixes #16347
---
 .../gradle/plugin/core/GrailsGradlePlugin.groovy   | 15 +++++-
 .../core/GrailsWebDaemonReuseFunctionalSpec.groovy | 56 ++++++++++++++++++++++
 .../test-projects/web-daemon-reuse/build.gradle    |  3 ++
 .../web-daemon-reuse/gradle.properties             |  1 +
 .../grails-app/conf/application.yml                |  0
 .../test-projects/web-daemon-reuse/settings.gradle |  1 +
 6 files changed, 74 insertions(+), 2 deletions(-)

diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
index 50ee24fdab..c4a2a90bed 100644
--- a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
+++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
@@ -940,8 +940,20 @@ ${importStatements}
         grailsVersion
     }
 
-    @CompileDynamic
     protected void configureAssetCompilation(Project project) {
+        configureAssetPipelineLayout(project)
+        configureAssetsOnTheClasspath(project)
+    }
+
+    /**
+     * Only the asset pipeline's own extension and task need dynamic dispatch, as the plugin is not
+     * a compile-time dependency. Calls to this plugin's own private methods stay out of here: on a
+     * reused daemon Gradle replaces the meta class of the applied plugin class with one that
+     * dispatches on the runtime class alone, so under Gradle 8 a private method of this class is
+     * not found when the applied plugin is a subclass.
+     */
+    @CompileDynamic
+    private static void configureAssetPipelineLayout(Project project) {
         if (project.extensions.findByName('assets')) {
             project.assets {
                 assetsPath = project.layout.projectDirectory.dir('grails-app/assets')
@@ -950,7 +962,6 @@ ${importStatements}
                 it.destinationDirectory = project.layout.buildDirectory.dir('assetCompile/assets')
             }
         }
-        configureAssetsOnTheClasspath(project)
     }
 
     /**
diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsWebDaemonReuseFunctionalSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsWebDaemonReuseFunctionalSpec.groovy
new file mode 100644
index 0000000000..9549bf4385
--- /dev/null
+++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsWebDaemonReuseFunctionalSpec.groovy
@@ -0,0 +1,56 @@
+/*
+ *  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.grails.gradle.plugin.core
+
+import org.gradle.testkit.runner.TaskOutcome
+
+/**
+ * Functional tests for applying {@code org.apache.grails.gradle.grails-web} repeatedly on one
+ * Gradle daemon, the way every build after the first does during normal development.
+ *
+ * <p>A daemon keeps the plugin classes loaded between builds. Once {@code buildProperties} has
+ * executed, Gradle has replaced the meta class of the applied plugin class with one that
+ * intercepts calls. Under the Groovy 3 runtime of Gradle 8 that meta class dispatches on the
+ * runtime class alone, so a private method of {@link GrailsGradlePlugin} called from dynamically
+ * compiled code was not found once the applied plugin was a subclass, and every later build on
+ * that daemon failed while the plugin was being applied. The Groovy 4 runtime of Gradle 9 resolves
+ * such a call, so this covers the reuse itself rather than that one dispatch.</p>
+ *
+ * @since 8.0
+ * @see org.grails.gradle.plugin.web.GrailsWebGradlePlugin
+ */
+class GrailsWebDaemonReuseFunctionalSpec extends GradleSpecification {
+
+    void 'the web plugin still applies on a daemon that has already executed buildProperties'() {
+        given:
+        setupTestResourceProject('web-daemon-reuse')
+
+        when:
+        def first = executeTask('buildProperties')
+
+        then:
+        first.task(':buildProperties').outcome == TaskOutcome.SUCCESS
+
+        when:
+        def second = executeTask('help')
+
+        then:
+        second.task(':help').outcome == TaskOutcome.SUCCESS
+    }
+}
diff --git a/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/build.gradle
new file mode 100644
index 0000000000..e0b1c5b1ae
--- /dev/null
+++ b/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/build.gradle
@@ -0,0 +1,3 @@
+plugins {
+    id 'org.apache.grails.gradle.grails-web'
+}
diff --git a/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/gradle.properties
new file mode 100644
index 0000000000..35c332fb87
--- /dev/null
+++ b/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/gradle.properties
@@ -0,0 +1 @@
+grailsVersion=__PROJECT_VERSION__
diff --git a/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/grails-app/conf/application.yml
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/settings.gradle
new file mode 100644
index 0000000000..e1140d4424
--- /dev/null
+++ b/grails-gradle/plugins/src/test/resources/test-projects/web-daemon-reuse/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'web-daemon-reuse'