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) + } + } + } + } + } + } + } +}
