This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch feat/8.0.x-legacy-command-compat
in repository https://gitbox.apache.org/repos/asf/grails-core.git
The following commit(s) were added to
refs/heads/feat/8.0.x-legacy-command-compat by this push:
new d1fe4956a6 Isolate Grails 7 command compat into execution-only
grails-core-cli-legacy
d1fe4956a6 is described below
commit d1fe4956a6d59ed6ac68a71bafe2020d813b11f6
Author: James Fredley <[email protected]>
AuthorDate: Sun Jul 19 11:44:35 2026 -0400
Isolate Grails 7 command compat into execution-only grails-core-cli-legacy
The backwards-compatibility layer that keeps unchanged Grails 7 command
plugins (grails.dev.commands.ApplicationCommand) working on Grails 8 was
restored inside grails-core-cli, which put the deprecated contract on the
compile ABI of every plugin that depends on the CLI tier. Move the
deprecated
types, the adapter, and the legacy loader into a new published subproject
grails-core-cli-legacy and provision it execution-only.
grails-core-cli now exposes a neutral SPI - ApplicationCommandProvider,
ApplicationCommandRegistrar, and ApplicationCommandTargetAware - and
ApplicationContextCommandRegistry discovers providers through
META-INF/grails-cli.factories (dual-classloader, class-identity
deduplicated,
failure isolated) with modern commands winning name collisions. The legacy
module ships LegacyApplicationCommandProvider, which performs the deprecated
dual-classloader load, adapter-wrap, and one-time deprecation warning.
grails-core-cli retains zero references to grails.dev.commands.
The Grails Gradle plugin adds a non-resolvable grailsCliLegacy bucket that
feeds only grailsCliClasspath and the test runtime classpaths - never any
compile classpath - and is auto-provisioned by default via cliAutoProvision,
so compatibility stays on by default with zero user action. A plugin that
still authors legacy commands compiles against grails-core-cli-legacy
explicitly.
Assisted-by: claude-code:claude-opus-4-8
---
gradle/publish-root-config.gradle | 1 +
.../GrailsApplicationContextCommandRunner.groovy | 12 +--
...railsApplicationContextCommandRunnerSpec.groovy | 6 +-
grails-core-cli-legacy/build.gradle | 67 +++++++++++++++++
.../grails/dev/commands/ApplicationCommand.groovy | 0
.../grails/dev/commands/ExecutionContext.groovy | 0
.../dev/commands/GrailsApplicationCommand.groovy | 0
.../dev/commands/io/FileSystemInteraction.groovy | 0
.../commands/io/FileSystemInteractionImpl.groovy | 0
.../dev/commands/template/TemplateException.groovy | 0
.../dev/commands/template/TemplateRenderer.groovy | 0
.../commands/template/TemplateRendererImpl.groovy | 0
.../compat/LegacyApplicationCommandAdapter.groovy | 6 +-
.../compat/LegacyApplicationCommandProvider.groovy | 69 +++++++++++++++++
.../main/resources/META-INF/grails-cli.factories | 16 ++++
.../cli/LegacyCommandRegistryLoadingSpec.groovy | 63 ++++++++++++++--
.../LegacyApplicationCommandAdapterSpec.groovy | 2 +-
...re.groovy => ApplicationCommandProvider.groovy} | 12 ++-
...e.groovy => ApplicationCommandRegistrar.groovy} | 9 ++-
...groovy => ApplicationCommandTargetAware.groovy} | 9 ++-
.../cli/ApplicationContextCommandRegistry.groovy | 55 ++++++--------
.../compiler/CommandFactoriesTransformation.groovy | 14 +++-
.../core/cli/ApplicationCommandProviderSpec.groovy | 86 ++++++++++++++++++++++
.../CommandFactoriesTransformationSpec.groovy | 16 ++++
grails-doc/src/en/guide/introduction/whatsNew.adoc | 9 ++-
.../src/en/guide/upgrading/upgrading80x.adoc | 18 ++++-
.../plugin/commands/GrailsCliGradlePlugin.groovy | 26 ++++++-
.../plugin/commands/CliAutoDiscoverySpec.groovy | 28 ++++++-
.../cli-companion-autodiscovery/app/build.gradle | 24 ++++++
.../legacy-command-plugin/build.gradle | 4 +-
.../legacy-commands-plugin/build.gradle | 4 +-
...egacyCommandCompatibilityIntegrationSpec.groovy | 10 +--
settings.gradle | 1 +
33 files changed, 485 insertions(+), 82 deletions(-)
diff --git a/gradle/publish-root-config.gradle
b/gradle/publish-root-config.gradle
index 17f2cede7b..78b1c0cf11 100644
--- a/gradle/publish-root-config.gradle
+++ b/gradle/publish-root-config.gradle
@@ -43,6 +43,7 @@ def publishedProjects = [
'grails-controllers',
'grails-converters',
'grails-core',
+ 'grails-core-cli-legacy',
'grails-databinding',
'grails-databinding-core',
'grails-datasource',
diff --git
a/grails-console/src/main/groovy/grails/ui/command/GrailsApplicationContextCommandRunner.groovy
b/grails-console/src/main/groovy/grails/ui/command/GrailsApplicationContextCommandRunner.groovy
index 92ebb447c7..5fc9801f18 100644
---
a/grails-console/src/main/groovy/grails/ui/command/GrailsApplicationContextCommandRunner.groovy
+++
b/grails-console/src/main/groovy/grails/ui/command/GrailsApplicationContextCommandRunner.groovy
@@ -24,7 +24,7 @@ import
org.springframework.context.ConfigurableApplicationContext
import grails.config.Settings
import org.apache.grails.core.cli.ApplicationContextCommandRegistry
import org.apache.grails.core.cli.ExecutionContext
-import org.apache.grails.core.cli.LegacyApplicationCommandAware
+import org.apache.grails.core.cli.ApplicationCommandTargetAware
import grails.ui.support.DevelopmentGrailsApplication
import org.grails.build.parsing.CommandLine
import org.grails.build.parsing.CommandLineParser
@@ -121,17 +121,17 @@ class GrailsApplicationContextCommandRunner extends
DevelopmentGrailsApplication
/**
* Resolves the object that Spring should autowire and inspect for the
{@code skipBootstrap}
* property. For a command loaded through the deprecated Grails 7
compatibility layer the
- * registry returns a {@link LegacyApplicationCommandAware} adapter; the
real legacy command it
- * wraps - not the adapter - is what must be autowired and queried,
because the adapter forwards
+ * registry returns an {@link ApplicationCommandTargetAware} adapter; the
target it wraps - not
+ * the adapter - is what must be autowired and queried, because the
adapter forwards
* {@code applicationContext} and {@code handle} to that same instance.
Any other command is
* returned unchanged.
*
* @param command the command resolved from the registry
- * @return the legacy target when {@code command} is a {@link
LegacyApplicationCommandAware}, otherwise {@code command}
+ * @return the adapter target when {@code command} is {@link
ApplicationCommandTargetAware}, otherwise {@code command}
*/
static Object resolveAutowireTarget(Object command) {
- command instanceof LegacyApplicationCommandAware ?
- ((LegacyApplicationCommandAware) command).legacyCommand : command
+ command instanceof ApplicationCommandTargetAware ?
+ ((ApplicationCommandTargetAware) command).target : command
}
/**
diff --git
a/grails-console/src/test/groovy/grails/ui/command/GrailsApplicationContextCommandRunnerSpec.groovy
b/grails-console/src/test/groovy/grails/ui/command/GrailsApplicationContextCommandRunnerSpec.groovy
index 7e9202f176..1d177e928b 100644
---
a/grails-console/src/test/groovy/grails/ui/command/GrailsApplicationContextCommandRunnerSpec.groovy
+++
b/grails-console/src/test/groovy/grails/ui/command/GrailsApplicationContextCommandRunnerSpec.groovy
@@ -16,7 +16,7 @@
*/
package grails.ui.command
-import org.apache.grails.core.cli.LegacyApplicationCommandAware
+import org.apache.grails.core.cli.ApplicationCommandTargetAware
import spock.lang.Specification
import spock.lang.Unroll
@@ -138,8 +138,8 @@ class GrailsApplicationContextCommandRunnerSpec extends
Specification {
def "resolveAutowireTarget unwraps a legacy command adapter"() {
given:
Object legacyCommand = new Object()
- LegacyApplicationCommandAware adapter = Stub() {
- getLegacyCommand() >> legacyCommand
+ ApplicationCommandTargetAware adapter = Stub() {
+ getTarget() >> legacyCommand
}
when:
diff --git a/grails-core-cli-legacy/build.gradle
b/grails-core-cli-legacy/build.gradle
new file mode 100644
index 0000000000..cdf1ace8e9
--- /dev/null
+++ b/grails-core-cli-legacy/build.gradle
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+plugins {
+ id 'groovy'
+ id 'java-library'
+ id 'project-report'
+ id 'org.apache.grails.buildsrc.properties'
+ id 'org.apache.grails.buildsrc.dependency-validator'
+ id 'org.apache.grails.buildsrc.compile'
+ id 'org.apache.grails.buildsrc.publish'
+ id 'org.apache.grails.buildsrc.sbom'
+ id 'org.apache.grails.buildsrc.vulnerability-scan'
+ id 'org.apache.grails.gradle.grails-code-style'
+ id 'org.apache.grails.gradle.grails-cli-library'
+ id 'org.apache.grails.gradle.grails-jacoco'
+}
+
+version = projectVersion
+group = 'org.apache.grails'
+
+dependencies {
+ implementation platform(project(':grails-bom'))
+
+ api project(':grails-core')
+ api(project(':grails-core')) {
+ capabilities {
+ requireCapability('org.apache.grails:grails-core-cli')
+ }
+ }
+ api 'org.apache.groovy:groovy'
+ implementation 'org.apache.groovy:groovy-templates'
+
+ testImplementation 'org.apache.groovy:groovy-test-junit5'
+ testImplementation 'org.junit.jupiter:junit-jupiter-api'
+ testImplementation 'org.junit.platform:junit-platform-suite'
+ testImplementation 'org.spockframework:spock-core'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
+ testImplementation 'ch.qos.logback:logback-classic'
+ testRuntimeOnly 'net.bytebuddy:byte-buddy'
+ testRuntimeOnly 'org.objenesis:objenesis'
+}
+
+tasks.named('jar', Jar).configure {
+ manifest.attributes('Automatic-Module-Name':
'org.apache.grails.core.cli.legacy')
+}
+
+apply {
+ from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle')
+ from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
+}
diff --git
a/grails-core/src/cli/groovy/grails/dev/commands/ApplicationCommand.groovy
b/grails-core-cli-legacy/src/main/groovy/grails/dev/commands/ApplicationCommand.groovy
similarity index 100%
rename from
grails-core/src/cli/groovy/grails/dev/commands/ApplicationCommand.groovy
rename to
grails-core-cli-legacy/src/main/groovy/grails/dev/commands/ApplicationCommand.groovy
diff --git
a/grails-core/src/cli/groovy/grails/dev/commands/ExecutionContext.groovy
b/grails-core-cli-legacy/src/main/groovy/grails/dev/commands/ExecutionContext.groovy
similarity index 100%
rename from
grails-core/src/cli/groovy/grails/dev/commands/ExecutionContext.groovy
rename to
grails-core-cli-legacy/src/main/groovy/grails/dev/commands/ExecutionContext.groovy
diff --git
a/grails-core/src/cli/groovy/grails/dev/commands/GrailsApplicationCommand.groovy
b/grails-core-cli-legacy/src/main/groovy/grails/dev/commands/GrailsApplicationCommand.groovy
similarity index 100%
rename from
grails-core/src/cli/groovy/grails/dev/commands/GrailsApplicationCommand.groovy
rename to
grails-core-cli-legacy/src/main/groovy/grails/dev/commands/GrailsApplicationCommand.groovy
diff --git
a/grails-core/src/cli/groovy/grails/dev/commands/io/FileSystemInteraction.groovy
b/grails-core-cli-legacy/src/main/groovy/grails/dev/commands/io/FileSystemInteraction.groovy
similarity index 100%
rename from
grails-core/src/cli/groovy/grails/dev/commands/io/FileSystemInteraction.groovy
rename to
grails-core-cli-legacy/src/main/groovy/grails/dev/commands/io/FileSystemInteraction.groovy
diff --git
a/grails-core/src/cli/groovy/grails/dev/commands/io/FileSystemInteractionImpl.groovy
b/grails-core-cli-legacy/src/main/groovy/grails/dev/commands/io/FileSystemInteractionImpl.groovy
similarity index 100%
rename from
grails-core/src/cli/groovy/grails/dev/commands/io/FileSystemInteractionImpl.groovy
rename to
grails-core-cli-legacy/src/main/groovy/grails/dev/commands/io/FileSystemInteractionImpl.groovy
diff --git
a/grails-core/src/cli/groovy/grails/dev/commands/template/TemplateException.groovy
b/grails-core-cli-legacy/src/main/groovy/grails/dev/commands/template/TemplateException.groovy
similarity index 100%
rename from
grails-core/src/cli/groovy/grails/dev/commands/template/TemplateException.groovy
rename to
grails-core-cli-legacy/src/main/groovy/grails/dev/commands/template/TemplateException.groovy
diff --git
a/grails-core/src/cli/groovy/grails/dev/commands/template/TemplateRenderer.groovy
b/grails-core-cli-legacy/src/main/groovy/grails/dev/commands/template/TemplateRenderer.groovy
similarity index 100%
rename from
grails-core/src/cli/groovy/grails/dev/commands/template/TemplateRenderer.groovy
rename to
grails-core-cli-legacy/src/main/groovy/grails/dev/commands/template/TemplateRenderer.groovy
diff --git
a/grails-core/src/cli/groovy/grails/dev/commands/template/TemplateRendererImpl.groovy
b/grails-core-cli-legacy/src/main/groovy/grails/dev/commands/template/TemplateRendererImpl.groovy
similarity index 100%
rename from
grails-core/src/cli/groovy/grails/dev/commands/template/TemplateRendererImpl.groovy
rename to
grails-core-cli-legacy/src/main/groovy/grails/dev/commands/template/TemplateRendererImpl.groovy
diff --git
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapter.groovy
b/grails-core-cli-legacy/src/main/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapter.groovy
similarity index 94%
rename from
grails-core/src/cli/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapter.groovy
rename to
grails-core-cli-legacy/src/main/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapter.groovy
index b572d46898..2480e0fd92 100644
---
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapter.groovy
+++
b/grails-core-cli-legacy/src/main/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapter.groovy
@@ -21,8 +21,8 @@ package org.apache.grails.core.cli.compat
import groovy.transform.CompileStatic
import org.apache.grails.core.cli.ApplicationCommand
+import org.apache.grails.core.cli.ApplicationCommandTargetAware
import org.apache.grails.core.cli.ExecutionContext
-import org.apache.grails.core.cli.LegacyApplicationCommandAware
import org.springframework.context.ConfigurableApplicationContext
/**
@@ -30,7 +30,7 @@ import
org.springframework.context.ConfigurableApplicationContext
*/
@SuppressWarnings('deprecation')
@CompileStatic
-class LegacyApplicationCommandAdapter implements ApplicationCommand,
LegacyApplicationCommandAware {
+class LegacyApplicationCommandAdapter implements ApplicationCommand,
ApplicationCommandTargetAware {
private final grails.dev.commands.ApplicationCommand legacyCommand
@@ -39,7 +39,7 @@ class LegacyApplicationCommandAdapter implements
ApplicationCommand, LegacyAppli
}
@Override
- Object getLegacyCommand() {
+ Object getTarget() {
legacyCommand
}
diff --git
a/grails-core-cli-legacy/src/main/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandProvider.groovy
b/grails-core-cli-legacy/src/main/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandProvider.groovy
new file mode 100644
index 0000000000..f9f5a14bb4
--- /dev/null
+++
b/grails-core-cli-legacy/src/main/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandProvider.groovy
@@ -0,0 +1,69 @@
+/*
+ * 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.apache.grails.core.cli.compat
+
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import grails.dev.commands.ApplicationCommand as LegacyApplicationCommand
+import org.apache.grails.core.cli.ApplicationCommand
+import org.apache.grails.core.cli.ApplicationCommandProvider
+import org.apache.grails.core.cli.ApplicationCommandRegistrar
+import org.grails.core.io.support.GrailsFactoriesLoader
+import org.grails.io.support.FactoriesLoaderSupport
+
+/**
+ * Loads commands implemented against the deprecated Grails 7 command contract.
+ */
+@Slf4j
+@CompileStatic
+class LegacyApplicationCommandProvider implements ApplicationCommandProvider {
+
+ private boolean warningLogged
+
+ @Override
+ @SuppressWarnings('deprecation')
+ void contributeCommands(
+ ClassLoader registryClassLoader,
+ ClassLoader contextClassLoader,
+ ApplicationCommandRegistrar registrar) {
+ Set<Class<? extends LegacyApplicationCommand>> legacyClasses = new
LinkedHashSet<>()
+ legacyClasses.addAll(GrailsFactoriesLoader.loadFactoryClasses(
+ LegacyApplicationCommand, registryClassLoader,
FactoriesLoaderSupport.FACTORIES_RESOURCE_LOCATION))
+ if (contextClassLoader != null && contextClassLoader !=
registryClassLoader) {
+ legacyClasses.addAll(GrailsFactoriesLoader.loadFactoryClasses(
+ LegacyApplicationCommand, contextClassLoader,
FactoriesLoaderSupport.FACTORIES_RESOURCE_LOCATION))
+ }
+
+ for (Class<? extends LegacyApplicationCommand> legacyClass :
legacyClasses) {
+ try {
+ LegacyApplicationCommand legacyCommand =
legacyClass.getDeclaredConstructor().newInstance()
+ ApplicationCommand command = new
LegacyApplicationCommandAdapter(legacyCommand)
+ if (registrar.register(command) && !warningLogged) {
+ log.warn("Command '{}' from a Grails 7 plugin was loaded
through the deprecated grails.dev.commands compatibility layer. Ask the plugin
author to migrate to the org.apache.grails.core.cli command API and publish a
-cli companion artifact; this compatibility path will be removed in a future
major release.", command.name)
+ warningLogged = true
+ }
+ }
+ catch (Throwable e) {
+ log.warn("Failed to load a Grails 7 legacy command from class
'{}' through the deprecated grails.dev.commands compatibility layer; skipping
it.",
+ legacyClass?.name, e)
+ }
+ }
+ }
+}
diff --git
a/grails-core-cli-legacy/src/main/resources/META-INF/grails-cli.factories
b/grails-core-cli-legacy/src/main/resources/META-INF/grails-cli.factories
new file mode 100644
index 0000000000..929762413d
--- /dev/null
+++ b/grails-core-cli-legacy/src/main/resources/META-INF/grails-cli.factories
@@ -0,0 +1,16 @@
+# 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
+#
+# http://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.
+
+org.apache.grails.core.cli.ApplicationCommandProvider=org.apache.grails.core.cli.compat.LegacyApplicationCommandProvider
diff --git
a/grails-core/src/test/groovy/org/apache/grails/core/cli/LegacyCommandRegistryLoadingSpec.groovy
b/grails-core-cli-legacy/src/test/groovy/org/apache/grails/core/cli/LegacyCommandRegistryLoadingSpec.groovy
similarity index 74%
rename from
grails-core/src/test/groovy/org/apache/grails/core/cli/LegacyCommandRegistryLoadingSpec.groovy
rename to
grails-core-cli-legacy/src/test/groovy/org/apache/grails/core/cli/LegacyCommandRegistryLoadingSpec.groovy
index 321633a65b..ca314e7a95 100644
---
a/grails-core/src/test/groovy/org/apache/grails/core/cli/LegacyCommandRegistryLoadingSpec.groovy
+++
b/grails-core-cli-legacy/src/test/groovy/org/apache/grails/core/cli/LegacyCommandRegistryLoadingSpec.groovy
@@ -18,9 +18,14 @@
*/
package org.apache.grails.core.cli
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.read.ListAppender
import grails.dev.commands.ExecutionContext as LegacyExecutionContext
import org.apache.grails.core.cli.compat.LegacyApplicationCommandAdapter
+import org.apache.grails.core.cli.compat.LegacyApplicationCommandProvider
import org.grails.build.parsing.CommandLine
+import org.slf4j.LoggerFactory
import org.springframework.context.ConfigurableApplicationContext
import spock.lang.Specification
import spock.lang.TempDir
@@ -32,10 +37,13 @@ class LegacyCommandRegistryLoadingSpec extends
Specification {
private ClassLoader originalContextClassLoader
private URLClassLoader factoryClassLoader
+ private Logger providerLogger
+ private ListAppender<ILoggingEvent> providerAppender
def cleanup() {
Thread.currentThread().contextClassLoader = originalContextClassLoader
factoryClassLoader?.close()
+ providerLogger?.detachAppender(providerAppender)
}
def "discovers adapts and executes a command registered under the legacy
factory key"() {
@@ -49,10 +57,10 @@ class LegacyCommandRegistryLoadingSpec extends
Specification {
then:
command instanceof LegacyApplicationCommandAdapter
- command instanceof LegacyApplicationCommandAware
+ command instanceof ApplicationCommandTargetAware
LegacyApplicationCommandAdapter adapter = command as
LegacyApplicationCommandAdapter
- LegacyRegistryTestCommand legacyCommand =
((LegacyApplicationCommandAware) command).legacyCommand as
LegacyRegistryTestCommand
- adapter.legacyCommand.is(legacyCommand)
+ LegacyRegistryTestCommand legacyCommand =
((ApplicationCommandTargetAware) command).target as LegacyRegistryTestCommand
+ adapter.target.is(legacyCommand)
when:
adapter.applicationContext = applicationContext
@@ -76,10 +84,10 @@ class LegacyCommandRegistryLoadingSpec extends
Specification {
then:
command instanceof LegacyApplicationCommandAdapter
- command instanceof LegacyApplicationCommandAware
+ command instanceof ApplicationCommandTargetAware
LegacyApplicationCommandAdapter adapter = command as
LegacyApplicationCommandAdapter
- LegacyRegistryGrailsApplicationTestCommand legacyCommand =
((LegacyApplicationCommandAware) command).legacyCommand as
LegacyRegistryGrailsApplicationTestCommand
- adapter.legacyCommand.is(legacyCommand)
+ LegacyRegistryGrailsApplicationTestCommand legacyCommand =
((ApplicationCommandTargetAware) command).target as
LegacyRegistryGrailsApplicationTestCommand
+ adapter.target.is(legacyCommand)
when:
boolean result = adapter.handle(new ExecutionContext(commandLine))
@@ -121,12 +129,53 @@ class LegacyCommandRegistryLoadingSpec extends
Specification {
and:
ApplicationCommand legacyCommand =
registry.findCommand('legacy-registry-command')
- legacyCommand instanceof LegacyApplicationCommandAware
+ legacyCommand instanceof ApplicationCommandTargetAware
and:
registry.findCommand('config-report') instanceof ConfigReportCommand
}
+ def "does not warn when every legacy command collides with a modern
command"() {
+ given:
+ attachProviderAppender()
+ useFactoryResources(
+
'grails.dev.commands.ApplicationCommand=org.apache.grails.core.cli.LegacyRegistryCollisionCommand',
+
'org.apache.grails.core.cli.ApplicationCommand=org.apache.grails.core.cli.ConfigReportCommand')
+
+ when:
+ new ApplicationContextCommandRegistry()
+
+ then:
+ deprecationWarnings.empty
+ }
+
+ def "warns once when multiple legacy commands are installed"() {
+ given:
+ attachProviderAppender()
+
useFactoryResources('grails.dev.commands.ApplicationCommand=org.apache.grails.core.cli.LegacyRegistryTestCommand,org.apache.grails.core.cli.LegacyRegistryGrailsApplicationTestCommand')
+
+ when:
+ ApplicationContextCommandRegistry registry = new
ApplicationContextCommandRegistry()
+
+ then:
+ registry.findCommand('legacy-registry-command') != null
+ registry.findCommand('legacy-registry-grails-application-command') !=
null
+ deprecationWarnings.size() == 1
+ }
+
+ private void attachProviderAppender() {
+ providerLogger =
LoggerFactory.getLogger(LegacyApplicationCommandProvider) as Logger
+ providerAppender = new ListAppender<>()
+ providerAppender.start()
+ providerLogger.addAppender(providerAppender)
+ }
+
+ private List<ILoggingEvent> getDeprecationWarnings() {
+ providerAppender.list.findAll { ILoggingEvent event ->
+ event.formattedMessage.contains('deprecated grails.dev.commands
compatibility layer')
+ }
+ }
+
private void useFactoryResources(String legacyFactories, String
cliFactories = null) {
File metaInf = new File(tempDir, 'META-INF')
assert metaInf.mkdirs()
diff --git
a/grails-core/src/test/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapterSpec.groovy
b/grails-core-cli-legacy/src/test/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapterSpec.groovy
similarity index 98%
rename from
grails-core/src/test/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapterSpec.groovy
rename to
grails-core-cli-legacy/src/test/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapterSpec.groovy
index 6627d1f8a6..8229762c6b 100644
---
a/grails-core/src/test/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapterSpec.groovy
+++
b/grails-core-cli-legacy/src/test/groovy/org/apache/grails/core/cli/compat/LegacyApplicationCommandAdapterSpec.groovy
@@ -42,7 +42,7 @@ class LegacyApplicationCommandAdapterSpec extends
Specification {
adapter.name == 'legacy-command'
adapter.description == 'Legacy command description'
adapter.applicationContext.is(applicationContext)
- adapter.legacyCommand.is(legacyCommand)
+ adapter.target.is(legacyCommand)
result
legacyCommand.applicationContext.is(applicationContext)
legacyCommand.executionContext instanceof LegacyExecutionContext
diff --git
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/LegacyApplicationCommandAware.groovy
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationCommandProvider.groovy
similarity index 74%
copy from
grails-core/src/cli/groovy/org/apache/grails/core/cli/LegacyApplicationCommandAware.groovy
copy to
grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationCommandProvider.groovy
index 87708ef6f3..e12e2f2a23 100644
---
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/LegacyApplicationCommandAware.groovy
+++
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationCommandProvider.groovy
@@ -19,10 +19,14 @@
package org.apache.grails.core.cli
/**
- * Implemented by adapters that wrap a legacy {@code
grails.dev.commands.ApplicationCommand}.
- * Lets a launcher unwrap the real command for Spring autowiring and property
inspection.
+ * Contributes application commands discovered outside the primary command
contract.
+ *
+ * @since 8.0
*/
-interface LegacyApplicationCommandAware {
+interface ApplicationCommandProvider {
- Object getLegacyCommand()
+ void contributeCommands(
+ ClassLoader registryClassLoader,
+ ClassLoader contextClassLoader,
+ ApplicationCommandRegistrar registrar)
}
diff --git
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/LegacyApplicationCommandAware.groovy
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationCommandRegistrar.groovy
similarity index 77%
copy from
grails-core/src/cli/groovy/org/apache/grails/core/cli/LegacyApplicationCommandAware.groovy
copy to
grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationCommandRegistrar.groovy
index 87708ef6f3..dd36889959 100644
---
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/LegacyApplicationCommandAware.groovy
+++
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationCommandRegistrar.groovy
@@ -19,10 +19,11 @@
package org.apache.grails.core.cli
/**
- * Implemented by adapters that wrap a legacy {@code
grails.dev.commands.ApplicationCommand}.
- * Lets a launcher unwrap the real command for Spring autowiring and property
inspection.
+ * Installs a contributed command unless its name is already registered.
+ *
+ * @since 8.0
*/
-interface LegacyApplicationCommandAware {
+interface ApplicationCommandRegistrar {
- Object getLegacyCommand()
+ boolean register(ApplicationCommand command)
}
diff --git
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/LegacyApplicationCommandAware.groovy
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationCommandTargetAware.groovy
similarity index 77%
rename from
grails-core/src/cli/groovy/org/apache/grails/core/cli/LegacyApplicationCommandAware.groovy
rename to
grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationCommandTargetAware.groovy
index 87708ef6f3..b9936d2ee9 100644
---
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/LegacyApplicationCommandAware.groovy
+++
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationCommandTargetAware.groovy
@@ -19,10 +19,11 @@
package org.apache.grails.core.cli
/**
- * Implemented by adapters that wrap a legacy {@code
grails.dev.commands.ApplicationCommand}.
- * Lets a launcher unwrap the real command for Spring autowiring and property
inspection.
+ * Implemented by command adapters whose underlying target should be autowired
and inspected.
+ *
+ * @since 8.0
*/
-interface LegacyApplicationCommandAware {
+interface ApplicationCommandTargetAware {
- Object getLegacyCommand()
+ Object getTarget()
}
diff --git
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationContextCommandRegistry.groovy
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationContextCommandRegistry.groovy
index 5007cb3e1d..93690c455d 100644
---
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationContextCommandRegistry.groovy
+++
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/ApplicationContextCommandRegistry.groovy
@@ -19,9 +19,7 @@ package org.apache.grails.core.cli
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
-import org.apache.grails.core.cli.compat.LegacyApplicationCommandAdapter
import org.grails.core.io.support.GrailsFactoriesLoader
-import org.grails.io.support.FactoriesLoaderSupport
/**
* A registry of {@link org.apache.grails.core.cli.ApplicationCommand}
instances
@@ -34,8 +32,6 @@ import org.grails.io.support.FactoriesLoaderSupport
class ApplicationContextCommandRegistry {
private final Map<String, ApplicationCommand> commands = [:]
- private boolean legacyCommandWarningLogged
-
ApplicationContextCommandRegistry() {
ClassLoader registryClassLoader =
ApplicationContextCommandRegistry.classLoader
ClassLoader contextClassLoader =
Thread.currentThread().contextClassLoader
@@ -51,7 +47,7 @@ class ApplicationContextCommandRegistry {
// also used to pull any commands that are loaded from the gradle
classpath. Only when it is a distinct
// classloader: repeating the scan for the same classloader would
re-instantiate every command (whose
// constructor may have side effects) just to discard it on the
name-collision check below.
- if (contextClassLoader != registryClassLoader) {
+ if (contextClassLoader != null && contextClassLoader !=
registryClassLoader) {
for (ApplicationCommand cmd :
GrailsFactoriesLoader.loadFactories(ApplicationCommand,
contextClassLoader,
GrailsFactoriesLoader.CLI_FACTORIES_RESOURCE_LOCATION)) {
if (!commands.containsKey(cmd.name)) {
@@ -60,44 +56,37 @@ class ApplicationContextCommandRegistry {
}
}
- loadLegacyCommands(registryClassLoader, contextClassLoader)
+ loadCommandProviders(registryClassLoader, contextClassLoader)
}
- @SuppressWarnings('deprecation')
- private void loadLegacyCommands(ClassLoader registryClassLoader,
ClassLoader contextClassLoader) {
- // Gather the legacy command classes from the registry classloader
and, when it is a distinct
- // classloader, the thread context classloader, de-duplicated by Class
identity before any are
- // instantiated. A child context classloader delegates to its parent,
so it also reports the
- // parent's grails.factories entries; de-duplicating by the resolved
Class avoids instantiating a
- // parent-visible legacy command twice (its constructor may have side
effects) only to discard the
- // duplicate on the name-collision check below.
- Set<Class<? extends grails.dev.commands.ApplicationCommand>>
legacyClasses = new LinkedHashSet<>()
- legacyClasses.addAll(GrailsFactoriesLoader.loadFactoryClasses(
- grails.dev.commands.ApplicationCommand, registryClassLoader,
FactoriesLoaderSupport.FACTORIES_RESOURCE_LOCATION))
+ private void loadCommandProviders(ClassLoader registryClassLoader,
ClassLoader contextClassLoader) {
+ Set<Class<? extends ApplicationCommandProvider>> providerClasses = new
LinkedHashSet<>()
+ providerClasses.addAll(GrailsFactoriesLoader.loadFactoryClasses(
+ ApplicationCommandProvider, registryClassLoader,
GrailsFactoriesLoader.CLI_FACTORIES_RESOURCE_LOCATION))
if (contextClassLoader != null && contextClassLoader !=
registryClassLoader) {
- legacyClasses.addAll(GrailsFactoriesLoader.loadFactoryClasses(
- grails.dev.commands.ApplicationCommand,
contextClassLoader, FactoriesLoaderSupport.FACTORIES_RESOURCE_LOCATION))
+ providerClasses.addAll(GrailsFactoriesLoader.loadFactoryClasses(
+ ApplicationCommandProvider, contextClassLoader,
GrailsFactoriesLoader.CLI_FACTORIES_RESOURCE_LOCATION))
}
- // Instantiate each in isolation: a single stale Grails 7 command
whose no-arg constructor (or
- // getName()) throws under Grails 8 must be skipped with a warning,
never abort the whole registry
- // and take valid legacy and new-contract commands down with it.
- for (Class<? extends grails.dev.commands.ApplicationCommand>
legacyClass : legacyClasses) {
- try {
- grails.dev.commands.ApplicationCommand legacyCommand =
legacyClass.getDeclaredConstructor().newInstance()
- ApplicationCommand command = new
LegacyApplicationCommandAdapter(legacyCommand)
+
+ ApplicationCommandRegistrar registrar = new
ApplicationCommandRegistrar() {
+ @Override
+ boolean register(ApplicationCommand command) {
String name = command.name
if (commands.containsKey(name)) {
- continue
+ return false
}
commands[name] = command
- if (!legacyCommandWarningLogged) {
- log.warn("Command '{}' from a Grails 7 plugin was loaded
through the deprecated grails.dev.commands compatibility layer. Ask the plugin
author to migrate to the org.apache.grails.core.cli command API and publish a
-cli companion artifact; this compatibility path will be removed in a future
major release.", name)
- legacyCommandWarningLogged = true
- }
+ true
+ }
+ }
+
+ for (Class<? extends ApplicationCommandProvider> providerClass :
providerClasses) {
+ try {
+ ApplicationCommandProvider provider =
providerClass.getDeclaredConstructor().newInstance()
+ provider.contributeCommands(registryClassLoader,
contextClassLoader, registrar)
}
catch (Throwable e) {
- log.warn("Failed to load a Grails 7 legacy command from class
'{}' through the deprecated grails.dev.commands compatibility layer; skipping
it.",
- legacyClass?.name, e)
+ log.warn("Failed to load application commands from provider
'{}'; skipping it.", providerClass?.name, e)
}
}
}
diff --git
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformation.groovy
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformation.groovy
index 913736d12e..34f1fae390 100644
---
a/grails-core/src/cli/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformation.groovy
+++
b/grails-core/src/cli/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformation.groovy
@@ -55,6 +55,8 @@ class CommandFactoriesTransformation implements
ASTTransformation, TransformWith
public static final ClassNode APPLICATION_COMMAND_CLASS =
ClassHelper.make('org.apache.grails.core.cli.ApplicationCommand')
+ public static final ClassNode APPLICATION_COMMAND_TARGET_AWARE_CLASS =
ClassHelper.make('org.apache.grails.core.cli.ApplicationCommandTargetAware')
+
/** The command registration file written into the compilation target
directory */
public static final String CLI_FACTORIES_LOCATION =
'META-INF/grails-cli.factories'
@@ -94,8 +96,16 @@ class CommandFactoriesTransformation implements
ASTTransformation, TransformWith
File compilationTargetDirectory =
GlobalGrailsClassInjectorTransformation.resolveCompilationTargetDirectory(source)
for (ClassNode classNode : classes) {
- FactoriesFileWriter.updateFactoriesWithType(classNode,
APPLICATION_COMMAND_CLASS,
- compilationTargetDirectory, CLI_FACTORIES_LOCATION,
SOURCE_CLI_FACTORIES_LOCATIONS)
+ if (shouldRegisterCommand(classNode)) {
+ FactoriesFileWriter.updateFactoriesWithType(classNode,
APPLICATION_COMMAND_CLASS,
+ compilationTargetDirectory, CLI_FACTORIES_LOCATION,
SOURCE_CLI_FACTORIES_LOCATIONS)
+ }
+ }
+ }
+
+ protected static boolean shouldRegisterCommand(ClassNode classNode) {
+ !classNode.allInterfaces.any { ClassNode interfaceNode ->
+ interfaceNode.name == APPLICATION_COMMAND_TARGET_AWARE_CLASS.name
}
}
diff --git
a/grails-core/src/test/groovy/org/apache/grails/core/cli/ApplicationCommandProviderSpec.groovy
b/grails-core/src/test/groovy/org/apache/grails/core/cli/ApplicationCommandProviderSpec.groovy
new file mode 100644
index 0000000000..d72122205a
--- /dev/null
+++
b/grails-core/src/test/groovy/org/apache/grails/core/cli/ApplicationCommandProviderSpec.groovy
@@ -0,0 +1,86 @@
+/*
+ * 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.apache.grails.core.cli
+
+import java.nio.file.Path
+
+import spock.lang.Specification
+import spock.lang.TempDir
+
+class ApplicationCommandProviderSpec extends Specification {
+
+ @TempDir
+ Path tempDir
+
+ private ClassLoader originalContextClassLoader
+ private URLClassLoader factoryClassLoader
+
+ def cleanup() {
+ Thread.currentThread().contextClassLoader = originalContextClassLoader
+ factoryClassLoader?.close()
+ }
+
+ def "continues loading providers when one provider constructor fails"() {
+ given:
+ File metaInf = new File(tempDir.toFile(), 'META-INF')
+ assert metaInf.mkdirs()
+ new File(metaInf, 'grails-cli.factories').text =
"${ApplicationCommandProvider.name}=${ThrowingCommandProvider.name},${TestCommandProvider.name}"
+ originalContextClassLoader = Thread.currentThread().contextClassLoader
+ factoryClassLoader = new URLClassLoader([tempDir.toUri().toURL()] as
URL[], getClass().classLoader)
+ Thread.currentThread().contextClassLoader = factoryClassLoader
+
+ when:
+ ApplicationCommand command = new
ApplicationContextCommandRegistry().findCommand('provider-test')
+
+ then:
+ command instanceof ProviderTestCommand
+ }
+}
+
+class ThrowingCommandProvider implements ApplicationCommandProvider {
+
+ ThrowingCommandProvider() {
+ throw new RuntimeException('boom')
+ }
+
+ @Override
+ void contributeCommands(ClassLoader registryClassLoader, ClassLoader
contextClassLoader, ApplicationCommandRegistrar registrar) {
+ }
+}
+
+class TestCommandProvider implements ApplicationCommandProvider {
+
+ @Override
+ void contributeCommands(ClassLoader registryClassLoader, ClassLoader
contextClassLoader, ApplicationCommandRegistrar registrar) {
+ registrar.register(new ProviderTestCommand())
+ }
+}
+
+class ProviderTestCommand implements ApplicationCommand {
+
+ @Override
+ String getName() {
+ 'provider-test'
+ }
+
+ @Override
+ boolean handle(ExecutionContext executionContext) {
+ true
+ }
+}
diff --git
a/grails-core/src/test/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformationSpec.groovy
b/grails-core/src/test/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformationSpec.groovy
index e051b67b5c..8c1e03dfe5 100644
---
a/grails-core/src/test/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformationSpec.groovy
+++
b/grails-core/src/test/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformationSpec.groovy
@@ -18,6 +18,9 @@
*/
package org.apache.grails.core.cli.compiler
+import org.codehaus.groovy.ast.ClassHelper
+
+import org.apache.grails.core.cli.ApplicationCommandTargetAware
import spock.lang.Specification
import spock.lang.Unroll
@@ -42,4 +45,17 @@ class CommandFactoriesTransformationSpec extends
Specification {
'/work/my-plugin/grails-app/commands/example/BackupCommand.groovy'
|| false
'/work/clipboard/src/climate/groovy/com/example/NotCliCommand.groovy'
|| false
}
+
+ def "does not register target-aware command adapters as command
factories"() {
+ expect:
+
!CommandFactoriesTransformation.shouldRegisterCommand(ClassHelper.make(TargetAwareTestAdapter))
+ }
+}
+
+class TargetAwareTestAdapter implements ApplicationCommandTargetAware {
+
+ @Override
+ Object getTarget() {
+ null
+ }
}
diff --git a/grails-doc/src/en/guide/introduction/whatsNew.adoc
b/grails-doc/src/en/guide/introduction/whatsNew.adoc
index c1a139dc63..be33cb8f93 100644
--- a/grails-doc/src/en/guide/introduction/whatsNew.adoc
+++ b/grails-doc/src/en/guide/introduction/whatsNew.adoc
@@ -118,6 +118,11 @@ dependencies {
The auto-provisioning can be disabled with `grails { cliAutoProvision = false
}`, in which case
`grailsCli` dependencies are declared explicitly.
+The plugin also provisions `grails-core-cli-legacy` into a separate
execution-only
+`grailsCliLegacy` bucket. That bucket contributes to `grailsCliClasspath`,
never to application or
+plugin compile classpaths. This keeps unchanged Grails 7 command plugins
working by default with
+zero user action without leaking `grails.dev.commands.*` into the Grails 8
command-authoring ABI.
+
Plugin authors get the whole publishing side from the new
`org.apache.grails.gradle.grails-plugin-cli` Gradle plugin: it creates the
`cli` source set as a
feature variant, wires the command contract onto its compile classpath,
advertises the companion
@@ -133,7 +138,9 @@ commands is unaffected, and a command-bearing plugin that
has not yet been rebui
keeps its runtime functionality. Its legacy
`grails.dev.commands.ApplicationCommand` registrations
also continue to work through a deprecated compatibility layer, so those
commands remain available
without a re-release; publishing a companion `-cli` artifact is recommended
when migrating to the
-Grails 8 command API. See the xref:upgrading#upgrading80x[upgrade guide] for
the migration steps and
+Grails 8 command API. New commands should use `org.apache.grails.core.cli.*`.
A plugin that still
+authors legacy commands must explicitly compile against
`grails-core-cli-legacy`; consumers of an
+already-published legacy plugin need no declaration. See the
xref:upgrading#upgrading80x[upgrade guide] for the migration steps and
the full upgrade-impact breakdown.
==== GSP Tag Library Improvements
diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
index 42283884f8..75553e7600 100644
--- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
+++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
@@ -1655,6 +1655,12 @@ The `console` and `shell` tasks also draw from the
provisioned cli tier, so the
`build.gradle` is no longer needed and can be removed (the `console`
configuration itself remains
for additional console-only dependencies).
+The deprecated Grails 7 command contract is isolated in
+`org.apache.grails:grails-core-cli-legacy`. While `cliAutoProvision` is
enabled, the plugin adds
+this artifact to the execution-only `grailsCliLegacy` bucket.
`grailsCliClasspath` includes that
+bucket, but application and plugin compile classpaths do not. Existing Grails
7 command plugins
+therefore keep working by default without exposing the deprecated API to new
command sources.
+
Disable the discovery with `grails { cliAutoProvision = false }` and declare
the cli tier
explicitly when full control over the command classpath is preferred:
@@ -1670,6 +1676,7 @@ dependencies {
grailsCli 'org.apache.grails:grails-core-cli'
grailsCli 'org.apache.grails:grails-console'
grailsCli 'org.apache.grails:grails-data-hibernate7-dbmigration-cli'
+ grailsCliLegacy 'org.apache.grails:grails-core-cli-legacy'
}
----
@@ -1699,10 +1706,12 @@ The renamed framework command packages are:
===== 35.1 Backwards Compatibility for Existing Command Plugins
-Grails 8 provides a deprecated compatibility layer for already-published
Grails 7 command
+Grails 8 provides a deprecated, execution-only compatibility layer for
already-published Grails 7 command
plugins. Application commands created with `create-command` that implement
`grails.dev.commands.ApplicationCommand` or
`grails.dev.commands.GrailsApplicationCommand` remain
-available without a plugin re-release. The deprecated `grails.dev.commands.*`
types are retained,
+available without a plugin re-release. The deprecated `grails.dev.commands.*`
types are retained in
+`org.apache.grails:grails-core-cli-legacy`, which is provisioned by default
only on the command
+runner classpath,
and registrations in `META-INF/grails.factories` under
`grails.dev.commands.ApplicationCommand` continue to be discovered at runtime.
@@ -1710,6 +1719,11 @@ Grails logs a one-time deprecation warning when it uses
this compatibility layer
to keep existing published plugins working while plugin authors migrate; it is
deprecated and will
be removed in a future major release.
+New and migrated commands should always compile against
`org.apache.grails.core.cli.*` from
+`grails-core-cli`. A plugin that intentionally continues to author commands
against the deprecated
+contract must declare `compileOnly 'org.apache.grails:grails-core-cli-legacy'`
explicitly; normal
+consumers still need no declaration or other action.
+
**What just works:**
* Existing published Grails 7 application-command plugins continue to work on
Grails 8 without a
diff --git
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/commands/GrailsCliGradlePlugin.groovy
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/commands/GrailsCliGradlePlugin.groovy
index 9e4264304a..ea9cd30552 100644
---
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/commands/GrailsCliGradlePlugin.groovy
+++
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/commands/GrailsCliGradlePlugin.groovy
@@ -77,6 +77,9 @@ class GrailsCliGradlePlugin implements Plugin<Project> {
*/
public static final String GRAILS_CLI_CONFIGURATION = 'grailsCli'
+ /** Execution-only dependencies used by the command compatibility layer */
+ public static final String GRAILS_CLI_LEGACY_CONFIGURATION =
'grailsCliLegacy'
+
/** The resolvable view of {@link #GRAILS_CLI_CONFIGURATION} used by the
command-runner tasks */
public static final String GRAILS_CLI_CLASSPATH_CONFIGURATION =
'grailsCliClasspath'
@@ -127,6 +130,17 @@ class GrailsCliGradlePlugin implements Plugin<Project> {
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.'
+ Configuration grailsCliLegacy =
configurations.create(GRAILS_CLI_LEGACY_CONFIGURATION)
+ grailsCliLegacy.canBeResolved = false
+ grailsCliLegacy.canBeConsumed = false
+ grailsCliLegacy.description = 'Execution-only compatibility
dependencies used by the command runner; never compile-visible.'
+
+ configurations.matching { Configuration it ->
+ it.name in ['testRuntimeClasspath',
'integrationTestRuntimeClasspath']
+ }.configureEach { Configuration it ->
+ it.extendsFrom(grailsCliLegacy)
+ }
+
// 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
@@ -139,7 +153,7 @@ class GrailsCliGradlePlugin implements Plugin<Project> {
}
Configuration grailsCliClasspath =
configurations.create(GRAILS_CLI_CLASSPATH_CONFIGURATION)
- grailsCliClasspath.extendsFrom(grailsCli)
+ grailsCliClasspath.extendsFrom(grailsCli, grailsCliLegacy)
grailsCliClasspath.canBeResolved = true
grailsCliClasspath.canBeConsumed = false
grailsCliClasspath.description = 'Resolvable view of grailsCli used by
the command-runner tasks.'
@@ -160,6 +174,9 @@ class GrailsCliGradlePlugin implements Plugin<Project> {
grailsCli.withDependencies { DependencySet dependencies ->
autoProvisionCliDependencies(project, dependencies)
}
+ grailsCliLegacy.withDependencies { DependencySet dependencies ->
+ autoProvisionLegacyCliDependencies(project, dependencies)
+ }
}
/**
@@ -213,6 +230,13 @@ class GrailsCliGradlePlugin implements Plugin<Project> {
}
}
+ protected void autoProvisionLegacyCliDependencies(Project project,
DependencySet dependencies) {
+ GrailsExtension grails = project.extensions.getByType(GrailsExtension)
+ if (grails.cliAutoProvision.get()) {
+
dependencies.add(project.dependencies.create('org.apache.grails:grails-core-cli-legacy'))
+ }
+ }
+
/**
* Returns the {@link Dependency} on the companion cli artifact advertised
by the given
* resolved artifact, or {@code null} if it advertises none.
diff --git
a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/commands/CliAutoDiscoverySpec.groovy
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/commands/CliAutoDiscoverySpec.groovy
index 0e93ba3e04..91640eec1c 100644
---
a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/commands/CliAutoDiscoverySpec.groovy
+++
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/commands/CliAutoDiscoverySpec.groovy
@@ -39,7 +39,7 @@ class CliAutoDiscoverySpec extends GradleSpecification {
setupTestResourceProject('cli-companion-autodiscovery')
when: 'the app resolves its grailsCli dependencies'
- def result = executeTask(':app:inspectGrailsCli')
+ def result = executeTask(':app:inspectGrailsCli',
[':app:inspectGrailsCliLegacy', ':app:inspectClasspathWiring'])
then: 'the companion is a project dependency requesting the plugin cli
capability'
result.output.contains('GRAILSCLI_DEP: project path=:my-plugin
capabilities=[org.example.test:my-plugin-cli]')
@@ -50,6 +50,17 @@ class CliAutoDiscoverySpec extends GradleSpecification {
and: 'the command contract and runner are auto-provisioned as well'
result.output.contains('GRAILSCLI_DEP: module
org.apache.grails:grails-core-cli')
result.output.contains('GRAILSCLI_DEP: module
org.apache.grails:grails-console')
+
+ and: 'the legacy bridge is execution-only'
+ result.output.contains('GRAILSCLILEGACY_DEP: module
org.apache.grails:grails-core-cli-legacy')
+ !result.output.contains('GRAILSCLI_DEP: module
org.apache.grails:grails-core-cli-legacy')
+ result.output.contains('CLASSPATH_EXTENDS_GRAILSCLILEGACY=true')
+ result.output.contains('COMPILE_EXTENDS_GRAILSCLILEGACY=false')
+ result.output.contains('COMPILE_HAS_GRAILSCLILEGACY=false')
+ result.output.contains('TEST_COMPILE_HAS_GRAILSCLILEGACY=false')
+
result.output.contains('INTEGRATION_TEST_COMPILE_HAS_GRAILSCLILEGACY=false')
+ result.output.contains('TEST_RUNTIME_HAS_GRAILSCLILEGACY=true')
+
result.output.contains('INTEGRATION_TEST_RUNTIME_HAS_GRAILSCLILEGACY=true')
}
def "a companion with a customized artifactId is discovered and resolves
through its advertised capability"() {
@@ -77,6 +88,13 @@ class CliAutoDiscoverySpec extends GradleSpecification {
then: 'grailsCli extends the compile classpath but not the runtime
classpath'
result.output.contains('COMPILE_EXTENDS_GRAILSCLI=true')
result.output.contains('RUNTIME_EXTENDS_GRAILSCLI=false')
+ result.output.contains('COMPILE_EXTENDS_GRAILSCLILEGACY=false')
+ result.output.contains('CLASSPATH_EXTENDS_GRAILSCLILEGACY=true')
+ result.output.contains('COMPILE_HAS_GRAILSCLILEGACY=false')
+ result.output.contains('TEST_COMPILE_HAS_GRAILSCLILEGACY=false')
+
result.output.contains('INTEGRATION_TEST_COMPILE_HAS_GRAILSCLILEGACY=false')
+ result.output.contains('TEST_RUNTIME_HAS_GRAILSCLILEGACY=true')
+
result.output.contains('INTEGRATION_TEST_RUNTIME_HAS_GRAILSCLILEGACY=true')
and: 'no companion (-cli) jar reaches the resolved runtime classpath'
!result.output.readLines().any { it.startsWith('RUNTIME_ARTIFACT:') &&
it.contains('-cli') }
@@ -87,9 +105,15 @@ class CliAutoDiscoverySpec extends GradleSpecification {
setupTestResourceProject('cli-companion-autodiscovery')
when:
- def result = executeTask(':app:inspectGrailsCli',
['-PgrailsCliAutoProvision=false'])
+ def result = executeTask(':app:inspectGrailsCli',
[':app:inspectGrailsCliLegacy', ':app:inspectClasspathWiring',
'-PgrailsCliAutoProvision=false'])
then: 'nothing is provisioned onto grailsCli — neither the companion
nor the contract/runner'
!result.output.contains('GRAILSCLI_DEP:')
+ !result.output.contains('GRAILSCLILEGACY_DEP:')
+ result.output.contains('COMPILE_HAS_GRAILSCLILEGACY=false')
+ result.output.contains('TEST_COMPILE_HAS_GRAILSCLILEGACY=false')
+
result.output.contains('INTEGRATION_TEST_COMPILE_HAS_GRAILSCLILEGACY=false')
+ result.output.contains('TEST_RUNTIME_HAS_GRAILSCLILEGACY=false')
+
result.output.contains('INTEGRATION_TEST_RUNTIME_HAS_GRAILSCLILEGACY=false')
}
}
diff --git
a/grails-gradle/plugins/src/test/resources/test-projects/cli-companion-autodiscovery/app/build.gradle
b/grails-gradle/plugins/src/test/resources/test-projects/cli-companion-autodiscovery/app/build.gradle
index 8294d50628..c07ff5e876 100644
---
a/grails-gradle/plugins/src/test/resources/test-projects/cli-companion-autodiscovery/app/build.gradle
+++
b/grails-gradle/plugins/src/test/resources/test-projects/cli-companion-autodiscovery/app/build.gradle
@@ -5,6 +5,10 @@ plugins {
id 'org.apache.grails.gradle.grails-cli'
}
+sourceSets {
+ integrationTest
+}
+
dependencies {
implementation project(':my-plugin')
implementation project(':renamed-plugin')
@@ -30,8 +34,28 @@ tasks.register('inspectGrailsCli') {
tasks.register('inspectClasspathWiring') {
doLast {
def extendsGrailsCli = { org.gradle.api.artifacts.Configuration c ->
'grailsCli' in (c.hierarchy*.name as Set) }
+ def extendsGrailsCliLegacy = { org.gradle.api.artifacts.Configuration
c -> 'grailsCliLegacy' in (c.hierarchy*.name as Set) }
+ def hasGrailsCliLegacy = { org.gradle.api.artifacts.Configuration c ->
+ c.allDependencies.any { it.group == 'org.apache.grails' && it.name
== 'grails-core-cli-legacy' }
+ }
println
"COMPILE_EXTENDS_GRAILSCLI=${extendsGrailsCli(configurations.compileClasspath)}"
+ println
"COMPILE_EXTENDS_GRAILSCLILEGACY=${extendsGrailsCliLegacy(configurations.compileClasspath)}"
println
"RUNTIME_EXTENDS_GRAILSCLI=${extendsGrailsCli(configurations.runtimeClasspath)}"
+ println
"CLASSPATH_EXTENDS_GRAILSCLILEGACY=${extendsGrailsCliLegacy(configurations.grailsCliClasspath)}"
+ println
"COMPILE_HAS_GRAILSCLILEGACY=${hasGrailsCliLegacy(configurations.compileClasspath)}"
+ println
"TEST_COMPILE_HAS_GRAILSCLILEGACY=${hasGrailsCliLegacy(configurations.testCompileClasspath)}"
+ println
"INTEGRATION_TEST_COMPILE_HAS_GRAILSCLILEGACY=${hasGrailsCliLegacy(configurations.integrationTestCompileClasspath)}"
+ println
"TEST_RUNTIME_HAS_GRAILSCLILEGACY=${hasGrailsCliLegacy(configurations.testRuntimeClasspath)}"
+ println
"INTEGRATION_TEST_RUNTIME_HAS_GRAILSCLILEGACY=${hasGrailsCliLegacy(configurations.integrationTestRuntimeClasspath)}"
+ }
+}
+
+tasks.register('inspectGrailsCliLegacy') {
+ def grailsCliLegacy = configurations.grailsCliLegacy
+ doLast {
+ grailsCliLegacy.dependencies.each { dep ->
+ println "GRAILSCLILEGACY_DEP: module ${dep.group}:${dep.name}"
+ }
}
}
diff --git
a/grails-gradle/plugins/src/test/resources/test-projects/legacy-command-discovery/legacy-command-plugin/build.gradle
b/grails-gradle/plugins/src/test/resources/test-projects/legacy-command-discovery/legacy-command-plugin/build.gradle
index a27cd2b250..65acb306d8 100644
---
a/grails-gradle/plugins/src/test/resources/test-projects/legacy-command-discovery/legacy-command-plugin/build.gradle
+++
b/grails-gradle/plugins/src/test/resources/test-projects/legacy-command-discovery/legacy-command-plugin/build.gradle
@@ -4,11 +4,11 @@ plugins {
configurations.configureEach {
resolutionStrategy.dependencySubstitution {
- substitute module('org.apache.grails:grails-core-cli') using
project(':legacy-command-api')
+ substitute module('org.apache.grails:grails-core-cli-legacy') using
project(':legacy-command-api')
}
}
dependencies {
implementation localGroovy()
- compileOnly 'org.apache.grails:grails-core-cli'
+ compileOnly 'org.apache.grails:grails-core-cli-legacy'
}
diff --git a/grails-test-examples/legacy-commands-plugin/build.gradle
b/grails-test-examples/legacy-commands-plugin/build.gradle
index 08b64595a0..d57c0a1d9b 100644
--- a/grails-test-examples/legacy-commands-plugin/build.gradle
+++ b/grails-test-examples/legacy-commands-plugin/build.gradle
@@ -31,12 +31,12 @@ group = 'legacy.commands.plugin'
dependencies {
implementation platform(project(':grails-bom'))
- // This fixture recompiles legacy command sources against Grails 8's
grails-core-cli to
+ // This fixture recompiles legacy command sources against Grails 8's
grails-core-cli-legacy to
// validate discovery, adapter, registry, and runner wiring end-to-end. It
does not
// re-validate a pre-compiled Grails 7 binary's Groovy-trait ABI. That
relies on Groovy's
// stable trait encoding across 4->5 and could be strengthened later with
a prebuilt Grails 7
// fixture jar.
- compileOnly 'org.apache.grails:grails-core-cli'
+ compileOnly 'org.apache.grails:grails-core-cli-legacy'
}
apply {
diff --git
a/grails-test-examples/legacy-commands/src/integration-test/groovy/legacycommands/LegacyCommandCompatibilityIntegrationSpec.groovy
b/grails-test-examples/legacy-commands/src/integration-test/groovy/legacycommands/LegacyCommandCompatibilityIntegrationSpec.groovy
index c3fd9efe13..1ca798fae5 100644
---
a/grails-test-examples/legacy-commands/src/integration-test/groovy/legacycommands/LegacyCommandCompatibilityIntegrationSpec.groovy
+++
b/grails-test-examples/legacy-commands/src/integration-test/groovy/legacycommands/LegacyCommandCompatibilityIntegrationSpec.groovy
@@ -26,7 +26,7 @@ import grails.testing.mixin.integration.Integration
import org.apache.grails.core.cli.ApplicationCommand
import org.apache.grails.core.cli.ApplicationContextCommandRegistry
import org.apache.grails.core.cli.ExecutionContext
-import org.apache.grails.core.cli.LegacyApplicationCommandAware
+import org.apache.grails.core.cli.ApplicationCommandTargetAware
import org.grails.build.parsing.CommandLine
import spock.lang.Specification
@@ -51,9 +51,9 @@ class LegacyCommandCompatibilityIntegrationSpec extends
Specification {
expect: 'both legacy factory registrations are discovered and adapted'
applicationCommand != null
- applicationCommand instanceof LegacyApplicationCommandAware
+ applicationCommand instanceof ApplicationCommandTargetAware
grailsApplicationCommand != null
- grailsApplicationCommand instanceof LegacyApplicationCommandAware
+ grailsApplicationCommand instanceof ApplicationCommandTargetAware
when: 'the legacy ApplicationCommand runs through its Grails 8 adapter'
applicationCommand.applicationContext = applicationContext
@@ -80,8 +80,8 @@ class LegacyCommandCompatibilityIntegrationSpec extends
Specification {
def "autowires the legacy command unwrapped from its Grails 8 adapter"() {
given: 'a legacy command adapter and its wrapped target'
ApplicationCommand applicationCommand =
ApplicationContextCommandRegistry.instance.findCommand('hello-legacy-app')
- LegacyApplicationCommandAware adapter =
(LegacyApplicationCommandAware) applicationCommand
- Object legacyCommand = adapter.legacyCommand
+ ApplicationCommandTargetAware adapter =
(ApplicationCommandTargetAware) applicationCommand
+ Object legacyCommand = adapter.target
ExecutionContext executionContext = new
ExecutionContext(Mock(CommandLine))
File applicationMarkerFile = new File(executionContext.baseDir,
'hello-legacy-app.txt')
applicationMarkerFile.delete()
diff --git a/settings.gradle b/settings.gradle
index 1b1889674b..2449ba89bf 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -115,6 +115,7 @@ include(
'grails-controllers',
'grails-converters',
'grails-core',
+ 'grails-core-cli-legacy',
'grails-databinding-core',
'grails-databinding',
'grails-datasource',