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

jdaugherty pushed a commit to branch feature/exploded
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 75bb205cbd721e0335d45afe595889d8f916e65c
Author: James Daugherty <[email protected]>
AuthorDate: Wed Sep 17 14:41:01 2025 -0400

    feature: simplify reloading for multiproject builds
---
 .../plugins/creatingAndInstallingPlugins.adoc      | 253 +++------------------
 .../src/en/guide/upgrading/upgrading60x.adoc       |  21 ++
 .../src/main/groovy/grails/util/Environment.groovy |  10 -
 grails-gradle/plugins/build.gradle                 |   6 +
 .../gradle/plugin/core/GrailsExtension.groovy      |  40 +++-
 .../gradle/plugin/core/GrailsGradlePlugin.groovy   |  20 ++
 .../plugin/core/GrailsPluginGradlePlugin.groovy    |  63 +----
 .../grails/gradle/plugin/core/PluginDefiner.groovy |  68 ++----
 .../exploded/ExplodedCompatibilityRule.groovy      |  20 ++
 .../exploded/ExplodedDisambiguationRule.groovy     |  18 ++
 .../plugin/exploded/GrailsExplodedPlugin.groovy    |  87 +++++++
 11 files changed, 254 insertions(+), 352 deletions(-)

diff --git a/grails-doc/src/en/guide/plugins/creatingAndInstallingPlugins.adoc 
b/grails-doc/src/en/guide/plugins/creatingAndInstallingPlugins.adoc
index 83787d3b4b..373ae0786d 100644
--- a/grails-doc/src/en/guide/plugins/creatingAndInstallingPlugins.adoc
+++ b/grails-doc/src/en/guide/plugins/creatingAndInstallingPlugins.adoc
@@ -20,7 +20,7 @@ under the License.
 
 ==== Creating Plugins
 
-Creating a Grails plugin is a simple matter of running the command:
+Creating a Grails plugin is a simple matter of running one of the supported 
plugin generators:
 
 Grails Shell CLI
 [source,groovy]
@@ -120,7 +120,7 @@ Adds Quartz job scheduling features
     Closure doWithSpring()......
 ----
 
-==== Plugin Configuration
+===== Plugin Configuration
 
 A Grails plugin can be configured in one of the following files:
 
@@ -129,7 +129,7 @@ A Grails plugin can be configured in one of the following 
files:
 
 These files will be included in the plugin package while the `application.*` 
files are only used internally when developing the plugin. See 
<<excludedArtefacts>>.
 
-==== Reading configuration properties
+===== Reading configuration properties
 
 Instead of directly accessing Grails configuration as 
`grailsApplication.config.mail.hostName`, use a Spring Boot configuration bean 
(or a POJO) annotated with 
{springbootapi}org/springframework/boot/context/properties/ConfigurationProperties.html[ConfigurationProperties]
 annotation. Here is an example plugin configuration:
 
@@ -169,7 +169,7 @@ class MailService {
 
 Please read the {springBootReference}features/external-config.html[Spring Boot 
Externalized Configuration] section for more information.
 
-==== Installing Local Plugins
+===== Installing Local Plugins
 
 In order to install the Grails plugin to your local Maven, you could use 
Gradle https://docs.gradle.org/current/userguide/publishing_maven.html[Maven 
Publish] plugin. You may also need to configure the publishing extension as:
 
@@ -216,9 +216,7 @@ implementation "org.apache.grails:grails-quartz:0.1"
 
 NOTE: In Grails 2.x plugins were packaged as ZIP files, however from Grails 
3.x+ plugins are simple JAR files that can be added to the classpath of the IDE.
 
-
-
-==== Plugins and Multi-Project Builds
+===== Plugins and Multi-Project Builds
 
 
 If you wish to setup a plugin as part of a multi project build then follow 
these steps.
@@ -267,11 +265,18 @@ grails {
 }
 ----
 
-NOTE: You can also declare the dependency within the `dependencies` block, 
however you will not get subproject reloading if you do this!
+NOTE: You can also declare the dependency within the `dependencies` block, 
however reloading will not work correctly if defined this way.
 
-*Step 4: Configure the plugin to enable reloading*
+*Step 4: (Optional) Configure the plugin for reloading
+
+Within the `build.gradle` of the plugin, apply the gradle plugin 
`org.apache.grails.gradle.grails-exploded` to allow for proper reloading of the 
plugin:
+
+[source,groovy]
+----
+apply plugin: 'org.apache.grails.gradle.grails-exploded'
+----
 
