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


##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/commands/GrailsCliGradlePlugin.groovy:
##########
@@ -0,0 +1,435 @@
+/*
+ *  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.commands
+
+import java.util.jar.JarFile
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+
+import org.gradle.api.NamedDomainObjectProvider
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.ConfigurationContainer
+import org.gradle.api.artifacts.Dependency
+import org.gradle.api.artifacts.DependencySet
+import org.gradle.api.artifacts.component.ProjectComponentIdentifier
+import org.gradle.api.artifacts.result.ResolvedArtifactResult
+import org.gradle.api.file.FileCollection
+import org.gradle.api.tasks.JavaExec
+import org.gradle.api.tasks.SourceSet
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+
+import grails.util.Environment
+import grails.util.GrailsNameUtils
+import org.grails.gradle.plugin.core.GrailsExtension
+import org.grails.gradle.plugin.core.GrailsGradlePlugin
+import org.grails.gradle.plugin.util.ClasspathUtils
+import org.grails.gradle.plugin.util.SourceSets
+import org.grails.io.support.FactoriesLoaderSupport
+import org.grails.build.parsing.CommandLineParser
+
+/**
+ * Configures the CLI tier of a Grails application or plugin project: the 
{@code grailsCli}
+ * configurations (with automatic discovery of companion {@code -cli} 
artifacts advertised by the
+ * dependency graph), the per-command tasks, the generic {@code 
runCommand}/{@code runScript}
+ * tasks, and the interactive {@code console}/{@code shell} tasks. Applied 
automatically by the
+ * Grails Gradle plugin.
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class GrailsCliGradlePlugin implements Plugin<Project> {
+
+    public static final String APPLICATION_CONTEXT_COMMAND_CLASS = 
'org.apache.grails.core.cli.ApplicationCommand'
+
+    /**
+     * The dependency bucket carrying CLI-only dependencies (Grails commands 
and their libraries):
+     * compile-visible so `grails-app/commands` sources compile against the 
cli-only contract, on
+     * the command-runner classpath, but never on `runtimeClasspath`, 
`bootRun`, or packaged
+     * artifacts.
+     */
+    public static final String GRAILS_CLI_CONFIGURATION = 'grailsCli'
+
+    /** The resolvable view of {@link #GRAILS_CLI_CONFIGURATION} used by the 
command-runner tasks */
+    public static final String GRAILS_CLI_CLASSPATH_CONFIGURATION = 
'grailsCliClasspath'
+
+    /**
+     * Set this project property to {@code false} to stop the plugin from 
auto-provisioning the
+     * CLI tier onto {@link #GRAILS_CLI_CONFIGURATION}; equivalent to
+     * {@code grails { cliAutoProvision = false }}.
+     */
+    public static final String GRAILS_CLI_AUTO_PROVISION_PROPERTY = 
'grailsCliAutoProvision'
+
+    /** The internal probe configuration used to detect companion `-cli` 
modules */
+    public static final String GRAILS_CLI_DETECT_CONFIGURATION = 
'grailsCliDetect'
+
+    @Override
+    void apply(Project project) {
+        // self-sufficient when applied standalone: the auto-provisioning 
behavior is configured
+        // through the `grails` extension, normally registered by the Grails 
Gradle plugin
+        if (project.extensions.findByName('grails') == null) {
+            project.extensions.create('grails', GrailsExtension, project)
+        }
+
+        configureGrailsCliConfiguration(project)
+
+        configureConsoleTask(project)
+
+        configureApplicationCommands(project)
+
+        configureRunScript(project)
+
+        configureRunCommand(project)
+    }
+
+    /**
+     * Registers the `grailsCli` dependency bucket and its resolvable 
`grailsCliClasspath` view.
+     * `grailsCli` carries the CLI tier — command companion artifacts 
(`<artifactId>-cli`) and the
+     * libraries they need. It extends the compile classpaths (the same wiring 
`compileOnly` uses)
+     * so `grails-app/commands` sources compile against the cli-only contract, 
while staying off
+     * `runtimeClasspath` — and therefore out of `bootRun`, `bootJar`, and 
`bootWar`.
+     */
+    protected void configureGrailsCliConfiguration(Project project) {
+        ConfigurationContainer configurations = project.configurations
+        if (configurations.names.contains(GRAILS_CLI_CONFIGURATION)) {
+            return
+        }
+
+        Configuration grailsCli = 
configurations.create(GRAILS_CLI_CONFIGURATION)
+        grailsCli.canBeResolved = false
+        grailsCli.canBeConsumed = false
+        grailsCli.description = 'CLI-only dependencies (Grails commands and 
the libraries they need); compile-visible and on the command-runner classpath, 
but never on runtimeClasspath, bootRun, or packaged artifacts.'
+
+        // compile visibility for grails-app/commands sources plus the TEST 
classpaths (tests
+        // exercise commands inside the test JVM), while the main 
runtimeClasspath — and therefore
+        // bootRun, bootJar, and bootWar — never sees the cli tier; matching 
configurations that
+        // appear later (e.g. the integrationTest pair) are included as they 
are created
+        configurations.matching { Configuration it ->
+            it.name in ['compileClasspath', 'testCompileClasspath', 
'testRuntimeClasspath',
+                        'integrationTestCompileClasspath', 
'integrationTestRuntimeClasspath']
+        }.configureEach { Configuration it ->
+            it.extendsFrom(grailsCli)
+        }
+
+        Configuration grailsCliClasspath = 
configurations.create(GRAILS_CLI_CLASSPATH_CONFIGURATION)
+        grailsCliClasspath.extendsFrom(grailsCli)
+        grailsCliClasspath.canBeResolved = true
+        grailsCliClasspath.canBeConsumed = false
+        grailsCliClasspath.description = 'Resolvable view of grailsCli used by 
the command-runner tasks.'
+
+        Configuration grailsCliDetect = 
configurations.create(GRAILS_CLI_DETECT_CONFIGURATION)
+        grailsCliDetect.canBeResolved = true
+        grailsCliDetect.canBeConsumed = false
+        grailsCliDetect.visible = false
+        grailsCliDetect.description = 'Internal probe used to discover 
companion -cli artifacts advertised by dependencies.'
+        for (String bucket : ['api', 'implementation', 'runtimeOnly']) {
+            configurations.matching { Configuration it -> it.name == bucket 
}.configureEach { Configuration it ->
+                grailsCliDetect.extendsFrom(it)
+            }
+        }
+
+        // computed when the configuration is first resolved, so every 
dependency (and the
+        // extension configuration) declared by the build script is visible
+        grailsCli.withDependencies { DependencySet dependencies ->
+            autoProvisionCliDependencies(project, dependencies)
+        }
+    }
+
+    /**
+     * Auto-provisions the CLI tier onto {@code grailsCli}: the command 
contract and runner, plus
+     * every companion {@code -cli} artifact advertised by a dependency of the 
application. A
+     * module advertises its companion through the {@code Grails-Cli-Artifact} 
manifest attribute
+     * of its runtime jar (stamped by the framework's cli-artifact build 
convention; third-party
+     * plugins set it on their jar task). Discovery walks the full dependency 
graph (including
+     * transitive plugins) through a lenient resolution of an internal probe 
configuration.
+     * Disable with {@code grails { cliAutoProvision = false }}.
+     */
+    @CompileDynamic
+    protected void autoProvisionCliDependencies(Project project, DependencySet 
dependencies) {
+        GrailsExtension grails = project.extensions.getByType(GrailsExtension)
+        if (!grails.cliAutoProvision.get()) {
+            return
+        }
+
+        // command authoring and execution work out of the box: the command 
contract + the runner
+        
dependencies.add(project.dependencies.create('org.apache.grails:grails-core-cli'))
+        
dependencies.add(project.dependencies.create('org.apache.grails:grails-console'))
+
+        Configuration probe = 
project.configurations.getByName(GRAILS_CLI_DETECT_CONFIGURATION)
+        Set<String> companions = [] as Set
+        def lenientArtifacts = probe.incoming.artifactView { it.lenient(true) 
}.artifacts

Review Comment:
   The concern is real today — Grails doesn't yet support the configuration 
cache (our own build runs with `org.gradle.configuration-cache=false`), so this 
cost is currently paid on every invocation. But it's worth separating what 
Gradle makes fixable here from what it doesn't.
   
   **The hard constraint:** the per-command task names (`dbmUpdate`, ...) come 
from `META-INF/grails-cli.factories` inside resolved dependency jars, and 
Gradle requires task names to exist when the task graph is assembled. There is 
no API for lazily-materialized task *names* — a `Provider<Set<String>>` fed by 
an `ArtifactView` works for a task's inputs, but not for the existence of the 
tasks themselves. So "dynamically named tasks, visible in `gradle tasks`/IDE 
sync, with zero configuration-time resolution" is not a combination Gradle 
offers; any plugin registering tasks from dependency content has this shape.
   
   **What is achievable within the current design:**
   
   1. *Task rules* (`tasks.addRule`) would defer discovery until an unmatched 
task name is actually requested — `./gradlew help` and unrelated builds would 
pay nothing. The tradeoff is real, though: the commands would no longer be 
listed individually in `gradle tasks` or IDE task views, which is a big part of 
why per-command tasks exist. Could be offered behind a flag for 
build-speed-sensitive projects.
   2. *Configuration cache is the caching mechanism* — configuration-time 
resolution is supported under CC; the resolved result is serialized into the 
cache entry, so discovery would be paid once and replayed on every subsequent 
build. The genuine CC blockers in this code are incidental rather than 
structural (cross-project `findProject` access during discovery, `Project` 
captures in task closures) and are tractable cleanup. So the right long-term 
answer to this comment largely coincides with Grails gaining CC support.
   3. *Cheap scoping wins* meanwhile: cache the manifest/factories scan per 
resolved artifact set so jars aren't re-opened within an invocation, and the 
generic `runCommand` task already executes any command without discovery.
   
   **The deeper issue** is that all of this complexity exists because we 
decided to surface commands as Gradle *tasks*. A better solution may be to keep 
tracking the dependencies in `grailsCli` but resolve and use them in the Grails 
CLI itself — fully separating the build system from the CLI system. That's a 
redesign (and a partial revert to the older design), and we haven't agreed on 
how to restructure profile or forge, so it's out of scope for this PR. The 
significance of this problem is why I've suggested we use the hackathon to 
address the profile & forge design — moving forward with a single CLI design is 
critical long term, and this thread is a good illustration of why.
   
   Proposed follow-up for the tactical items: keep eager registration as the 
default (discoverability is the feature), do the CC-compatibility cleanup with 
a `--configuration-cache` functional test to lock it in, and consider the 
task-rule mode as an opt-in. Agreed this shouldn't block the PR — tracked in 
#16008.



-- 
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