jdaugherty commented on code in PR #16082:
URL: https://github.com/apache/grails-core/pull/16082#discussion_r3699569658


##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsPluginGradlePlugin.groovy:
##########
@@ -244,22 +248,71 @@ class GrailsPluginGradlePlugin extends GrailsGradlePlugin 
{
         }
     }
 
+    /**
+     * Packages plugin templates into the runtime jar and routes command 
scripts into either the
+     * runtime jar or the companion {@code -cli} jar.
+     *
+     * <p>When {@link GrailsCliArtifactGradlePlugin} is applied, {@code 
src/main/scripts} is copied
+     * into the cli source set as {@code META-INF/commands} so 
Groovy/YAML/JSON command resources
+     * ship only on {@code grailsCliClasspath} and stay out of {@code 
runtimeClasspath},
+     * {@code bootJar}, and {@code bootWar}. Without a companion, the 
historical behavior is
+     * preserved: scripts remain in the runtime plugin jar so unmigrated 
Grails 7 plugins and
+     * {@code legacyCommandSupport} consumers keep discovering them on the 
application classpath.
+     * Templates always stay on the runtime jar.</p>
+     *
+     * <p>{@code copyCommands} and {@code copyTemplates} are {@link Sync} 
tasks with unique output
+     * directories under the build dir (never writing into {@code 
processResources.destinationDir} as
+     * a side effect). The appropriate {@code process*Resources} task consumes 
that output via
+     * {@code from(...)}, so Gradle owns each path and can drop removed 
sources without a forced
+     * clean task wired into every build.</p>
+     */
     @CompileDynamic
     protected void configurePluginResources(Project project) {
         project.afterEvaluate() {
             ProcessResources processResources = (ProcessResources) 
project.tasks.getByName('processResources')
+            boolean hasCliCompanion = 
project.pluginManager.hasPlugin(GrailsCliArtifactGradlePlugin.PLUGIN_ID)
 
-            TaskProvider<Copy> copyCommands = 
project.tasks.register('copyCommands', Copy) {
-                from("${project.projectDir}/src/main/scripts")
-                into("${processResources.destinationDir}/META-INF/commands")
+            // Unique Sync outputs - never side-write into 
processResources.destinationDir.
+            TaskProvider<Sync> copyCommands = 
project.tasks.register('copyCommands', Sync) { Sync sync ->
+                sync.from("${project.projectDir}/src/main/scripts")
+                
sync.into(project.layout.buildDirectory.dir('tmp/grails-plugin-commands'))
             }
 
-            TaskProvider<Copy> copyTemplates = 
project.tasks.register('copyTemplates', Copy) {
-                from("${project.projectDir}/src/main/templates")
-                into("${processResources.destinationDir}/META-INF/templates")
+            TaskProvider<Sync> copyTemplates = 
project.tasks.register('copyTemplates', Sync) { Sync sync ->
+                sync.from("${project.projectDir}/src/main/templates")
+                
sync.into(project.layout.buildDirectory.dir('tmp/grails-plugin-templates'))
             }
+
             processResources.setDuplicatesStrategy(DuplicatesStrategy.INCLUDE)
-            processResources.dependsOn(copyCommands, copyTemplates)
+            processResources.from(copyTemplates) { into 'META-INF/templates' }
+
+            if (hasCliCompanion) {
+                SourceSet cliSourceSet = 
project.extensions.getByType(SourceSetContainer)
+                        
.getByName(GrailsCliArtifactGradlePlugin.CLI_SOURCE_SET_NAME)
+                ProcessResources commandResources = (ProcessResources) 
project.tasks
+                        .getByName(cliSourceSet.processResourcesTaskName)
+                
commandResources.setDuplicatesStrategy(DuplicatesStrategy.INCLUDE)

Review Comment:
   DuplicatesStrategy.INCLUDE is a strong smell here - it means we've not 
properly setup the input.  We should error by default as it's a gradle 
configuration problem



##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsPluginGradlePlugin.groovy:
##########
@@ -244,22 +248,71 @@ class GrailsPluginGradlePlugin extends GrailsGradlePlugin 
{
         }
     }
 
+    /**
+     * Packages plugin templates into the runtime jar and routes command 
scripts into either the
+     * runtime jar or the companion {@code -cli} jar.
+     *
+     * <p>When {@link GrailsCliArtifactGradlePlugin} is applied, {@code 
src/main/scripts} is copied
+     * into the cli source set as {@code META-INF/commands} so 
Groovy/YAML/JSON command resources
+     * ship only on {@code grailsCliClasspath} and stay out of {@code 
runtimeClasspath},
+     * {@code bootJar}, and {@code bootWar}. Without a companion, the 
historical behavior is
+     * preserved: scripts remain in the runtime plugin jar so unmigrated 
Grails 7 plugins and
+     * {@code legacyCommandSupport} consumers keep discovering them on the 
application classpath.
+     * Templates always stay on the runtime jar.</p>
+     *
+     * <p>{@code copyCommands} and {@code copyTemplates} are {@link Sync} 
tasks with unique output
+     * directories under the build dir (never writing into {@code 
processResources.destinationDir} as
+     * a side effect). The appropriate {@code process*Resources} task consumes 
that output via
+     * {@code from(...)}, so Gradle owns each path and can drop removed 
sources without a forced
+     * clean task wired into every build.</p>
+     */
     @CompileDynamic
     protected void configurePluginResources(Project project) {
         project.afterEvaluate() {
             ProcessResources processResources = (ProcessResources) 
project.tasks.getByName('processResources')
+            boolean hasCliCompanion = 
project.pluginManager.hasPlugin(GrailsCliArtifactGradlePlugin.PLUGIN_ID)
 
-            TaskProvider<Copy> copyCommands = 
project.tasks.register('copyCommands', Copy) {
-                from("${project.projectDir}/src/main/scripts")
-                into("${processResources.destinationDir}/META-INF/commands")
+            // Unique Sync outputs - never side-write into 
processResources.destinationDir.
+            TaskProvider<Sync> copyCommands = 
project.tasks.register('copyCommands', Sync) { Sync sync ->
+                sync.from("${project.projectDir}/src/main/scripts")
+                
sync.into(project.layout.buildDirectory.dir('tmp/grails-plugin-commands'))
             }
 
-            TaskProvider<Copy> copyTemplates = 
project.tasks.register('copyTemplates', Copy) {
-                from("${project.projectDir}/src/main/templates")
-                into("${processResources.destinationDir}/META-INF/templates")
+            TaskProvider<Sync> copyTemplates = 
project.tasks.register('copyTemplates', Sync) { Sync sync ->
+                sync.from("${project.projectDir}/src/main/templates")
+                
sync.into(project.layout.buildDirectory.dir('tmp/grails-plugin-templates'))
             }
+
             processResources.setDuplicatesStrategy(DuplicatesStrategy.INCLUDE)
-            processResources.dependsOn(copyCommands, copyTemplates)
+            processResources.from(copyTemplates) { into 'META-INF/templates' }
+
+            if (hasCliCompanion) {
+                SourceSet cliSourceSet = 
project.extensions.getByType(SourceSetContainer)
+                        
.getByName(GrailsCliArtifactGradlePlugin.CLI_SOURCE_SET_NAME)
+                ProcessResources commandResources = (ProcessResources) 
project.tasks
+                        .getByName(cliSourceSet.processResourcesTaskName)
+                
commandResources.setDuplicatesStrategy(DuplicatesStrategy.INCLUDE)
+                commandResources.from(copyCommands) { into 'META-INF/commands' 
}
+                // Prior builds may have left script copies under 
processResources.destinationDir
+                // (Copy does not purge foreign files). Filter the runtime jar 
so only hand-authored
+                // src/main/resources/META-INF/commands entries remain - no 
forced clean task.
+                File handAuthoredCommands = 
project.file('src/main/resources/META-INF/commands')
+                project.tasks.named('jar', Jar).configure { Jar jarTask ->
+                    jarTask.eachFile { FileCopyDetails details ->
+                        String path = details.path.replace('\\', '/')
+                        if (path.startsWith('META-INF/commands/') && path != 
'META-INF/commands/') {
+                            String relative = 
path.substring('META-INF/commands/'.length())
+                            if (!new File(handAuthoredCommands, 
relative).file) {
+                                details.exclude()
+                            }
+                        }
+                    }
+                }

Review Comment:
   The unique `Sync` output for `copyCommands` is the right change. This filter 
isn't.
   
   It runs for every entry in the runtime jar on every build and does a 
filesystem stat per `META-INF/commands` entry, and the only thing it 
accomplishes is hiding files that a *previous version* of this plugin left in 
`build/resources/main`. On a clean build there is nothing to hide — with a 
companion, `src/main/scripts` never reaches `build/resources/main` at all. I 
removed the filter (and the `seedStaleRuntimeCommands` fixture) and both 
`PluginScriptCommandPackagingSpec` cases still pass, 
`RUNTIME_HAS_HAND_AUTHORED=true` included: 
`src/main/resources/META-INF/commands/hand-authored.yml` ships in the runtime 
jar on its own because it is an ordinary main resource. So the filter's net 
effect is to exclude everything *else*.
   
   Two problems with that:
   
   1. It doesn't remove the stale state, it masks the archive. The leftovers 
stay in `build/resources/main`, which is `sourceSets.main.output`, so they 
remain on the plugin's own `test` / integration-test / `bootRun` classpath. I 
confirmed they survive the migration there.
   2. It silently drops legitimate entries. Any `META-INF/commands` file not 
backed by a real file under `src/main/resources/META-INF/commands` is excluded, 
including command resources a plugin generates with its own task and wires in 
through `processResources.from(...)`. No warning and no way to opt out.
   
   A `build/` directory holding output from an older version of the build logic 
is what `clean` is for. A packaging-time filter is a clean task in disguise, 
just paid on every build instead of once. Please drop it and note the one-time 
`clean` in `upgrading80x.adoc` next to the `grails-plugin-cli` instructions.



##########
grails-gradle/plugins/src/test/resources/test-projects/plugin-script-commands/plugin-with-cli/build.gradle:
##########
@@ -0,0 +1,67 @@
+// Plugin that publishes a companion -cli artifact. src/main/scripts must land 
in the companion
+// jar (META-INF/commands) and must not ship in the runtime plugin jar.
+plugins {
+    id 'org.apache.grails.gradle.grails-plugin'
+    id 'org.apache.grails.gradle.grails-plugin-cli'
+}
+
+group = 'org.example.test'
+
+grails {
+    bom = null
+    cliAutoProvision = false
+}
+
+cliArtifact {
+    defaultDependencies = false
+    automaticModuleName = 'org.example.test.plugin.with.cli'
+}
+
+// Seed leftover script copies under main resources (prior non-companion 
packaging). The runtime
+// jar filter must drop them without a forced clean task while keeping 
hand-authored commands.
+tasks.register('seedStaleRuntimeCommands') {
+    def staleDir = 
layout.buildDirectory.dir('resources/main/META-INF/commands')
+    doLast {
+        def dir = staleDir.get().asFile
+        dir.mkdirs()
+        new File(dir, 'example-script.groovy').text = "description('stale') 
{}\n"
+        new File(dir, 'removed-before-migration.groovy').text = 
"description('gone') {}\n"
+    }
+}
+
+tasks.named('jar', Jar).configure { jarTask ->
+    jarTask.dependsOn('seedStaleRuntimeCommands')
+}

Review Comment:
   `seedStaleRuntimeCommands` writes into `build/resources/main`, which is 
`processResources`' output directory — the exact side-write this PR removes 
from the plugin itself. The fixture manufactures the only condition under which 
the runtime jar filter does anything, so `RUNTIME_HAS_SCRIPT=false` and 
`RUNTIME_HAS_REMOVED=false` are asserting the workaround rather than the 
packaging behavior.
   
   Drop this block along with the filter. With both gone the remaining 
assertions still pass and cover what the change is actually for: scripts land 
only in the companion, and hand-authored `META-INF/commands` plus templates 
stay in the runtime jar.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to