-In the plugin directory, add or modify the `gradle.properties` file. A new 
property `exploded=true` needs to be set in order for the plugin to add the 
exploded directories to the classpath.
+NOTE: This plugin ensures that development commands such as `bootRun` use the 
exploded classes and resources of the plugin rather than the packaged JAR file.
 
 *Step 5: Run the application*
 
@@ -309,7 +314,7 @@ Grails application running at http://localhost:8080 in 
environment: development
 
 
 [[excludedArtefacts]]
-==== Notes on excluded Artefacts
+===== Notes on excluded Artefacts
 
 
 Although the link:{commandLineRef}create-plugin.html[create-plugin] command 
creates certain files for you so that the plugin can be run as a Grails 
application, not all of these files are included when packaging a plugin. The 
following is a list of artefacts created, but not included by 
link:{commandLineRef}package-plugin.html[package-plugin]:
@@ -323,8 +328,7 @@ Although the 
link:{commandLineRef}create-plugin.html[create-plugin] command crea
 * Everything within `/src/test/\*\*`
 * SCM management files within `\*\*/.svn/\*\*` and `\*\*/CVS/\*\*`
 
-
-==== Customizing the plugin contents
+===== Customizing the plugin contents
 
 
 When developing a plugin you may create test classes and sources that are used 
during the development and testing of the plugin but should not be exported to 
the application.
@@ -348,9 +352,7 @@ jar {
 }
 ----
 
-
-
-==== Inline Plugins in Grails
+===== Inline Plugins in Grails
 
 
 In Grails 2.x it was possible to specify inline plugins in `BuildConfig`, from 
Grails 3.x+ this functionality has been replaced by Gradle's multi-project 
build feature.
@@ -374,13 +376,18 @@ Finally add a dependency in your application's 
`build.gradle` on the plugin:
 
 [source,groovy]
 ----
-implementation project(':myplugin')
+grails {
+    plugins {
+        implementation project(':myplugin')
+    }
+}
 ----
 
-Using this technique you have achieved the equivalent of inline plugins from 
Grails 2.x.
+NOTE: You can also declare the dependency within the `dependencies` block, 
however your subproject will not reload correctly.
 
+Using this technique you have achieved the equivalent of inline plugins from 
Grails 2.x.
 
-==== Grails Forge Creating Plugins
+===== Grails Forge Creating Plugins
 
 
 Creating a Grails plugin is a simple matter of running the command:
@@ -509,211 +516,3 @@ class MailService {
 
 Please read the {springBootReference}features/external-config.html[Spring Boot 
Externalized Configuration] section for more information.
 
-==== Installing Local Plugins
-
-In order to install the Grails plugin to your local Maven, you could use 
Gradle https://docs.gradle.org/current/userguide/publishing_maven.html[Maven 
Publish] plugin. You may also need to configure the publishing extension as:
-
-[source,groovy]
-----
-publishing {
-    publications {
-        maven(MavenPublication) {
-            versionMapping {
-                usage('java-api') {
-                    fromResolutionOf('runtimeClasspath')
-                }
-                usage('java-runtime') {
-                    fromResolutionResult()
-                }
-            }
-            from components.java
-        }
-    }
-}
-----
-
-NOTE: Please refer to the Gradle Maven Publish plugin documentation for 
up-to-date information.
-
-To make your plugin available for use in a Grails application run the 
`./gradlew publishToMavenLocal` command:
-
-[source,bash]
-----
-./gradlew publishToMavenLocal
-----
-
-This will install the plugin into your local Maven cache. Then to use the 
plugin within an application declare a dependency on the plugin in your 
`build.gradle` file and include `mavenLocal()` in your repositories hash:
-
-[source,groovy]
-----
-...
-repositories {
-    ...
-    mavenLocal()
-}
-...
-implementation "org.apache.grails:grails-quartz:0.1"
-----
-
-NOTE: In Grails 2.x plugins were packaged as ZIP files, however from Grails 
3.x+ plugins are simple JAR files that can be added to the classpath of the IDE.
-
-
-
-==== Plugins and Multi-Project Builds
-
-
-If you wish to setup a plugin as part of a multi project build then follow 
these steps.
-
-*Step 1: Create the application and the plugin*
-
-Using the `grails` command create an application and a plugin:
-
-[source,groovy]
-----
-$ grails create-app myapp
-$ grails create-plugin myplugin
-----
-
-*Step 2: Create a settings.gradle file*
-
-In the same directory create a `settings.gradle` file with the following 
contents:
-
-[source,groovy]
-----
-include "myapp", "myplugin"
-----
-
-The directory structure should be as follows:
-
-[source,groovy]
-----
-PROJECT_DIR
-  - settings.gradle
-  - myapp
-    - build.gradle
-  - myplugin
-    - build.gradle
-----
-
-*Step 3: Declare a project dependency on the plugin*
-
-Within the `build.gradle` of the application declare a dependency on the 
plugin within the `plugins` block:
-
-[source,groovy]
-----
-grails {
-    plugins {
-        implementation project(':myplugin')
-    }
-}
-----
-
-NOTE: You can also declare the dependency within the `dependencies` block, 
however you will not get subproject reloading if you do this!
-
-*Step 4: Configure the plugin to enable reloading*
-
-In the plugin directory, add or modify the `gradle.properties` file. A new 
property `exploded=true` needs to be set in order for the plugin to add the 
exploded directories to the classpath.
-
-*Step 5: Run the application*
-
-Now run the application using the `./gradlew bootRun` command from the root of 
the application directory, you can use the `verbose` flag to see the Gradle 
output:
-
-[source,groovy]
-----
-$ cd myapp
-$ ./gradlew bootRun --verbose
-----
-
-You will notice from the Gradle output that plugins sources are built and 
placed on the classpath of your application:
-
-[source,groovy]
-----
-:myplugin:compileAstJava UP-TO-DATE
-:myplugin:compileAstGroovy UP-TO-DATE
-:myplugin:processAstResources UP-TO-DATE
-:myplugin:astClasses UP-TO-DATE
-:myplugin:compileJava UP-TO-DATE
-:myplugin:configScript UP-TO-DATE
-:myplugin:compileGroovy
-:myplugin:copyAssets UP-TO-DATE
-:myplugin:copyCommands UP-TO-DATE
-:myplugin:copyTemplates UP-TO-DATE
-:myplugin:processResources
-:myapp:compileJava UP-TO-DATE
-:myapp:compileGroovy
-:myapp:processResources UP-TO-DATE
-:myapp:classes
-:myapp:findMainClass
-:myapp:bootRun
-Grails application running at http://localhost:8080 in environment: development
-----
-
-
-==== Notes on excluded Artefacts
-
-
-Although the link:{commandLineRef}create-plugin.html[create-plugin] command 
creates certain files for you so that the plugin can be run as a Grails 
application, not all of these files are included when packaging a plugin. The 
following is a list of artefacts created, but not included by 
link:{commandLineRef}package-plugin.html[package-plugin]:
-
-* `grails-app/build.gradle` (although it is used to generate 
`dependencies.groovy`)
-* `grails-app/conf/logback.xml`
-* `grails-app/conf/logback-spring.xml`
-* `grails-app/conf/application.yml`
-* `grails-app/conf/application.groovy`
-* `grails-app/conf/spring/resources.groovy`
-* Everything within `/src/test/\*\*`
-* SCM management files within `\*\*/.svn/\*\*` and `\*\*/CVS/\*\*`
-
-
-==== Customizing the plugin contents
-
-
-When developing a plugin you may create test classes and sources that are used 
during the development and testing of the plugin but should not be exported to 
the application.
-
-To exclude test sources you need to modify the `pluginExcludes` property of 
the plugin descriptor AND exclude the resources inside your `build.gradle` 
file. For example say you have some classes under the `com.demo` package that 
are in your plugin source tree but should not be packaged in the application. 
In your plugin descriptor you should exclude these:
-
-[source,groovy]
-----
-// resources that should be loaded by the plugin once installed in the 
application
-  def pluginExcludes = [
-    '**/com/demo/**'
-  ]
-----
-
-And in your `build.gradle` you should exclude the compiled classes from the 
JAR file:
-
-[source,groovy]
-----
-jar {
-  exclude "com/demo/**/**"
-}
-----
-
-
-
-==== Inline Plugins in Grails
-
-
-In Grails 2.x it was possible to specify inline plugins in `BuildConfig`, from 
Grails 3.x+ this functionality has been replaced by Gradle's multi-project 
build feature.
-
-To set up a multi project build create an appliation and a plugin in a parent 
directory:
-
-[source,groovy]
-----
-$ grails create-app myapp
-$ grails create-plugin myplugin
-----
-
-Then create a `settings.gradle` file in the parent directory specifying the 
location of your application and plugin:
-
-[source,groovy]
-----
-include 'myapp', 'myplugin'
-----
-
-Finally add a dependency in your application's `build.gradle` on the plugin:
-
-[source,groovy]
-----
-implementation project(':myplugin')
-----
-
-Using this technique you have achieved the equivalent of inline plugins from 
Grails 2.x.
diff --git a/grails-doc/src/en/guide/upgrading/upgrading60x.adoc 
b/grails-doc/src/en/guide/upgrading/upgrading60x.adoc
index 57ccd1435e..c09ff4a6ad 100644
--- a/grails-doc/src/en/guide/upgrading/upgrading60x.adoc
+++ b/grails-doc/src/en/guide/upgrading/upgrading60x.adoc
@@ -492,3 +492,24 @@ This scenario is not supported and can lead to unexpected 
behavior due to the AS
 
 Starting with Grails 7, a validation error will trigger if both a Grails 
Application Gradle Plugin & a Grails Plugin Gradle Plugin are configured in 
`build.gradle` for the same Gradle Project.
 
+===== 12.21 `exploded` is supported again to enable multi-project reloading
+
+Earlier versions of Grails supported an `exploded` Gradle configuration that 
forced defined plugins to use class and resource files, instead of jar files, 
on the Grails Application runtime classpath if several conditions were true:
+1. the property `grails.run.active` was set 2. the plugin project had the 
property `exploded` set to `true` prior to the application of the 
`grails-plugin` Gradle plugin 3. plugins were added to the Grails Application 
project via the `plugins` block inside of the `grails` extension in 
`build.gradle` instead of the `dependencies` block 4. the property `exploded` 
was set on the `grails` extension in `build.gradle` of the Grails Application 
project
+
+The `exploded` setup facilitates better class reloading behavior.
+For Grails 7, it has been simplified to the following:
+
+1. In the plugin project, apply the gradle plugin 
`org.apache.grails.gradle.grails-exploded`
+2. In the application project, define plugins inside of the `plugins` block of 
the `grails` extension in `build.gradle`:
+
+[source,groovy]
+----
+grails {
+   plugins {
+        implementation project(":my-plugin")
+   }
+}
+----
+
+NOTE: Expanded class files & resource files will only be used over the jar 
file if the plugin applies the gradle plugin 
`org.apache.grails.gradle.grails-exploded` & the plugin is a subproject of your 
Gradle build.
diff --git a/grails-gradle/model/src/main/groovy/grails/util/Environment.groovy 
b/grails-gradle/model/src/main/groovy/grails/util/Environment.groovy
index a6a5a64d34..d8b83deb8e 100644
--- a/grails-gradle/model/src/main/groovy/grails/util/Environment.groovy
+++ b/grails-gradle/model/src/main/groovy/grails/util/Environment.groovy
@@ -351,16 +351,6 @@ enum Environment {
         return BuildSettings.GRAILS_APP_DIR_PRESENT && !isStandaloneDeployed() 
&& !isWarDeployed()
     }
 
-    /**
-     * This method will return true the application is run
-     *
-     * @return True if the development sources are present
-     */
-    static boolean isDevelopmentRun() {
-        Environment env = getCurrent()
-        return isDevelopmentEnvironmentAvailable() && 
Boolean.getBoolean(RUN_ACTIVE) && (env == Environment.DEVELOPMENT)
-    }
-
     /**
      * Checks if the run of the app is due to spring dev-tools or not.
      * @return True if spring-dev-tools restart
diff --git a/grails-gradle/plugins/build.gradle 
b/grails-gradle/plugins/build.gradle
index 1ea1c98f3a..01b230951e 100644
--- a/grails-gradle/plugins/build.gradle
+++ b/grails-gradle/plugins/build.gradle
@@ -104,6 +104,12 @@ gradlePlugin {
             id = 'org.apache.grails.gradle.grails-publish-profile'
             implementationClass = 
'org.grails.gradle.plugin.profiles.GrailsProfilePublishGradlePlugin'
         }
+        grailsExploded {
+            displayName = 'Grails Exploded Plugin'
+            description = 'A plugin that configures a Grails Plugin so that 
its classes and resources are exploded when running bootRun from a dependent 
Application'
+            id = 'org.apache.grails.gradle.grails-exploded'
+            implementationClass = 
'org.grails.gradle.plugin.exploded.GrailsExplodedPlugin'
+        }
     }
 }
 
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
index d20bdf60dd..d345847087 100644
--- 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy
@@ -22,6 +22,10 @@ import groovy.transform.CompileStatic
 
 import org.apache.tools.ant.taskdefs.condition.Os
 import org.gradle.api.Project
+import org.gradle.api.artifacts.dsl.DependencyHandler
+import org.gradle.util.internal.ConfigureUtil
+
+import grails.util.Environment
 
 /**
  * A extension to the Gradle plugin to configure Grails settings
@@ -33,10 +37,13 @@ import org.gradle.api.Project
 class GrailsExtension {
 
     Project project
+    PluginDefiner pluginDefiner
 
     GrailsExtension(Project project) {
         this.project = project
+        this.pluginDefiner = new PluginDefiner(project)
     }
+
     /**
      * Whether to invoke native2ascii on resource bundles
      */
@@ -52,11 +59,6 @@ class GrailsExtension {
      */
     boolean packageAssets = true
 
-    /**
-     * Whether to include subproject dependencies as directories directly on 
the classpath, instead of as JAR files
-     */
-    boolean exploded = true
-
     /**
      * Whether java.time.* package should be a default import package
      */
@@ -89,15 +91,25 @@ class GrailsExtension {
         return agent
     }
 
+    DependencyHandler getPlugins() {
+        if (pluginDefiner == null) {
+            pluginDefiner = new PluginDefiner(project)
+        }
+
+        pluginDefiner
+    }
+
     /**
      * Allows defining plugins in the available scopes
      */
-    void plugins(Closure pluginDefinitions) {
-        PluginDefiner definer = new PluginDefiner(project, exploded)
-        pluginDefinitions.delegate = definer
-        pluginDefinitions.resolveStrategy = Closure.DELEGATE_FIRST
-        pluginDefinitions.call()
+    void plugins(@DelegatesTo(DependencyHandler) Closure configureClosure) {
+        if (pluginDefiner == null) {
+            pluginDefiner = new PluginDefiner(project)
+        }
+        pluginDefiner.grailsRun = developmentRun
+        ConfigureUtil.configure(configureClosure, plugins)
     }
+
     /**
      * Configuration for the reloading agent
      */
@@ -115,4 +127,12 @@ class GrailsExtension {
         List<String> jvmArgs = ['-Xverify:none']
     }
 
+    boolean isDevelopmentRun() {
+        boolean devMode = Environment.developmentEnvironmentAvailable && 
Environment.developmentMode
+        if (!devMode) {
+            return false
+        }
+
+        project.gradle.startParameter.taskNames.any { String taskName -> 
taskName in ['bootRun', 'console'] } || 
project.hasProperty('force.grails.exploded')
+    }
 }
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 d8b1aa744b..2c9b75d7c8 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
@@ -23,6 +23,8 @@ import javax.inject.Inject
 import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
 
+import org.gradle.api.attributes.AttributeMatchingStrategy
+
 import io.spring.gradle.dependencymanagement.DependencyManagementPlugin
 import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
 import org.apache.tools.ant.filters.EscapeUnicode
@@ -69,6 +71,9 @@ import org.grails.build.parsing.CommandLineParser
 import org.grails.gradle.plugin.agent.AgentTasksEnhancer
 import org.grails.gradle.plugin.commands.ApplicationContextCommandTask
 import org.grails.gradle.plugin.commands.ApplicationContextScriptTask
+import org.grails.gradle.plugin.exploded.ExplodedCompatibilityRule
+import org.grails.gradle.plugin.exploded.ExplodedDisambiguationRule
+import org.grails.gradle.plugin.exploded.GrailsExplodedPlugin
 import org.grails.gradle.plugin.model.GrailsClasspathToolingModelBuilder
 import org.grails.gradle.plugin.run.FindMainClassTask
 import org.grails.gradle.plugin.util.SourceSets
@@ -155,6 +160,21 @@ class GrailsGradlePlugin extends GroovyPlugin {
         configureRunCommand(project)
 
         configureGroovyCompiler(project)
+
+        configureMatchingExplodedRules(project)
+    }
+
+    private void configureMatchingExplodedRules(Project project) {
+        /**
+         * the exploded plugin may or may not be configured for the given 
project, these rules ensure tasks that are considered "development"
+         * running tasks (like bootRun) will prefer the exploded variant of a 
plugin if it is available but still match the non-exploded variant if not.
+         */
+        project.dependencies.attributesSchema { schema ->
+            schema.attribute(GrailsExplodedPlugin.EXPLODED_ATTRIBUTE).with { 
AttributeMatchingStrategy details ->
+                details.compatibilityRules.add(ExplodedCompatibilityRule)
+                details.disambiguationRules.add(ExplodedDisambiguationRule)
+            }
+        }
     }
 
     private static Provider<String> getMainClassProvider(Project project) {
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsPluginGradlePlugin.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsPluginGradlePlugin.groovy
index 88f98a172b..7486c45d74 100644
--- 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsPluginGradlePlugin.groovy
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsPluginGradlePlugin.groovy
@@ -25,16 +25,10 @@ import groovy.transform.CompileStatic
 
 import org.gradle.api.Project
 import org.gradle.api.Task
-import org.gradle.api.artifacts.Configuration
-import org.gradle.api.artifacts.ConfigurationContainer
-import org.gradle.api.artifacts.PublishArtifact
 import org.gradle.api.file.DuplicatesStrategy
-import org.gradle.api.internal.tasks.DefaultTaskDependency
 import org.gradle.api.tasks.Copy
 import org.gradle.api.tasks.JavaExec
 import org.gradle.api.tasks.SourceSetContainer
-import org.gradle.api.tasks.TaskContainer
-import org.gradle.api.tasks.TaskDependency
 import org.gradle.api.tasks.bundling.Jar
 import org.gradle.api.tasks.compile.GroovyCompile
 import org.gradle.language.jvm.tasks.ProcessResources
@@ -42,7 +36,6 @@ import 
org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
 
 import org.springframework.boot.gradle.tasks.bundling.BootJar
 
-import grails.util.Environment
 import org.grails.gradle.plugin.run.FindMainClassTask
 import org.grails.gradle.plugin.util.SourceSets
 
@@ -55,6 +48,7 @@ import org.grails.gradle.plugin.util.SourceSets
  */
 @CompileStatic
 class GrailsPluginGradlePlugin extends GrailsGradlePlugin {
+    public static final String PLUGIN_ID = 
"org.apache.grails.gradle.grails-plugin"
 
     @Inject
     GrailsPluginGradlePlugin(ToolingModelBuilderRegistry registry) {
@@ -99,43 +93,12 @@ class GrailsPluginGradlePlugin extends GrailsGradlePlugin {
             }
             """ as String
         }
-
     }
 
     protected String getDefaultProfile() {
         'web-plugin'
     }
 
-    /**
-     * Configures an exploded configuration that can be used to build the 
classpath of the application from subprojects that are plugins without 
contructing a JAR file
-     *
-     * @param project The project instance
-     */
-    protected void configureExplodedDirConfiguration(Project project) {
-        ConfigurationContainer allConfigurations = project.configurations
-        allConfigurations.register('exploded').configure {
-            Configuration runtimeConfiguration = 
allConfigurations.named('runtimeClasspath').get()
-            it.extendsFrom(runtimeConfiguration)
-
-            if (Environment.isDevelopmentRun() && isExploded(project)) {
-                runtimeConfiguration.artifacts.clear()
-                // add the subproject classes as outputs
-                TaskContainer allTasks = project.tasks
-
-                GroovyCompile groovyCompile = allTasks.named('compileGroovy', 
GroovyCompile).get()
-                ProcessResources processResources = 
allTasks.named('processResources', ProcessResources).get()
-
-                runtimeConfiguration.artifacts.add(new 
ExplodedDir(groovyCompile.destinationDir, groovyCompile, processResources))
-                it.artifacts.add(new 
ExplodedDir(processResources.destinationDir, groovyCompile, processResources))
-            }
-        }
-    }
-
-    @CompileDynamic
-    private boolean isExploded(Project project) {
-        Boolean.valueOf(project.properties.getOrDefault('exploded', 
'false').toString())
-    }
-
     @Override
     protected Task createBuildPropertiesTask(Project project) {
         // no-op
@@ -310,28 +273,4 @@ class GrailsPluginGradlePlugin extends GrailsGradlePlugin {
             throw new RuntimeException('A plugin may define a plugin.yml or a 
plugin.groovy, but not both')
         }
     }
-
-    static class ExplodedDir implements PublishArtifact {
-        final String extension = ''
-        final String type = 'dir'
-        final Date date = new Date()
-
-        final File file
-        final TaskDependency buildDependencies
-
-        ExplodedDir(File file, Object... tasks) {
-            this.file = file
-            this.buildDependencies = new DefaultTaskDependency().add(tasks)
-        }
-
-        @Override
-        String getName() {
-            file.name
-        }
-
-        @Override
-        String getClassifier() {
-            ''
-        }
-    }
 }
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/PluginDefiner.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/PluginDefiner.groovy
index 6990f770cc..89b2911a8e 100644
--- 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/PluginDefiner.groovy
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/PluginDefiner.groovy
@@ -18,68 +18,50 @@
  */
 package org.grails.gradle.plugin.core
 
-import groovy.transform.CompileStatic
 import groovy.transform.PackageScope
 
 import org.gradle.api.Project
-import org.gradle.api.artifacts.Dependency
 import org.gradle.api.artifacts.ProjectDependency
+import org.gradle.api.artifacts.dsl.DependencyHandler
+import org.gradle.api.attributes.AttributeContainer
 
-import grails.util.BuildSettings
-import grails.util.Environment
+import org.grails.gradle.plugin.exploded.GrailsExplodedPlugin
 
 /**
- * Makes it easier to define Grails plugins and also makes them aware of the 
development environment so that they can be run inline without creating a JAR
- *
- * @author Graeme Rocher
- * @since 3.2
+ * Tracks Grails Plugins & handles prioritizing using the exploded variants 
for easier class reloading during development
  */
 @PackageScope
-class PluginDefiner {
+class PluginDefiner implements DependencyHandler, GroovyInterceptable {
+
+    @Delegate
+    DependencyHandler target  // IDE/types; not used for the actual calls due 
to interception
+
+    private final Project project
+    boolean grailsRun
 
-    final Project project
-    final exploded
+    List<ProjectDependency> dependencies = []
 
-    PluginDefiner(Project project, boolean exploded = true) {
+    PluginDefiner(Project project) {
         this.project = project
-        this.exploded = exploded
+        this.target = project.dependencies
     }
 
-    void methodMissing(String name, args) {
-        Object[] argArray = (Object[]) args
+    def invokeMethod(String name, Object objArgs) {
+        def argArray = (objArgs instanceof Object[]) ? objArgs : [objArgs] as 
Object[]
 
-        if (!argArray) {
-            throw new MissingMethodException(name, GrailsExtension, args)
-        }
-        else {
-            if (argArray[0] instanceof Map) {
-                Map notation = (Map) argArray[0]
-                if (!notation.containsKey('group')) {
-                    notation.put('group', 'org.grails.plugins')
-                }
-            }
-            else if (argArray[0] instanceof CharSequence) {
-                String str = argArray[0].toString()
+        def methodMethod = target.metaClass.getMetaMethod(name, argArray)
+        def result = (methodMethod ? methodMethod.invoke(target, argArray) : 
target.invokeMethod(name, argArray))
 
-                if (str.startsWith(':')) {
-                    argArray[0] = "org.grails.plugins$str".toString()
+        if (result instanceof ProjectDependency) {
+            ProjectDependency dependency = (ProjectDependency) result
+            if (grailsRun) {
+                dependency.attributes { AttributeContainer ac ->
+                    ac.attribute(GrailsExplodedPlugin.EXPLODED_ATTRIBUTE, true)
                 }
             }
-            else if (Environment.isDevelopmentRun() && (argArray[0] instanceof 
ProjectDependency)) {
-                ProjectDependency pd = argArray[0]
-                project.dependencies.add(name, project.files(new 
File(pd.dependencyProject.projectDir, BuildSettings.BUILD_RESOURCES_PATH)))
-            }
-            project.dependencies.add(name, *argArray)
+            dependencies << dependency
         }
-    }
 
-    @CompileStatic
-    Dependency project(String path) {
-        if (Environment.isDevelopmentRun()) {
-            project.dependencies.project(path: path, configuration: 'exploded')
-        }
-        else {
-            project.dependencies.project(path: path)
-        }
+        return result
     }
 }
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/ExplodedCompatibilityRule.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/ExplodedCompatibilityRule.groovy
new file mode 100644
index 0000000000..753d0d78bc
--- /dev/null
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/ExplodedCompatibilityRule.groovy
@@ -0,0 +1,20 @@
+package org.grails.gradle.plugin.exploded
+
+import org.gradle.api.attributes.AttributeCompatibilityRule
+import org.gradle.api.attributes.CompatibilityCheckDetails
+
+/**
+ * Compatible if:
+ * 1. attribute not defined
+ * 2. attribute matches desired value
+ */
+class ExplodedCompatibilityRule implements AttributeCompatibilityRule<Boolean> 
{
+    @Override
+    void execute(CompatibilityCheckDetails<Boolean> details) {
+        if (details.getConsumerValue() == null) {
+            details.compatible()
+        } else if (details.getProducerValue() == details.getConsumerValue()) {
+            details.compatible()
+        }
+    }
+}
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/ExplodedDisambiguationRule.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/ExplodedDisambiguationRule.groovy
new file mode 100644
index 0000000000..de9374bbb6
--- /dev/null
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/ExplodedDisambiguationRule.groovy
@@ -0,0 +1,18 @@
+package org.grails.gradle.plugin.exploded
+
+import org.gradle.api.attributes.AttributeDisambiguationRule
+import org.gradle.api.attributes.MultipleCandidatesDetails
+
+/**
+ * If true, then it's the closest match, otherwise not the closest. 
Prioritizes the exploded variant
+ */
+class ExplodedDisambiguationRule implements 
AttributeDisambiguationRule<Boolean> {
+    @Override
+    void execute(MultipleCandidatesDetails<Boolean> details) {
+        if (details.candidateValues.contains(Boolean.TRUE)) {
+            details.closestMatch(Boolean.TRUE)
+        } else {
+            details.closestMatch(Boolean.FALSE)
+        }
+    }
+}
diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/GrailsExplodedPlugin.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/GrailsExplodedPlugin.groovy
new file mode 100644
index 0000000000..66007f5a10
--- /dev/null
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/exploded/GrailsExplodedPlugin.groovy
@@ -0,0 +1,87 @@
+package org.grails.gradle.plugin.exploded
+
+import javax.inject.Inject
+
+import groovy.transform.CompileStatic
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.ConfigurablePublishArtifact
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.ConfigurationVariant
+import org.gradle.api.artifacts.type.ArtifactTypeDefinition
+import org.gradle.api.attributes.Attribute
+import org.gradle.api.attributes.AttributeContainer
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.tasks.compile.GroovyCompile
+import org.gradle.language.jvm.tasks.ProcessResources
+
+import org.grails.gradle.plugin.core.GrailsExtension
+import org.grails.gradle.plugin.core.GrailsPluginGradlePlugin
+
+/**
+ * Configures a Grails Plugin so that its classes and resources are exploded 
when running bootRun from a dependent Grails Application
+ */
+@CompileStatic
+class GrailsExplodedPlugin implements Plugin<Project> {
+    public static final String PLUGIN_ID = 
'org.apache.grails.gradle.grails-exploded'
+    public static final String EXPLODED_VARIANT_NAME = 'exploded'
+    public static final Attribute EXPLODED_ATTRIBUTE = 
Attribute.of('grails.plugin.exploded', Boolean)
+
+    private ObjectFactory objects
+
+    @Inject
+    GrailsExplodedPlugin(ObjectFactory objects) {
+        this.objects = objects
+    }
+
+    @Override
+    void apply(Project project) {
+        project.pluginManager.withPlugin(GrailsPluginGradlePlugin.PLUGIN_ID) {
+            project.logger.info('Project {} will be exploded for bootRuns', 
project.name)
+
+            GrailsExtension grails = 
project.extensions.findByType(GrailsExtension)
+            Objects.requireNonNull(grails, 'GrailsExtension should be applied 
by the `grails-plugin` Gradle plugin.')
+
+            if (grails.developmentRun) {
+                project.logger.lifecycle('bootRun detected - exploding plugin 
{}', project.name)
+                project.configurations.named('runtimeElements').configure { 
Configuration runtimeElements ->
+                    
runtimeElements.outgoing.variants.create(EXPLODED_VARIANT_NAME) { 
ConfigurationVariant v ->
+                        v.attributes { AttributeContainer ac ->
+                            // copy values from runtimeElements
+                            runtimeElements.attributes.keySet().each { 
Attribute<?> key ->
+                                Attribute<Object> typedKey = 
(Attribute<Object>) key
+                                Object value = 
runtimeElements.attributes.getAttribute(typedKey)
+                                if (value != null) {
+                                    ac.attribute(typedKey, value)
+                                }
+                            }
+
+                            // add our additional attribute
+                            ac.attribute(EXPLODED_ATTRIBUTE, true)
+
+                            def groovyCompileProvider = 
project.tasks.named('compileGroovy', GroovyCompile)
+                            def processResourcesProvider = 
project.tasks.named('processResources', ProcessResources)
+
+                            def groovyDir = 
groovyCompileProvider.get().destinationDirectory.get().asFile
+                            def resourcesDir = 
processResourcesProvider.get().destinationDir
+
+                            v.artifact(groovyDir) { 
ConfigurablePublishArtifact a ->
+                                a.type = ArtifactTypeDefinition.DIRECTORY_TYPE
+                                a.extension = ''
+                                a.classifier = ''
+                                a.builtBy(groovyCompileProvider, 
processResourcesProvider)
+                            }
+                            v.artifact(resourcesDir) { 
ConfigurablePublishArtifact a ->
+                                a.type = ArtifactTypeDefinition.DIRECTORY_TYPE
+                                a.extension = ''
+                                a.classifier = ''
+                                a.builtBy(groovyCompileProvider, 
processResourcesProvider)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}


Reply via email to