You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@grails.apache.org by "jdaugherty (via GitHub)" <gi...@apache.org> on 2026/07/09 19:53:03 UTC
[PR] Split CLI dependencies from the runtime classpath [grails-core]
jdaugherty opened a new pull request, #15948:
URL: https://github.com/apache/grails-core/pull/15948
## Description
Grails commands (`ApplicationCommand` implementations) have always shipped inside runtime plugin
artifacts, dragging CLI-only dependencies onto application runtime classpaths. This causes real
failures: `grails-shell-cli` leaks a conflicting Groovy onto the app classpath (see the
long-standing exclusion + TODO in `grails-data-hibernate{5,7}/dbmigration/build.gradle`), and a
CLI-only Spring Boot `WebApplicationInitializer` breaks classic WAR deployment with an NPE
(#15377) unless users hand-filter the `bootWar` classpath.
(no classifiers or capability syntax for consumers), fully consumable by Maven as well as Gradle.
### Framework changes
- **`grails-core-cli`**: the command contract moves from `grails.dev.commands.*` to
`org.apache.grails.core.cli.*`. Command registrations move from `META-INF/grails.factories` to a
dedicated `META-INF/grails-cli.factories`, written by a new `CommandFactoriesTransformation`
that ships in the cli artifact (so it is active exactly when a command can compile). The default
`grails-core` jar contains no command code and no command contract.
- **Companion artifacts** for all command-bearing modules, each with a unique package and
`Automatic-Module-Name`: dbmigration (h5/h7, 29 `dbm-*` commands each), the hibernate plugins
(`schema-export`), scaffolding (9 `generate-*` commands), web-url-mappings
(`url-mappings-report`), spring-security, and spring-security-oauth2. `grails-shell-cli` is gone
from every runtime dependency graph.
- **BOM**: the base BOM enumerates and manages every companion coordinate.
### Build/tooling changes
- **New `org.apache.grails.gradle.grails-cli` plugin** (applied automatically by the Grails
application/plugin plugins): registers the `grailsCli` configuration — compile-visible for
`grails-app/commands` sources and on the command-runner and test classpaths, but never on the
main `runtimeClasspath`, so commands and their CLI-only dependencies are excluded from
`bootRun`/`bootJar`/`bootWar`. It auto-provisions `grails-core-cli` + `grails-console`, and
**discovers plugin companions automatically**: a command-bearing plugin advertises its companion
through a `Grails-Cli-Artifact` manifest attribute on its runtime jar, and the plugin walks the
resolved dependency graph (including transitive plugins) and adds each advertised companion to
`grailsCli`. Per-command Gradle tasks (`dbmUpdate`, …) register from the discovered companions —
no `buildscript { classpath }` entry needed. Opt out with `grails { cliAutoProvision = false }`.
The `console`/`shell` tasks draw from the same provisioned tier, so generated apps no longer
declare `console "org.apache.grails:grails-console"`.
- **New `org.apache.grails.gradle.grails-plugin-cli` plugin** for plugin authors (dogfooded by the
framework's own modules): one `apply` sets up the cli source set as a feature variant, the
command contract on its compile classpath, the manifest advertisement, and the companion
publication.
- **grails-publish**: builds on the additional-publications support added in
apache/grails-gradle-publish#34 (requires `grails-publish` ≥ `1.0.0-SNAPSHOT`; the version is
bumped in `dependencies.gradle`).
- **Forge/profiles**: generated apps declare only runtime plugins; all CLI wiring is automatic.
### Consumer impact
Most applications upgrade with **no build changes**: declaring a command-bearing plugin is enough,
and its commands (and Gradle tasks) follow automatically. Application commands in
`grails-app/commands` only need the import rename (`grails.dev.commands.*` →
- [x] This PR contains a **single, focused change**.
- [x] This PR targets the **correct branch** for the type of change (`8.0.x` — breaking changes are part of the major release).
### Code Quality
- [x] I have **added or updated tests** that cover the changes introduced in this PR (module unit
suites, dbmigration integration tests, the config-report end-to-end integration test
exercising the full discovery → factories → runner chain, forge feature specs, and
grails-gradle plugin tests).
- [x] I have verified that all existing tests pass by running `./gradlew build --rerun-tasks`.
- [x] My code follows the project's **code style** guidelines (`aggregateViolations` reports zero
Checkstyle/CodeNarc/PMD/SpotBugs findings).
- [x] This PR does **not** include mass reformatting, style-only changes, or large-scale refactoring
beyond the approved scope.
- [x] If generative AI tooling was used in preparing this contribution, a quality model was used to ensure contributions are **consistent with the project's quality standards**.
### Licensing and Attribution
- [x] All contributed code is provided under the Apache License 2.0, and new source files include the **Apache license header**.
- [x] I have the necessary rights to submit this contribution.
- [x] If generative AI tooling was used in preparing this contribution, I have followed the ASF policy on generative tooling and have properly attributed its use.
### Documentation
- [x] User-facing changes are documented (Creating Custom Commands, Providing Basic Artefacts,
Gradle plugins catalog, `create-command` reference).
- [x] The **What's New** section covers the companion `-cli` artifacts, automatic discovery, and the new plugins.
- [x] The **Upgrade Notes** (section 33) cover the package renames, the `grails-cli.factories`
clean break, the `grailsCli` configuration, and the third-party plugin migration path.
- [x] The PR description clearly explains **what** was changed and **why**.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3608643049
##########
grails-core/src/main/groovy/org/grails/compiler/injection/FactoriesFileWriter.groovy:
##########
@@ -0,0 +1,136 @@
+/*
+ * 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.compiler.injection
+
+import java.lang.reflect.Modifier
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.ClassNode
+
+import org.springframework.core.CollectionFactory
+
+import org.apache.grails.gradle.common.PropertyFileUtils
+
+/**
+ * Writes factory registrations for compiled classes into a factories file in the compilation
+ * target directory, merging any existing entries from previous compilation runs and from
+ * hand-authored source registrations. The factories file location is supplied by the caller,
+ * so the writer is shared by transformations targeting different registration files
+ * (e.g. {@code META-INF/grails.factories} and {@code META-INF/grails-cli.factories}).
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class FactoriesFileWriter {
+
+ /**
+ * Registers the class as an implementation of the given super type in the factories file
+ * when the class is a non-abstract subtype.
+ *
+ * @param classNode the compiled class
+ * @param superType the factory type to register the class under
+ * @param compilationTargetDirectory the compilation output directory
+ * @param factoriesLocation the factories file path relative to the target directory
+ * @param sourceFactoriesLocations project-relative paths of hand-authored factories files to merge
+ * @return {@code true} when the class was a subtype of the factory type
+ */
+ static boolean updateFactoriesWithType(ClassNode classNode, ClassNode superType, File compilationTargetDirectory,
+ String factoriesLocation, List<String> sourceFactoriesLocations) {
+ if (GrailsASTUtils.isSubclassOfOrImplementsInterface(classNode, superType)) {
+ if (Modifier.isAbstract(classNode.getModifiers())) {
+ return false
+ }
+
+ def classNodeName = classNode.name
+ // Use SortedProperties to ensure a consistent order of entries for reproducible builds
+ def props = CollectionFactory.createSortedProperties(false)
+ def superTypeName = superType.getName()
+
+ File factoriesFile = new File(compilationTargetDirectory, factoriesLocation)
+ if (!factoriesFile.parentFile.exists()) {
+ factoriesFile.parentFile.mkdirs()
+ }
+ loadFromFile(props, factoriesFile)
+
+ File sourceDirectory = findSourceDirectory(compilationTargetDirectory)
+ if (sourceDirectory != null) {
+ for (String sourceFactoriesLocation : sourceFactoriesLocations) {
+ File sourceFactoriesFile = new File(sourceDirectory, sourceFactoriesLocation)
+ loadFromFile(props, sourceFactoriesFile)
+ }
+ }
+
+ addToProps(props, superTypeName, classNodeName)
+
+ factoriesFile.withWriter { Writer writer ->
Review Comment:
Gradle already handles "exclusive" locks because the project doesn't parallel compile these files - they're part of a single project and the build system is what handles these locks. We would not want to add exclusivitiy here - it's the role of the build system instead.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5007589115
## ✅ All tests passed ✅
🏷️ Commit: 42ef1ac1c6b36f1be722ded1ee7a2550c0c75b58
▶️ Tests: 19581 executed
⚪️ Checks: 61/61 completed
---
_Learn more about TestLens at [testlens.app](https://testlens.app)._
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3605761290
##########
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
+ for (ResolvedArtifactResult artifact : lenientArtifacts.artifacts) {
+ String companion = findAdvertisedCliArtifact(project, artifact)
+ if (companion) {
+ companions.add(companion)
+ }
+ }
+
+ for (String companion : companions) {
+ List<String> coordinate = companion.tokenize(':')
+ if (coordinate.size() != 2) {
+ project.logger.warn('Ignoring malformed Grails-Cli-Artifact value [{}] found in the dependencies of project {}', companion, project.name)
+ continue
+ }
+ boolean alreadyDeclared = dependencies.any { Dependency existing ->
+ existing.group == coordinate[0] && existing.name == coordinate[1]
+ }
+ if (!alreadyDeclared) {
+ project.logger.info('Detected cli companion artifact {}, adding it to the {} configuration of project {}',
+ companion, GRAILS_CLI_CONFIGURATION, project.name)
+ dependencies.add(project.dependencies.create(companion))
+ }
+ }
+ }
+
+ /**
+ * Returns the companion cli coordinate ({@code group:artifactId}) advertised by the given
+ * resolved artifact, or {@code null}. For artifacts produced by a project of the same build
+ * the (possibly not yet built) jar is not read — the coordinate comes from the project's
+ * {@code cliArtifactId} property exported by the cli-artifact convention.
+ */
+ @CompileDynamic
+ protected String findAdvertisedCliArtifact(Project project, ResolvedArtifactResult artifact) {
+ def componentIdentifier = artifact.id.componentIdentifier
+ if (componentIdentifier instanceof ProjectComponentIdentifier) {
+ Project target = project.rootProject.findProject(((ProjectComponentIdentifier) componentIdentifier).projectPath)
+ def cliArtifactId = target?.findProperty('cliArtifactId')
+ return cliArtifactId ? "${target.group}:${cliArtifactId}" as String : null
Review Comment:
Fixed. findAdvertisedCliArtifact now binds a companion advertised by a project of the current build as a project dependency requesting its cli feature capability, instead of a bare group:artifactId module coordinate. The module-coordinate path is kept for external modules and included-build components (distinguished by BuildIdentifier.buildPath). Verified end-to-end: the companion resolves to the sibling project's cli variant with no repository lookup.
##########
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
+ for (ResolvedArtifactResult artifact : lenientArtifacts.artifacts) {
+ String companion = findAdvertisedCliArtifact(project, artifact)
+ if (companion) {
+ companions.add(companion)
+ }
+ }
+
+ for (String companion : companions) {
+ List<String> coordinate = companion.tokenize(':')
+ if (coordinate.size() != 2) {
+ project.logger.warn('Ignoring malformed Grails-Cli-Artifact value [{}] found in the dependencies of project {}', companion, project.name)
+ continue
+ }
+ boolean alreadyDeclared = dependencies.any { Dependency existing ->
+ existing.group == coordinate[0] && existing.name == coordinate[1]
+ }
+ if (!alreadyDeclared) {
+ project.logger.info('Detected cli companion artifact {}, adding it to the {} configuration of project {}',
+ companion, GRAILS_CLI_CONFIGURATION, project.name)
+ dependencies.add(project.dependencies.create(companion))
+ }
+ }
+ }
+
+ /**
+ * Returns the companion cli coordinate ({@code group:artifactId}) advertised by the given
+ * resolved artifact, or {@code null}. For artifacts produced by a project of the same build
+ * the (possibly not yet built) jar is not read — the coordinate comes from the project's
+ * {@code cliArtifactId} property exported by the cli-artifact convention.
+ */
+ @CompileDynamic
+ protected String findAdvertisedCliArtifact(Project project, ResolvedArtifactResult artifact) {
+ def componentIdentifier = artifact.id.componentIdentifier
+ if (componentIdentifier instanceof ProjectComponentIdentifier) {
+ Project target = project.rootProject.findProject(((ProjectComponentIdentifier) componentIdentifier).projectPath)
+ def cliArtifactId = target?.findProperty('cliArtifactId')
+ return cliArtifactId ? "${target.group}:${cliArtifactId}" as String : null
+ }
+
+ File file = artifact.file
+ if (file == null || !file.isFile() || !file.name.endsWith('.jar')) {
+ return null
+ }
+ try (JarFile jarFile = new JarFile(file)) {
+ return jarFile.manifest?.mainAttributes?.getValue('Grails-Cli-Artifact')
+ }
+ catch (IOException ignored) {
+ return null
+ }
+ }
+
+ @CompileDynamic
+ protected void configureApplicationCommands(Project project) {
+ def applicationContextCommands = FactoriesLoaderSupport.loadFactoryNames(APPLICATION_CONTEXT_COMMAND_CLASS,
+ FactoriesLoaderSupport.classLoader, FactoriesLoaderSupport.CLI_FACTORIES_RESOURCE_LOCATION)
+ project.afterEvaluate {
+ FileCollection fileCollection = ClasspathUtils.buildClasspath(project, project.configurations.runtimeClasspath, project.configurations.grailsCliClasspath)
+ // commands come from the buildscript classloader (legacy placement) and from the
+ // project's cli tier, so auto-provisioned command artifacts register their tasks
+ // without any buildscript classpath entry
+ Set<String> commandClassNames = new LinkedHashSet<String>()
+ if (applicationContextCommands) {
+ commandClassNames.addAll(applicationContextCommands as List)
+ }
+ GrailsExtension grails = project.extensions.getByType(GrailsExtension)
+ if (grails.cliAutoProvision.get()) {
+ commandClassNames.addAll(loadCommandNamesFromCliClasspath(project))
+ }
+ for (ctxCommand in commandClassNames) {
+ String taskName = GrailsNameUtils.getLogicalPropertyName(ctxCommand, 'Command')
+ String commandName = GrailsNameUtils.getScriptName(GrailsNameUtils.getLogicalName(ctxCommand, 'Command'))
+ if (!project.tasks.names.contains(taskName)) {
+ project.tasks.register(taskName, ApplicationContextCommandTask).configure {
+ it.classpath = fileCollection
+ it.command = commandName
+ it.systemProperty(Environment.KEY, System.getProperty(Environment.KEY, Environment.DEVELOPMENT.getName()))
+ List<Object> args = []
+ def otherArgs = project.findProperty('args')
+ if (otherArgs) {
+ args.addAll(CommandLineParser.translateCommandline(otherArgs as String))
+ }
+
+ def appClassProvider = GrailsGradlePlugin.getMainClassProvider(project)
+
+ it.doFirst {
+ args << appClassProvider.get()
+ it.args(args)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads command class names from the {@code META-INF/grails-cli.factories} files of the
+ * resolved {@code grailsCliClasspath} jars, so per-command Gradle tasks (e.g. {@code dbmUpdate})
+ * are registered for auto-provisioned cli artifacts. Resolution is lenient and jars that are
+ * not built yet (project dependencies within a composite) are skipped — the generic
+ * {@code runCommand} task can always execute those commands regardless.
+ */
+ @CompileDynamic
+ protected Collection<String> loadCommandNamesFromCliClasspath(Project project) {
+ Set<String> names = new LinkedHashSet<String>()
+ Configuration cliClasspath = project.configurations.findByName(GRAILS_CLI_CLASSPATH_CONFIGURATION)
+ if (cliClasspath == null) {
+ return names
+ }
+ for (File file : cliClasspath.incoming.artifactView { it.lenient(true) }.files) {
Review Comment:
Agreed the caching/scoping is a good future improvement. For the silent-drop concern I've added a warning log over the discovery probe's lenient resolution failures, so a companion (and its per-command tasks) dropped by an unresolvable dependency is surfaced rather than lost quietly.
##########
grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/commands/CliCompanionPublishingSpec.groovy:
##########
@@ -0,0 +1,83 @@
+/*
+ * 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 groovy.json.JsonSlurper
+
+import org.grails.gradle.plugin.core.GradleSpecification
+
+/**
+ * Verifies that cli-tier dependencies declared with a capability
+ * ({@code project(':owner') { capabilities { requireCapability('g:owner-cli') } }}) are published
+ * as plain dependencies on the companion coordinate ({@code owner-cli}) in both the pom and the
+ * Gradle Module Metadata, and that the published artifacts resolve from a Maven repository.
+ *
+ * A published capability request cannot be resolved against the published component tree: the
+ * owner's cli variant and its {@code available-at} redirect target both provide the capability,
+ * so consumers fail with a capability self-conflict (the grails-forge generated-app failure).
+ */
+class CliCompanionPublishingSpec extends GradleSpecification {
Review Comment:
Added CliAutoDiscoverySpec and a cli-companion-autodiscovery test project: an in-build command-bearing plugin plus a consuming app with cliAutoProvision at its default. It asserts the companion is wired as a project dependency on its cli capability (the bug this pins), that grailsCli never leaks onto runtimeClasspath, and that cliAutoProvision = false opts out entirely.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3608692179
##########
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
+ for (ResolvedArtifactResult artifact : lenientArtifacts.artifacts) {
+ String companion = findAdvertisedCliArtifact(project, artifact)
+ if (companion) {
+ companions.add(companion)
+ }
+ }
+
+ for (String companion : companions) {
+ List<String> coordinate = companion.tokenize(':')
+ if (coordinate.size() != 2) {
+ project.logger.warn('Ignoring malformed Grails-Cli-Artifact value [{}] found in the dependencies of project {}', companion, project.name)
+ continue
+ }
+ boolean alreadyDeclared = dependencies.any { Dependency existing ->
+ existing.group == coordinate[0] && existing.name == coordinate[1]
+ }
+ if (!alreadyDeclared) {
+ project.logger.info('Detected cli companion artifact {}, adding it to the {} configuration of project {}',
+ companion, GRAILS_CLI_CONFIGURATION, project.name)
+ dependencies.add(project.dependencies.create(companion))
+ }
+ }
+ }
+
+ /**
+ * Returns the companion cli coordinate ({@code group:artifactId}) advertised by the given
+ * resolved artifact, or {@code null}. For artifacts produced by a project of the same build
+ * the (possibly not yet built) jar is not read — the coordinate comes from the project's
+ * {@code cliArtifactId} property exported by the cli-artifact convention.
+ */
+ @CompileDynamic
+ protected String findAdvertisedCliArtifact(Project project, ResolvedArtifactResult artifact) {
+ def componentIdentifier = artifact.id.componentIdentifier
+ if (componentIdentifier instanceof ProjectComponentIdentifier) {
+ Project target = project.rootProject.findProject(((ProjectComponentIdentifier) componentIdentifier).projectPath)
+ def cliArtifactId = target?.findProperty('cliArtifactId')
Review Comment:
Largely addressed across the last three commits: discovery now reads the `cliArtifact` extension directly (it exists with its convention from plugin-apply time, so the afterEvaluate ordering dependency is gone), matches the producing project by build tree path, and falls back to the jar-manifest read otherwise. The residual window is a producer that is both unevaluated and unbuilt when `grailsCli` is first observed — that's the fully-lazy discovery redesign, tracked with the related configuration-time work 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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3608663437
##########
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:
I'm going to resolve this since this issue already exists with the existing implementation - this isn't new to this change. The ticket was created to address that issue.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5012333260
## ✅ All tests passed ✅
🏷️ Commit: d6d49e85f38a99d53c06f9f611b4478d4c197d29
▶️ Tests: 36415 executed
⚪️ Checks: 61/61 completed
---
_Learn more about TestLens at [testlens.app](https://testlens.app)._
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5005967158
> I am for this and the PR looks fine.
>
> But I would want to know the clear path for end apps to quickly upgrade to Grails 8, which they will need to do, when we GA 8.0.0, due to Spring Boot EOL. Will they have to wait on common plugins to re-release and will that bottleneck upgrading to Grails 8.
I suspect the breaking part of this PR is only the command part. So from an upgrade perspective, that is likely the only blocker. With that said, we've already made changes to 8.x that will likely break plugins (due to spring boot4 changes). Based on the above, I'm assuming if the core plugin code continues to function, then that's probably good enough to merge.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5006538486
I was testing grails-web-console with this code line and seeing what the breakage would be since it doesn't have commands. I can confirm that the plugin should work, but I did find a specific issue on the web console plugin. We removed the get/getAt methods on Metadata as part of the early 8.0 deprecation work. This means the following breaks your app now, unless you fix it:
<img width="901" height="57" alt="image" src="https://github.com/user-attachments/assets/176e8eb0-4dcd-467c-9fae-0d3a95b9effc" />
Older grails apps would have generated using this syntax. To @jamesfredley original point, this change is only breaking for the command portion. plugins continue to work with this implementation otherwise.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3608689909
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/commands/GrailsCliArtifactGradlePlugin.groovy:
##########
@@ -0,0 +1,216 @@
+/*
+ * 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 groovy.transform.CompileStatic
+
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.PublishArtifact
+import org.gradle.api.component.AdhocComponentWithVariants
+import org.gradle.api.component.ConfigurationVariantDetails
+import org.gradle.api.component.SoftwareComponentFactory
+import org.gradle.api.tasks.SourceSet
+import org.gradle.api.tasks.SourceSetContainer
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.api.plugins.JavaPluginExtension
+
+import javax.inject.Inject
+
+import org.apache.grails.gradle.publish.AdditionalPublication
+import org.apache.grails.gradle.publish.GrailsPublishExtension
+
+/**
+ * Configures a Grails plugin project to ship its CLI commands as a companion artifact under its
+ * own Maven coordinate ({@code <artifactId>-cli}), keeping command code and CLI-only dependencies
+ * off the runtime classpath of consuming applications.
+ *
+ * Applying the plugin:
+ * <ul>
+ * <li>creates a {@code cli} source set ({@code src/cli/groovy}) exposed as a feature variant;</li>
+ * <li>adds {@code org.apache.grails:grails-core-cli} (the command contract) and the plugin's own
+ * runtime classes to the cli compile classpath, so commands compile with no additional
+ * configuration and are registered automatically in {@code META-INF/grails-cli.factories};</li>
+ * <li>keeps the cli variants out of the plugin's default publication and exposes them through a
+ * dedicated {@code cli} software component;</li>
+ * <li>stamps the runtime jar with the {@code Grails-Cli-Artifact} manifest attribute, so the
+ * Grails Gradle plugin adds the companion to a consuming application's {@code grailsCli}
+ * configuration automatically;</li>
+ * <li>registers the companion publication with the Grails publish plugin
+ * ({@code org.apache.grails.gradle.grails-publish}) when it is applied.</li>
+ * </ul>
+ *
+ * Configure through the {@code cliArtifact} extension:
+ * <pre>
+ * cliArtifact {
+ * automaticModuleName = 'com.example.myplugin.cli'
+ * }
+ * </pre>
+ *
+ * @since 8.0
+ */
+@CompileStatic
+abstract class GrailsCliArtifactGradlePlugin implements Plugin<Project> {
+
+ public static final String CLI_SOURCE_SET_NAME = 'cli'
+ public static final String CLI_COMPONENT_NAME = 'cli'
+ public static final String CLI_PUBLICATION_NAME = 'cli'
+
+ /** The runtime-jar manifest attribute advertising the companion cli coordinate */
+ public static final String CLI_ARTIFACT_MANIFEST_ATTRIBUTE = 'Grails-Cli-Artifact'
+
+ @Inject
+ abstract SoftwareComponentFactory getSoftwareComponentFactory()
+
+ @Override
+ void apply(Project project) {
+ if (!project.pluginManager.hasPlugin('java')) {
+ throw new GradleException("The Grails cli-artifact plugin requires the `java` plugin (or a plugin that applies it, e.g. the Grails plugin plugin) to be applied to project `${project.name}` first.")
+ }
+
+ CliArtifactExtension extension = project.extensions.create('cliArtifact', CliArtifactExtension)
+ extension.artifactId.convention(project.provider { "${project.name}-cli" as String })
+ extension.defaultDependencies.convention(true)
+
+ SourceSetContainer sourceSets = project.extensions.getByType(SourceSetContainer)
+ SourceSet cliSourceSet = sourceSets.create(CLI_SOURCE_SET_NAME)
+
+ JavaPluginExtension java = project.extensions.getByType(JavaPluginExtension)
+ java.registerFeature(CLI_SOURCE_SET_NAME) { spec ->
+ // no explicit capability: the default (`${group}:${project.name}-cli:${version}`)
Review Comment:
Fixed. When `cliArtifact { artifactId }` is customized, the cli variants now additionally carry the advertised capability (`$group:$artifactId`) alongside the Gradle default, so an in-build requireCapability against the advertised coordinate resolves. The published-metadata rewrite also recognizes customized companion coordinates now (previously only the `<module>-cli` naming convention), so the capability request is still replaced with the plain companion coordinate in the GMM. Covered end-to-end in CliAutoDiscoverySpec (in-build discovery + resolution) and CliCompanionPublishingSpec (publish + external resolve) with a customized acme-tools companion.
##########
grails-core/src/test/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformationSpec.groovy:
##########
@@ -0,0 +1,45 @@
+/*
+ * 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.compiler
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+/**
+ * Commands shipped in a companion cli artifact compile from the {@code cli} source set
+ * ({@code src/cli/groovy}), which is not a standard project-source location — the transformation
+ * must still register them in {@code META-INF/grails-cli.factories}.
+ */
+class CommandFactoriesTransformationSpec extends Specification {
Review Comment:
Also added the indirect-implementor case: a command implementing the contract through a trait (the GrailsApplicationCommand pattern) is registered correctly.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jamesfredley (via GitHub)" <gi...@apache.org>.
jamesfredley commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3605219263
##########
grails-core/src/main/groovy/org/grails/compiler/injection/FactoriesFileWriter.groovy:
##########
@@ -0,0 +1,136 @@
+/*
+ * 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.compiler.injection
+
+import java.lang.reflect.Modifier
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.ClassNode
+
+import org.springframework.core.CollectionFactory
+
+import org.apache.grails.gradle.common.PropertyFileUtils
+
+/**
+ * Writes factory registrations for compiled classes into a factories file in the compilation
+ * target directory, merging any existing entries from previous compilation runs and from
+ * hand-authored source registrations. The factories file location is supplied by the caller,
+ * so the writer is shared by transformations targeting different registration files
+ * (e.g. {@code META-INF/grails.factories} and {@code META-INF/grails-cli.factories}).
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class FactoriesFileWriter {
+
+ /**
+ * Registers the class as an implementation of the given super type in the factories file
+ * when the class is a non-abstract subtype.
+ *
+ * @param classNode the compiled class
+ * @param superType the factory type to register the class under
+ * @param compilationTargetDirectory the compilation output directory
+ * @param factoriesLocation the factories file path relative to the target directory
+ * @param sourceFactoriesLocations project-relative paths of hand-authored factories files to merge
+ * @return {@code true} when the class was a subtype of the factory type
+ */
+ static boolean updateFactoriesWithType(ClassNode classNode, ClassNode superType, File compilationTargetDirectory,
+ String factoriesLocation, List<String> sourceFactoriesLocations) {
+ if (GrailsASTUtils.isSubclassOfOrImplementsInterface(classNode, superType)) {
+ if (Modifier.isAbstract(classNode.getModifiers())) {
+ return false
+ }
+
+ def classNodeName = classNode.name
+ // Use SortedProperties to ensure a consistent order of entries for reproducible builds
+ def props = CollectionFactory.createSortedProperties(false)
+ def superTypeName = superType.getName()
+
+ File factoriesFile = new File(compilationTargetDirectory, factoriesLocation)
+ if (!factoriesFile.parentFile.exists()) {
+ factoriesFile.parentFile.mkdirs()
+ }
+ loadFromFile(props, factoriesFile)
Review Comment:
**Incremental compilation never removes stale entries.** This always loads the previously generated file and merges into it, but nothing ever prunes. When a command class is deleted or renamed, its old entry survives in `grails-cli.factories`, so an incremental build produces a *different* (superset) result than a clean build. At runtime that ghost entry becomes a `ClassNotFoundException` / failed instantiation for a command that no longer exists.
Fix: regenerate the machine-written registrations from the current compilation's class set each run, merging in only the hand-authored `sourceFactoriesLocations` entries - don't treat the prior generated output as an input to merge.
##########
grails-core/src/main/groovy/org/grails/compiler/injection/FactoriesFileWriter.groovy:
##########
@@ -0,0 +1,136 @@
+/*
+ * 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.compiler.injection
+
+import java.lang.reflect.Modifier
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.ClassNode
+
+import org.springframework.core.CollectionFactory
+
+import org.apache.grails.gradle.common.PropertyFileUtils
+
+/**
+ * Writes factory registrations for compiled classes into a factories file in the compilation
+ * target directory, merging any existing entries from previous compilation runs and from
+ * hand-authored source registrations. The factories file location is supplied by the caller,
+ * so the writer is shared by transformations targeting different registration files
+ * (e.g. {@code META-INF/grails.factories} and {@code META-INF/grails-cli.factories}).
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class FactoriesFileWriter {
+
+ /**
+ * Registers the class as an implementation of the given super type in the factories file
+ * when the class is a non-abstract subtype.
+ *
+ * @param classNode the compiled class
+ * @param superType the factory type to register the class under
+ * @param compilationTargetDirectory the compilation output directory
+ * @param factoriesLocation the factories file path relative to the target directory
+ * @param sourceFactoriesLocations project-relative paths of hand-authored factories files to merge
+ * @return {@code true} when the class was a subtype of the factory type
+ */
+ static boolean updateFactoriesWithType(ClassNode classNode, ClassNode superType, File compilationTargetDirectory,
+ String factoriesLocation, List<String> sourceFactoriesLocations) {
+ if (GrailsASTUtils.isSubclassOfOrImplementsInterface(classNode, superType)) {
+ if (Modifier.isAbstract(classNode.getModifiers())) {
+ return false
+ }
+
+ def classNodeName = classNode.name
+ // Use SortedProperties to ensure a consistent order of entries for reproducible builds
+ def props = CollectionFactory.createSortedProperties(false)
+ def superTypeName = superType.getName()
+
+ File factoriesFile = new File(compilationTargetDirectory, factoriesLocation)
+ if (!factoriesFile.parentFile.exists()) {
+ factoriesFile.parentFile.mkdirs()
+ }
+ loadFromFile(props, factoriesFile)
+
+ File sourceDirectory = findSourceDirectory(compilationTargetDirectory)
+ if (sourceDirectory != null) {
+ for (String sourceFactoriesLocation : sourceFactoriesLocations) {
+ File sourceFactoriesFile = new File(sourceDirectory, sourceFactoriesLocation)
+ loadFromFile(props, sourceFactoriesFile)
+ }
+ }
+
+ addToProps(props, superTypeName, classNodeName)
+
+ factoriesFile.withWriter { Writer writer ->
+ props.store(writer, 'Grails Factories File')
+ }
+
+ PropertyFileUtils.makePropertiesFileReproducible(factoriesFile)
+
+ return true
+ }
+ return false
+ }
+
+ private static void loadFromFile(Properties props, File factoriesFile) {
+ if (factoriesFile.exists()) {
+ Properties fileProps = new Properties()
+ factoriesFile.withInputStream { InputStream input ->
+ fileProps.load(input)
+ fileProps.each { Map.Entry prop ->
+ addToProps(props, (String) prop.key, (String) prop.value)
+ }
+ }
+ }
+ }
+
+ private static Properties addToProps(Properties props, String superTypeName, String classNodeNames) {
+ final List<String> classNodesNameList = classNodeNames.tokenize(',')
+ classNodesNameList.forEach(classNodeName -> {
+ String existing = props.getProperty(superTypeName)
+ if (!existing) {
+ props.put(superTypeName, classNodeName)
+ } else if (existing && !existing.contains(classNodeName)) {
Review Comment:
**Dedup uses substring matching, which drops distinct registrations.** `existing.contains(classNodeName)` is a substring test, not an exact-membership test. If `com.example.FooCommand` is already registered and `com.example.Foo` is added (or vice-versa - e.g. `MyCommand` vs `MyCommand2`), `contains` returns true and the second, distinct command is **silently not registered**. Any two commands whose FQCNs are prefixes of one another collide.
Fix: split the existing value on `,`, trim into a `Set`, and test exact membership before joining:
```groovy
private static void addToProps(Properties props, String superTypeName, String classNodeNames) {
def names = new LinkedHashSet<String>()
def existing = props.getProperty(superTypeName)
if (existing) names.addAll(existing.tokenize(',')*.trim())
classNodeNames.tokenize(',')*.trim().each { names.add(it) }
props.put(superTypeName, names.join(','))
}
```
##########
grails-core/src/main/groovy/org/grails/compiler/injection/FactoriesFileWriter.groovy:
##########
@@ -0,0 +1,136 @@
+/*
+ * 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.compiler.injection
+
+import java.lang.reflect.Modifier
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.ClassNode
+
+import org.springframework.core.CollectionFactory
+
+import org.apache.grails.gradle.common.PropertyFileUtils
+
+/**
+ * Writes factory registrations for compiled classes into a factories file in the compilation
+ * target directory, merging any existing entries from previous compilation runs and from
+ * hand-authored source registrations. The factories file location is supplied by the caller,
+ * so the writer is shared by transformations targeting different registration files
+ * (e.g. {@code META-INF/grails.factories} and {@code META-INF/grails-cli.factories}).
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class FactoriesFileWriter {
+
+ /**
+ * Registers the class as an implementation of the given super type in the factories file
+ * when the class is a non-abstract subtype.
+ *
+ * @param classNode the compiled class
+ * @param superType the factory type to register the class under
+ * @param compilationTargetDirectory the compilation output directory
+ * @param factoriesLocation the factories file path relative to the target directory
+ * @param sourceFactoriesLocations project-relative paths of hand-authored factories files to merge
+ * @return {@code true} when the class was a subtype of the factory type
+ */
+ static boolean updateFactoriesWithType(ClassNode classNode, ClassNode superType, File compilationTargetDirectory,
+ String factoriesLocation, List<String> sourceFactoriesLocations) {
+ if (GrailsASTUtils.isSubclassOfOrImplementsInterface(classNode, superType)) {
+ if (Modifier.isAbstract(classNode.getModifiers())) {
+ return false
+ }
+
+ def classNodeName = classNode.name
+ // Use SortedProperties to ensure a consistent order of entries for reproducible builds
+ def props = CollectionFactory.createSortedProperties(false)
+ def superTypeName = superType.getName()
+
+ File factoriesFile = new File(compilationTargetDirectory, factoriesLocation)
+ if (!factoriesFile.parentFile.exists()) {
+ factoriesFile.parentFile.mkdirs()
+ }
+ loadFromFile(props, factoriesFile)
+
+ File sourceDirectory = findSourceDirectory(compilationTargetDirectory)
+ if (sourceDirectory != null) {
+ for (String sourceFactoriesLocation : sourceFactoriesLocations) {
+ File sourceFactoriesFile = new File(sourceDirectory, sourceFactoriesLocation)
+ loadFromFile(props, sourceFactoriesFile)
+ }
+ }
+
+ addToProps(props, superTypeName, classNodeName)
+
+ factoriesFile.withWriter { Writer writer ->
Review Comment:
**Read-modify-write is neither synchronized nor atomic.** Each invocation reads the file, mutates, and rewrites it in place (L65-85). Under parallel compilation (multiple classes / source units, or forked compile processes) targeting the same output directory, two writers can interleave and lose registrations or leave a partially written factories file. Also note the `withWriter` here uses the platform-default charset while reads go through byte-oriented `Properties.load`.
Fix: serialize updates per canonical output file, and write to a temp file followed by an atomic rename (`Files.move(..., ATOMIC_MOVE)`); pin the charset to UTF-8 on both read and write.
##########
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:
**Eager dependency resolution at configuration time / configuration-cache hostility.** The `grailsCli.withDependencies { autoProvisionCliDependencies(...) }` callback (L152-154) resolves the `grailsCliDetect` probe graph and opens every dependency jar's manifest here, and `configureApplicationCommands` (L235-247) additionally realizes `grailsCliClasspath` artifact files inside `afterEvaluate`. This means **every** build - even `./gradlew help` or a task that never touches the CLI - pays full dependency resolution + jar I/O, defeats task-configuration avoidance, and is fragile under the Gradle 9.6 configuration cache (project/artifact access at the wrong time).
Fix: model command discovery as lazy task inputs (a `Provider<Set<String>>` fed by an `ArtifactView`/`FileCollection`) resolved at execution time, and avoid resolving configurations while registering tasks. Recommend adding a `--configuration-cache` functional test to lock this down.
##########
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
+ for (ResolvedArtifactResult artifact : lenientArtifacts.artifacts) {
+ String companion = findAdvertisedCliArtifact(project, artifact)
+ if (companion) {
+ companions.add(companion)
+ }
+ }
+
+ for (String companion : companions) {
+ List<String> coordinate = companion.tokenize(':')
+ if (coordinate.size() != 2) {
+ project.logger.warn('Ignoring malformed Grails-Cli-Artifact value [{}] found in the dependencies of project {}', companion, project.name)
+ continue
+ }
+ boolean alreadyDeclared = dependencies.any { Dependency existing ->
+ existing.group == coordinate[0] && existing.name == coordinate[1]
+ }
+ if (!alreadyDeclared) {
+ project.logger.info('Detected cli companion artifact {}, adding it to the {} configuration of project {}',
+ companion, GRAILS_CLI_CONFIGURATION, project.name)
+ dependencies.add(project.dependencies.create(companion))
+ }
+ }
+ }
+
+ /**
+ * Returns the companion cli coordinate ({@code group:artifactId}) advertised by the given
+ * resolved artifact, or {@code null}. For artifacts produced by a project of the same build
+ * the (possibly not yet built) jar is not read — the coordinate comes from the project's
+ * {@code cliArtifactId} property exported by the cli-artifact convention.
+ */
+ @CompileDynamic
+ protected String findAdvertisedCliArtifact(Project project, ResolvedArtifactResult artifact) {
+ def componentIdentifier = artifact.id.componentIdentifier
+ if (componentIdentifier instanceof ProjectComponentIdentifier) {
+ Project target = project.rootProject.findProject(((ProjectComponentIdentifier) componentIdentifier).projectPath)
+ def cliArtifactId = target?.findProperty('cliArtifactId')
Review Comment:
**Project-dependency companion discovery has an evaluation-order race.** For a same-build project dependency this reads the sibling project's `cliArtifactId` extra property - but `GrailsCliArtifactGradlePlugin` only sets that property inside *its own* `afterEvaluate`. A consumer project (e.g. the application, or an aggregating module) that is evaluated before the producing subproject will observe `cliArtifactId == null`, this branch returns `null` without falling back to reading the jar manifest, and the companion (plus its per-command tasks) is **silently omitted**. This bites composite / multi-project builds - including this repo's own modules - even though the published-jar path works.
Fix: defer the project-companion lookup until all projects are evaluated (`gradle.projectsEvaluated`), or expose the companion coordinate as a lazy `Provider` / published variant that can be queried without depending on `afterEvaluate` ordering.
##########
grails-bom/base/build.gradle:
##########
@@ -97,6 +97,23 @@ dependencies {
}
}
+// Companion cli artifacts (published by the cli-artifact convention plugin) are additional
+// publications of existing projects, so the subproject enumeration above cannot see them. Each
+// applying project exports its companion coordinate via the `cliArtifactId` extra property, which
+// only exists once that project has been evaluated — so the constraints are computed lazily, in
+// the mutation window Gradle provides right before the configuration is first observed.
+configurations.named('api').configure { apiConfiguration ->
Review Comment:
**Companion `-cli` constraints are missing for `enforcedPlatform` consumers of the derived BOMs.** This block adds the companion constraints dynamically only to the `api` configuration of `grails-bom/base`. The derived BOMs (`grails-bom/default`, `grails-bom/hibernate5`, ...) that consumers typically import via `enforcedPlatform(...)` re-declare only the *static* base constraints, so they do not carry these companion constraints in their own direct `constraints` block. Because `autoProvisionCliDependencies` adds `org.apache.grails:grails-core-cli` / `...-cli` with **no version**, an app that pins versions through `enforcedPlatform(grails-bom)` can end up with an unversioned `*-cli` dependency and a resolution failure - a consumer-facing break, contrary to AGENTS.md rule 14.
(Verified separately: the generated `grails-base-bom` POM does contain all 9 companions at `8.0.0-SNAPSHOT`; the gap is specifically the derived BOMs used through `enforcedPlatform`.)
Fix: share/reapply the companion-constraint enumeration in every derived BOM's direct `constraints` block, and add a resolution test using `enforcedPlatform(grails-bom)` that asserts each `*-cli` resolves to a version.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5011941668
@jamesfredley following up on the upgrade-path question: the docs now spell out the bounded impact (upgrade notes §35 "What this change means for upgrading an application" + What's New). Verified empirically as well — a Grails 7 plugin (grails-web-console 7.1.0) runs on a Grails 8 app built from this branch with its runtime functionality intact; the split itself required no plugin changes. Plugin re-releases are only needed for commands (companion `-cli` artifact) or where a plugin relied on APIs deprecated in 7 and removed in 8.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4930362965
The published Gradle Module Metadata still contains capability-style dependencies (org.apache.grails:grails-core + requireCapability('org.apache.grails:grails-core-cli')) — violating the split's design rule that external consumers must see the plain -cli coordinate, never capabilities. When Gradle resolves that from a Maven repo, it selects grails-core's cliRuntimeElements variant, follows its available-at redirect to the standalone grails-core-cli module, and then sees both nodes providing the same capability — a self-conflict. I reproduced it with a one-dependency project against build/local-maven, and it fails with both timestamped and non-unique snapshots, so it's not a snapshot-timestamp issue.
There are two distinct leak paths:
1. Every -cli companion (grails-scaffolding-cli, grails-data-hibernate5-cli, grails-web-url-mappings-cli, dbmigration, spring-security plugins): configureDependencyMapping (publishResolvedCoordinates in GrailsCliArtifactGradlePlugin) rewrites the POM correctly to grails-core-cli, but the GMM still records grails-core + requestedCapabilities — and Gradle consumers prefer the GMM.
2. grails-console declares the capability dep in its main api (grails-console/build.gradle:43). Its main java component has no dependency mapping at all, so its GMM leaks the capability dep — and worse, its POM collapses it to plain grails-core, meaning Maven consumers silently lose the cli dependency entirely.
I'm still researching possible solutions for this one.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4929206078
## 🚨 TestLens detected 2 failed tests 🚨
Here is what you can do:
1) Inspect the test failures carefully.
2) If you are convinced that some of the tests are flaky, you can mute them below.
3) Finally, trigger a rerun by checking the rerun checkbox.
### Test Summary
#### [CI / Build Grails Forge \(Java 21, indy\=true\)](https://github.com/apache/grails-core/actions/runs/29046287064/job/86215744487?pr=15948) > :test-core:test [grails-forge]
| Test | Runs | Flakiness |
|---|---|--:|
| CreateAppSpec > test basic create-app build task | ❌ | 0% 🟢 |
| CreateRestApiSpec > test rest-api app with default json-views | ❌ | 0% 🟢 |
🏷️ Commit: 01ddd85d574b54a4447b51471abe402beade340f
▶️ Tests: 7866 executed
🟡 Checks: 33/55 completed
### Test Failures
<details>
<summary><strong>CreateAppSpec > test basic create-app build task</strong> (:test-core:test [grails-forge] in <a href="https://github.com/apache/grails-core/actions/runs/29046287064/job/86215744487?pr=15948">CI / Build Grails Forge (Java 21, indy=true)</a>)</summary>
```
org.gradle.testkit.runner.UnexpectedBuildFailure: Unexpected build execution failure in /tmp/test-app11842820339447174111 with arguments [build, -x, iT]
Output:
> Task :assetCompile FAILED
[Incubating] Problems report is available at: file:///tmp/test-app11842820339447174111/build/reports/problems/problems-report.html
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':assetCompile' (registered by plugin 'cloud.wondrify.asset-pipeline').
> Could not resolve all files for configuration ':compileClasspath'.
> Could not resolve org.apache.grails:grails-core-cli.
Required by:
root project 'test-app11842820339447174111'
> Module 'org.apache.grails:grails-core-cli' has been rejected:
Cannot select module with conflict on capability 'org.apache.grails:grails-core-cli:8.0.0-SNAPSHOT' also provided by ['org.apache.grails:grails-core:8.0.0-SNAPSHOT:20260709.200025-1' (cliApiElements)]
> Could not resolve org.apache.grails:grails-core:8.0.0-SNAPSHOT.
Required by:
root project 'test-app11842820339447174111' > org.apache.grails:grails-console:8.0.0-SNAPSHOT:20260709.200025-1
root project 'test-app11842820339447174111' > org.apache.grails:grails-data-hibernate5-cli:8.0.0-SNAPSHOT:20260709.200025-1
root project 'test-app11842820339447174111' > org.apache.grails:grails-scaffolding-cli:8.0.0-SNAPSHOT:20260709.200025-1
root project 'test-app11842820339447174111' > org.apache.grails.web:grails-web-url-mappings-cli:8.0.0-SNAPSHOT:20260709.200025-1
> Module 'org.apache.grails:grails-core' has been rejected:
Cannot select module with conflict on capability 'org.apache.grails:grails-core-cli:8.0.0-SNAPSHOT' also provided by ['org.apache.grails:grails-core-cli:8.0.0-SNAPSHOT:20260709.200025-1' (cliApiElements)]
> There is 1 more failure with an identical cause.
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.
Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/9.6.0/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD FAILED in 1m 7s
1 actionable task: 1 executed
at org.gradle.testkit.runner.internal.DefaultGradleRunner.lambda$build$2(DefaultGradleRunner.java:275)
at org.gradle.testkit.runner.internal.DefaultGradleRunner.run(DefaultGradleRunner.java:374)
at org.gradle.testkit.runner.internal.DefaultGradleRunner.build(DefaultGradleRunner.java:273)
at org.grails.forge.utils.CommandSpec.executeGradle(CommandSpec.groovy:79)
at org.grails.forge.create.CreateAppSpec.test basic create-app build task(CreateAppSpec.groovy:49)
```
</details>
<details>
<summary><strong>CreateRestApiSpec > test rest-api app with default json-views</strong> (:test-core:test [grails-forge] in <a href="https://github.com/apache/grails-core/actions/runs/29046287064/job/86215744487?pr=15948">CI / Build Grails Forge (Java 21, indy=true)</a>)</summary>
```
org.gradle.testkit.runner.UnexpectedBuildFailure: Unexpected build execution failure in /tmp/testrestapi2018528820345638851 with arguments [build]
Output:
> Task :compileJava NO-SOURCE
> Task :compileGroovy FAILED
[Incubating] Problems report is available at: file:///tmp/testrestapi2018528820345638851/build/reports/problems/problems-report.html
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileGroovy' (registered by plugin class 'org.gradle.api.plugins.GroovyBasePlugin').
> Could not resolve all files for configuration ':compileClasspath'.
> Could not resolve org.apache.grails:grails-core-cli.
Required by:
root project 'testrestapi2018528820345638851'
> Module 'org.apache.grails:grails-core-cli' has been rejected:
Cannot select module with conflict on capability 'org.apache.grails:grails-core-cli:8.0.0-SNAPSHOT' also provided by ['org.apache.grails:grails-core:8.0.0-SNAPSHOT:20260709.200025-1' (cliApiElements)]
> Could not resolve org.apache.grails:grails-core:8.0.0-SNAPSHOT.
Required by:
root project 'testrestapi2018528820345638851' > org.apache.grails:grails-console:8.0.0-SNAPSHOT:20260709.200025-1
root project 'testrestapi2018528820345638851' > org.apache.grails:grails-data-hibernate5-cli:8.0.0-SNAPSHOT:20260709.200025-1
root project 'testrestapi2018528820345638851' > org.apache.grails.web:grails-web-url-mappings-cli:8.0.0-SNAPSHOT:20260709.200025-1
> Module 'org.apache.grails:grails-core' has been rejected:
Cannot select module with conflict on capability 'org.apache.grails:grails-core-cli:8.0.0-SNAPSHOT' also provided by ['org.apache.grails:grails-core-cli:8.0.0-SNAPSHOT:20260709.200025-1' (cliApiElements)]
> There is 1 more failure with an identical cause.
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.
Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/9.6.0/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD FAILED in 55s
1 actionable task: 1 executed
at org.gradle.testkit.runner.internal.DefaultGradleRunner.lambda$build$2(DefaultGradleRunner.java:275)
at org.gradle.testkit.runner.internal.DefaultGradleRunner.run(DefaultGradleRunner.java:374)
at org.gradle.testkit.runner.internal.DefaultGradleRunner.build(DefaultGradleRunner.java:273)
at org.grails.forge.utils.CommandSpec.executeGradle(CommandSpec.groovy:79)
at org.grails.forge.create.CreateRestApiSpec.test rest-api app with default json-views(CreateRestApiSpec.groovy:30)
```
</details>
### Muted Tests
> [!NOTE]
> Checks are currently running using the configuration below.
Select tests to mute in this pull request:
🔲 CreateAppSpec > test basic create-app build task <!-- uniqueId=[engine:spock]/[spec:org.grails.forge.create.CreateAppSpec]/[feature:$spock_feature_1_0] -->
🔲 CreateRestApiSpec > test rest-api app with default json-views <!-- uniqueId=[engine:spock]/[spec:org.grails.forge.create.CreateRestApiSpec]/[feature:$spock_feature_1_0] -->
Reuse successful test results:
🔲 ♻️ Only rerun the tests that failed or were muted before
Click the checkbox to trigger a rerun:
🔲 **Rerun jobs**
---
_Learn more about TestLens at [testlens.app](https://testlens.app)._
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3605786178
##########
dependencies.gradle:
##########
@@ -30,7 +30,7 @@ ext {
'directory-watcher.version' : '0.19.1',
'gradle-groovy.version' : '4.0.32',
'gradle-spock.version' : '2.4-groovy-4.0',
- 'grails-publish-plugin.version' : '1.0.0-M1',
+ 'grails-publish-plugin.version' : '1.0.0-SNAPSHOT',
Review Comment:
I'm going to resolve this - as part of the release vote we'll set it to a non-snapshot, staged version.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4997919401
@jamesfredley I'd like to make sure you're ok with the direction this PR takes. Can you review this before Sunday so we can possibly stage the next milestone?
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5005308008
> Didn't say it wasn't
awesome, glad you are onboard. Don't get me wrong, I *definitely* appreciate all your feedback and contributions.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "davydotcom (via GitHub)" <gi...@apache.org>.
davydotcom commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5005236591
Didn't say it wasn't
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "davydotcom (via GitHub)" <gi...@apache.org>.
davydotcom commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3604338009
##########
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
+ for (ResolvedArtifactResult artifact : lenientArtifacts.artifacts) {
+ String companion = findAdvertisedCliArtifact(project, artifact)
+ if (companion) {
+ companions.add(companion)
+ }
+ }
+
+ for (String companion : companions) {
+ List<String> coordinate = companion.tokenize(':')
+ if (coordinate.size() != 2) {
+ project.logger.warn('Ignoring malformed Grails-Cli-Artifact value [{}] found in the dependencies of project {}', companion, project.name)
+ continue
+ }
+ boolean alreadyDeclared = dependencies.any { Dependency existing ->
+ existing.group == coordinate[0] && existing.name == coordinate[1]
+ }
+ if (!alreadyDeclared) {
+ project.logger.info('Detected cli companion artifact {}, adding it to the {} configuration of project {}',
+ companion, GRAILS_CLI_CONFIGURATION, project.name)
+ dependencies.add(project.dependencies.create(companion))
+ }
+ }
+ }
+
+ /**
+ * Returns the companion cli coordinate ({@code group:artifactId}) advertised by the given
+ * resolved artifact, or {@code null}. For artifacts produced by a project of the same build
+ * the (possibly not yet built) jar is not read — the coordinate comes from the project's
+ * {@code cliArtifactId} property exported by the cli-artifact convention.
+ */
+ @CompileDynamic
+ protected String findAdvertisedCliArtifact(Project project, ResolvedArtifactResult artifact) {
+ def componentIdentifier = artifact.id.componentIdentifier
+ if (componentIdentifier instanceof ProjectComponentIdentifier) {
+ Project target = project.rootProject.findProject(((ProjectComponentIdentifier) componentIdentifier).projectPath)
+ def cliArtifactId = target?.findProperty('cliArtifactId')
+ return cliArtifactId ? "${target.group}:${cliArtifactId}" as String : null
+ }
+
+ File file = artifact.file
+ if (file == null || !file.isFile() || !file.name.endsWith('.jar')) {
+ return null
+ }
+ try (JarFile jarFile = new JarFile(file)) {
+ return jarFile.manifest?.mainAttributes?.getValue('Grails-Cli-Artifact')
+ }
+ catch (IOException ignored) {
+ return null
+ }
+ }
+
+ @CompileDynamic
+ protected void configureApplicationCommands(Project project) {
+ def applicationContextCommands = FactoriesLoaderSupport.loadFactoryNames(APPLICATION_CONTEXT_COMMAND_CLASS,
+ FactoriesLoaderSupport.classLoader, FactoriesLoaderSupport.CLI_FACTORIES_RESOURCE_LOCATION)
+ project.afterEvaluate {
+ FileCollection fileCollection = ClasspathUtils.buildClasspath(project, project.configurations.runtimeClasspath, project.configurations.grailsCliClasspath)
+ // commands come from the buildscript classloader (legacy placement) and from the
+ // project's cli tier, so auto-provisioned command artifacts register their tasks
+ // without any buildscript classpath entry
+ Set<String> commandClassNames = new LinkedHashSet<String>()
+ if (applicationContextCommands) {
+ commandClassNames.addAll(applicationContextCommands as List)
+ }
+ GrailsExtension grails = project.extensions.getByType(GrailsExtension)
+ if (grails.cliAutoProvision.get()) {
+ commandClassNames.addAll(loadCommandNamesFromCliClasspath(project))
+ }
+ for (ctxCommand in commandClassNames) {
+ String taskName = GrailsNameUtils.getLogicalPropertyName(ctxCommand, 'Command')
+ String commandName = GrailsNameUtils.getScriptName(GrailsNameUtils.getLogicalName(ctxCommand, 'Command'))
+ if (!project.tasks.names.contains(taskName)) {
+ project.tasks.register(taskName, ApplicationContextCommandTask).configure {
+ it.classpath = fileCollection
+ it.command = commandName
+ it.systemProperty(Environment.KEY, System.getProperty(Environment.KEY, Environment.DEVELOPMENT.getName()))
+ List<Object> args = []
+ def otherArgs = project.findProperty('args')
+ if (otherArgs) {
+ args.addAll(CommandLineParser.translateCommandline(otherArgs as String))
+ }
+
+ def appClassProvider = GrailsGradlePlugin.getMainClassProvider(project)
+
+ it.doFirst {
+ args << appClassProvider.get()
+ it.args(args)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads command class names from the {@code META-INF/grails-cli.factories} files of the
+ * resolved {@code grailsCliClasspath} jars, so per-command Gradle tasks (e.g. {@code dbmUpdate})
+ * are registered for auto-provisioned cli artifacts. Resolution is lenient and jars that are
+ * not built yet (project dependencies within a composite) are skipped — the generic
+ * {@code runCommand} task can always execute those commands regardless.
+ */
+ @CompileDynamic
+ protected Collection<String> loadCommandNamesFromCliClasspath(Project project) {
+ Set<String> names = new LinkedHashSet<String>()
+ Configuration cliClasspath = project.configurations.findByName(GRAILS_CLI_CLASSPATH_CONFIGURATION)
+ if (cliClasspath == null) {
+ return names
+ }
+ for (File file : cliClasspath.incoming.artifactView { it.lenient(true) }.files) {
Review Comment:
A caching/scope mechanism would indeed be handy as a future.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5005982627
@scott @davydotcom @jamesfredley I know @jamesfredley brought up purchasing AI credits for key contributors. We should have a wider discussion about this in the weekly. If AI helps us get the reviews done easier, I'm for it. What my concern is is AI generating 100's of PRs that then no one but AI reviews. That's what often gets categorized as "AI Slop" and it can lead to contributor time being wasted. Another way to say this: if AI helps accelerate contributions, great. If AI helps accelerate only commits and then not reviews, it's a major problem for us.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3608620497
##########
grails-core/src/test/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformationSpec.groovy:
##########
@@ -0,0 +1,45 @@
+/*
+ * 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.compiler
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+/**
+ * Commands shipped in a companion cli artifact compile from the {@code cli} source set
+ * ({@code src/cli/groovy}), which is not a standard project-source location — the transformation
+ * must still register them in {@code META-INF/grails-cli.factories}.
+ */
+class CommandFactoriesTransformationSpec extends Specification {
Review Comment:
Added FactoriesFileWriterSpec, which drives the registration mechanism end-to-end: a concrete subtype is registered in the factories file, an abstract base class is excluded, a non-subtype is ignored, and two distinct commands whose fully-qualified names share a prefix are both kept. The stale legacy grails.factories clean-break scenario is left as-is per the thread.
##########
grails-core/src/main/groovy/org/grails/compiler/injection/FactoriesFileWriter.groovy:
##########
@@ -0,0 +1,136 @@
+/*
+ * 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.compiler.injection
+
+import java.lang.reflect.Modifier
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.ClassNode
+
+import org.springframework.core.CollectionFactory
+
+import org.apache.grails.gradle.common.PropertyFileUtils
+
+/**
+ * Writes factory registrations for compiled classes into a factories file in the compilation
+ * target directory, merging any existing entries from previous compilation runs and from
+ * hand-authored source registrations. The factories file location is supplied by the caller,
+ * so the writer is shared by transformations targeting different registration files
+ * (e.g. {@code META-INF/grails.factories} and {@code META-INF/grails-cli.factories}).
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class FactoriesFileWriter {
+
+ /**
+ * Registers the class as an implementation of the given super type in the factories file
+ * when the class is a non-abstract subtype.
+ *
+ * @param classNode the compiled class
+ * @param superType the factory type to register the class under
+ * @param compilationTargetDirectory the compilation output directory
+ * @param factoriesLocation the factories file path relative to the target directory
+ * @param sourceFactoriesLocations project-relative paths of hand-authored factories files to merge
+ * @return {@code true} when the class was a subtype of the factory type
+ */
+ static boolean updateFactoriesWithType(ClassNode classNode, ClassNode superType, File compilationTargetDirectory,
+ String factoriesLocation, List<String> sourceFactoriesLocations) {
+ if (GrailsASTUtils.isSubclassOfOrImplementsInterface(classNode, superType)) {
+ if (Modifier.isAbstract(classNode.getModifiers())) {
+ return false
+ }
+
+ def classNodeName = classNode.name
+ // Use SortedProperties to ensure a consistent order of entries for reproducible builds
+ def props = CollectionFactory.createSortedProperties(false)
+ def superTypeName = superType.getName()
+
+ File factoriesFile = new File(compilationTargetDirectory, factoriesLocation)
+ if (!factoriesFile.parentFile.exists()) {
+ factoriesFile.parentFile.mkdirs()
+ }
+ loadFromFile(props, factoriesFile)
+
+ File sourceDirectory = findSourceDirectory(compilationTargetDirectory)
+ if (sourceDirectory != null) {
+ for (String sourceFactoriesLocation : sourceFactoriesLocations) {
+ File sourceFactoriesFile = new File(sourceDirectory, sourceFactoriesLocation)
+ loadFromFile(props, sourceFactoriesFile)
+ }
+ }
+
+ addToProps(props, superTypeName, classNodeName)
+
+ factoriesFile.withWriter { Writer writer ->
+ props.store(writer, 'Grails Factories File')
+ }
+
+ PropertyFileUtils.makePropertiesFileReproducible(factoriesFile)
+
+ return true
+ }
+ return false
+ }
+
+ private static void loadFromFile(Properties props, File factoriesFile) {
+ if (factoriesFile.exists()) {
+ Properties fileProps = new Properties()
+ factoriesFile.withInputStream { InputStream input ->
+ fileProps.load(input)
+ fileProps.each { Map.Entry prop ->
+ addToProps(props, (String) prop.key, (String) prop.value)
+ }
+ }
+ }
+ }
+
+ private static Properties addToProps(Properties props, String superTypeName, String classNodeNames) {
+ final List<String> classNodesNameList = classNodeNames.tokenize(',')
+ classNodesNameList.forEach(classNodeName -> {
+ String existing = props.getProperty(superTypeName)
+ if (!existing) {
+ props.put(superTypeName, classNodeName)
+ } else if (existing && !existing.contains(classNodeName)) {
Review Comment:
Fixed. addToProps now dedups on exact membership (a LinkedHashSet of trimmed names) rather than a substring contains() test, so distinct commands whose fully-qualified names are prefixes of one another (Foo vs FooCommand) are both registered. FactoriesFileWriterSpec includes a regression case for the prefix collision.
##########
grails-bom/base/build.gradle:
##########
@@ -97,6 +97,23 @@ dependencies {
}
}
+// Companion cli artifacts (published by the cli-artifact convention plugin) are additional
+// publications of existing projects, so the subproject enumeration above cannot see them. Each
+// applying project exports its companion coordinate via the `cliArtifactId` extra property, which
+// only exists once that project has been evaluated — so the constraints are computed lazily, in
+// the mutation window Gradle provides right before the configuration is first observed.
+configurations.named('api').configure { apiConfiguration ->
Review Comment:
Fixed. The companion-constraint enumeration is extracted into gradle/cli-companion-bom-constraints.gradle and applied to the base and every derived BOM, so the -cli constraints are declared directly on each platform rather than only inherited via platform(), and enforcedPlatform() consumers receive forced companion versions. Verified from the generated POMs that grails-bom, both hibernate BOMs, and all three micronaut BOMs now carry all nine companions.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4930303385
## ✅ All tests passed ✅
🏷️ Commit: 45632e28f5d6c450f3ae2d0d9faae8967c91be1e
▶️ Tests: 23917 executed
⚪️ Checks: 62/62 completed
---
_Learn more about TestLens at [testlens.app](https://testlens.app)._
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4931124584
## 🚨 TestLens detected 1 failed test 🚨
Here is what you can do:
1) Inspect the test failures carefully.
2) If you are convinced that some of the tests are flaky, you can mute them below.
3) Finally, trigger a rerun by checking the rerun checkbox.
### Test Summary
#### [CI - Groovy Joint Validation Build / build\_grails](https://github.com/apache/grails-core/actions/runs/29058533594/job/86255729961?pr=15948) > :grails-test-examples-gsp-sitemesh3:integrationTest
| Test | Runs | Flakiness |
|---|---|--:|
| EndToEndSpec > async multiple levels of layouts | ❌ | [1% 🟡](https://github.com/apache/grails-core/issues/15938) |
🏷️ Commit: c6739f408e71399ea9b0803b5fa1e6694b9f5a74
▶️ Tests: 26319 executed
🟡 Checks: 54/56 completed
### Test Failures
<details>
<summary><strong>EndToEndSpec > async multiple levels of layouts</strong> (:grails-test-examples-gsp-sitemesh3:integrationTest in <a href="https://github.com/apache/grails-core/actions/runs/29058533594/job/86255729961?pr=15948">CI - Groovy Joint Validation Build / build_grails</a>)</summary>
```
org.openqa.selenium.TimeoutException: java.util.concurrent.TimeoutException
Build info: version: '4.43.0', revision: 'dd0f534'
System info: os.name: 'Linux', os.arch: 'amd64', os.version: '6.17.0-1018-azure', java.version: '21.0.11'
Driver info: org.openqa.selenium.remote.RemoteWebDriver
Command: [2e56d013782943b6bcc37da593fc4518, get {url=http://host.testcontainers.internal:35703/endToEnd/asyncMultipleLevelsOfLayouts}]
Capabilities {acceptInsecureCerts: false, browserName: chrome, browserVersion: 147.0.7727.55, chrome: {chromedriverVersion: 147.0.7727.56 (b28eac7a1a97..., userDataDir: /tmp/org.chromium.Chromium....}, fedcm:accounts: true, goog:chromeOptions: {debuggerAddress: localhost:41371}, goog:processID: 185, networkConnectionEnabled: false, pageLoadStrategy: normal, platformName: linux, proxy: Proxy(), se:bidiEnabled: false, se:cdp: ws://172.17.0.7:4444/sessio..., se:cdpVersion: 147.0.7727.55, se:containerName: 8e2543c953d7, se:deleteSessionOnUi: true, se:downloadsEnabled: true, se:noVncPort: 7900, se:vnc: ws://172.17.0.7:4444/sessio..., se:vncEnabled: true, se:vncLocalAddress: ws://172.17.0.7:7900, setWindowRect: true, strictFileInteractability: false, timeouts: {implicit: 0, pageLoad: 300000, script: 30000}, unhandledPromptBehavior: dismiss and notify, webauthn:extension:credBlob: true, webauthn:extension:largeBlob: true, webauthn:extension:minPinLength: true, webauthn:extension:prf: tru
e, webauthn:virtualAuthenticators: true}
Session ID: 2e56d013782943b6bcc37da593fc4518
at org.openqa.selenium.remote.http.jdk.JdkHttpClient.execute(JdkHttpClient.java:429)
at org.openqa.selenium.remote.tracing.TracedHttpClient.execute(TracedHttpClient.java:54)
at org.openqa.selenium.remote.HttpCommandExecutor.execute(HttpCommandExecutor.java:220)
at org.openqa.selenium.remote.TracedCommandExecutor.execute(TracedCommandExecutor.java:53)
at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:606)
at org.openqa.selenium.remote.RemoteWebDriver.get(RemoteWebDriver.java:374)
at geb.Browser.go(Browser.groovy:515)
at geb.Browser.go(Browser.groovy:492)
at grails.plugin.geb.support.delegate.BrowserDelegate$Trait$Helper.go(BrowserDelegate.groovy:132)
at EndToEndSpec.async multiple levels of layouts(EndToEndSpec.groovy:104)
Caused by: java.util.concurrent.TimeoutException
at java.base/java.util.concurrent.CompletableFuture$Timeout.run(CompletableFuture.java:2920)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
```
</details>
### Muted Tests
> [!NOTE]
> Checks are currently running using the configuration below.
Select tests to mute in this pull request:
🔲 EndToEndSpec > async multiple levels of layouts <!-- uniqueId=[engine:spock]/[spec:EndToEndSpec]/[feature:$spock_feature_1_7] -->
Reuse successful test results:
🔲 ♻️ Only rerun the tests that failed or were muted before
Click the checkbox to trigger a rerun:
🔲 **Rerun jobs**
---
_Learn more about TestLens at [testlens.app](https://testlens.app)._
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "codecov[bot] (via GitHub)" <gi...@apache.org>.
codecov[bot] commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4929328418
## [Codecov](https://app.codecov.io/gh/apache/grails-core/pull/15948?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
:x: Patch coverage is `3.31325%` with `321 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 49.4106%. Comparing base ([`05f71e6`](https://app.codecov.io/gh/apache/grails-core/commit/05f71e6624f9681192d9f866b800e7003e001f18?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`01ddd85`](https://app.codecov.io/gh/apache/grails-core/commit/01ddd85d574b54a4447b51471abe402beade340f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
:warning: Report is 11 commits behind head on 8.0.x.
| [Files with missing lines](https://app.codecov.io/gh/apache/grails-core/pull/15948?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Patch % | Lines |
|---|---|---|
| [...radle/plugin/commands/GrailsCliGradlePlugin.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Fcommands%2FGrailsCliGradlePlugin.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vY29tbWFuZHMvR3JhaWxzQ2xpR3JhZGxlUGx1Z2luLmdyb292eQ==) | 0.0000% | [178 Missing :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...ugin/commands/GrailsCliArtifactGradlePlugin.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Fcommands%2FGrailsCliArtifactGradlePlugin.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vY29tbWFuZHMvR3JhaWxzQ2xpQXJ0aWZhY3RHcmFkbGVQbHVnaW4uZ3Jvb3Z5) | 0.0000% | [73 Missing :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...ails/compiler/injection/FactoriesFileWriter.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-core%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fcompiler%2Finjection%2FFactoriesFileWriter.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWNvcmUvc3JjL21haW4vZ3Jvb3Z5L29yZy9ncmFpbHMvY29tcGlsZXIvaW5qZWN0aW9uL0ZhY3Rvcmllc0ZpbGVXcml0ZXIuZ3Jvb3Z5) | 15.2174% | [35 Missing and 4 partials :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...rg/grails/gradle/plugin/util/ClasspathUtils.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Futil%2FClasspathUtils.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vdXRpbC9DbGFzc3BhdGhVdGlscy5ncm9vdnk=) | 0.0000% | [11 Missing :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...rails/core/io/support/GrailsFactoriesLoader.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-core%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fcore%2Fio%2Fsupport%2FGrailsFactoriesLoader.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWNvcmUvc3JjL21haW4vZ3Jvb3Z5L29yZy9ncmFpbHMvY29yZS9pby9zdXBwb3J0L0dyYWlsc0ZhY3Rvcmllc0xvYWRlci5ncm9vdnk=) | 12.5000% | [7 Missing :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...rg/grails/io/support/FactoriesLoaderSupport.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fmodel%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fio%2Fsupport%2FFactoriesLoaderSupport.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9tb2RlbC9zcmMvbWFpbi9ncm9vdnkvb3JnL2dyYWlscy9pby9zdXBwb3J0L0ZhY3Rvcmllc0xvYWRlclN1cHBvcnQuZ3Jvb3Z5) | 0.0000% | [7 Missing :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...g/grails/gradle/plugin/core/GrailsExtension.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Fcore%2FGrailsExtension.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vY29yZS9HcmFpbHNFeHRlbnNpb24uZ3Jvb3Z5) | 33.3333% | [2 Missing :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...ds/factory/ApplicationContextCommandFactory.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-shell-cli%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fcli%2Fprofile%2Fcommands%2Ffactory%2FApplicationContextCommandFactory.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLXNoZWxsLWNsaS9zcmMvbWFpbi9ncm9vdnkvb3JnL2dyYWlscy9jbGkvcHJvZmlsZS9jb21tYW5kcy9mYWN0b3J5L0FwcGxpY2F0aW9uQ29udGV4dENvbW1hbmRGYWN0b3J5Lmdyb292eQ==) | 0.0000% | [2 Missing :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...rails/gradle/plugin/core/GrailsGradlePlugin.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Fcore%2FGrailsGradlePlugin.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vY29yZS9HcmFpbHNHcmFkbGVQbHVnaW4uZ3Jvb3Z5) | 0.0000% | [1 Missing :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...ils/gradle/plugin/web/GrailsWebGradlePlugin.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Fweb%2FGrailsWebGradlePlugin.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vd2ViL0dyYWlsc1dlYkdyYWRsZVBsdWdpbi5ncm9vdnk=) | 0.0000% | [1 Missing :warning: ](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
<details><summary>Additional details and impacted files</summary>
[](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
```diff
@@ Coverage Diff @@
## 8.0.x #15948 +/- ##
================================================
+ Coverage 0 49.4106% +49.4106%
- Complexity 0 16750 +16750
================================================
Files 0 1975 +1975
Lines 0 92984 +92984
Branches 0 16265 +16265
================================================
+ Hits 0 45944 +45944
- Misses 0 39947 +39947
- Partials 0 7093 +7093
```
| [Files with missing lines](https://app.codecov.io/gh/apache/grails-core/pull/15948?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
|---|---|---|
| [...mmand/GrailsApplicationContextCommandRunner.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-console%2Fsrc%2Fmain%2Fgroovy%2Fgrails%2Fui%2Fcommand%2FGrailsApplicationContextCommandRunner.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWNvbnNvbGUvc3JjL21haW4vZ3Jvb3Z5L2dyYWlscy91aS9jb21tYW5kL0dyYWlsc0FwcGxpY2F0aW9uQ29udGV4dENvbW1hbmRSdW5uZXIuZ3Jvb3Z5) | `2.7778% <ø> (ø)` | |
| [...ion/GlobalGrailsClassInjectorTransformation.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-core%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fcompiler%2Finjection%2FGlobalGrailsClassInjectorTransformation.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWNvcmUvc3JjL21haW4vZ3Jvb3Z5L29yZy9ncmFpbHMvY29tcGlsZXIvaW5qZWN0aW9uL0dsb2JhbEdyYWlsc0NsYXNzSW5qZWN0b3JUcmFuc2Zvcm1hdGlvbi5ncm9vdnk=) | `64.0000% <100.0000%> (ø)` | |
| [...rails/gradle/plugin/core/GrailsGradlePlugin.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Fcore%2FGrailsGradlePlugin.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vY29yZS9HcmFpbHNHcmFkbGVQbHVnaW4uZ3Jvb3Z5) | `0.0000% <0.0000%> (ø)` | |
| [...ils/gradle/plugin/web/GrailsWebGradlePlugin.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Fweb%2FGrailsWebGradlePlugin.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vd2ViL0dyYWlsc1dlYkdyYWRsZVBsdWdpbi5ncm9vdnk=) | `0.0000% <0.0000%> (ø)` | |
| [...g/grails/gradle/plugin/core/GrailsExtension.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Fcore%2FGrailsExtension.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vY29yZS9HcmFpbHNFeHRlbnNpb24uZ3Jvb3Z5) | `55.8140% <33.3333%> (ø)` | |
| [...ds/factory/ApplicationContextCommandFactory.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-shell-cli%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fcli%2Fprofile%2Fcommands%2Ffactory%2FApplicationContextCommandFactory.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLXNoZWxsLWNsaS9zcmMvbWFpbi9ncm9vdnkvb3JnL2dyYWlscy9jbGkvcHJvZmlsZS9jb21tYW5kcy9mYWN0b3J5L0FwcGxpY2F0aW9uQ29udGV4dENvbW1hbmRGYWN0b3J5Lmdyb292eQ==) | `0.0000% <0.0000%> (ø)` | |
| [...rails/core/io/support/GrailsFactoriesLoader.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-core%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fcore%2Fio%2Fsupport%2FGrailsFactoriesLoader.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWNvcmUvc3JjL21haW4vZ3Jvb3Z5L29yZy9ncmFpbHMvY29yZS9pby9zdXBwb3J0L0dyYWlsc0ZhY3Rvcmllc0xvYWRlci5ncm9vdnk=) | `47.5000% <12.5000%> (ø)` | |
| [...rg/grails/io/support/FactoriesLoaderSupport.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fmodel%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fio%2Fsupport%2FFactoriesLoaderSupport.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9tb2RlbC9zcmMvbWFpbi9ncm9vdnkvb3JnL2dyYWlscy9pby9zdXBwb3J0L0ZhY3Rvcmllc0xvYWRlclN1cHBvcnQuZ3Jvb3Z5) | `0.0000% <0.0000%> (ø)` | |
| [...rg/grails/gradle/plugin/util/ClasspathUtils.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-gradle%2Fplugins%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fgradle%2Fplugin%2Futil%2FClasspathUtils.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWdyYWRsZS9wbHVnaW5zL3NyYy9tYWluL2dyb292eS9vcmcvZ3JhaWxzL2dyYWRsZS9wbHVnaW4vdXRpbC9DbGFzc3BhdGhVdGlscy5ncm9vdnk=) | `0.0000% <0.0000%> (ø)` | |
| [...ails/compiler/injection/FactoriesFileWriter.groovy](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree&filepath=grails-core%2Fsrc%2Fmain%2Fgroovy%2Forg%2Fgrails%2Fcompiler%2Finjection%2FFactoriesFileWriter.groovy&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Z3JhaWxzLWNvcmUvc3JjL21haW4vZ3Jvb3Z5L29yZy9ncmFpbHMvY29tcGlsZXIvaW5qZWN0aW9uL0ZhY3Rvcmllc0ZpbGVXcml0ZXIuZ3Jvb3Z5) | `15.2174% <15.2174%> (ø)` | |
| ... and [2 more](https://app.codecov.io/gh/apache/grails-core/pull/15948?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | |
... and [1963 files with indirect coverage changes](https://app.codecov.io/gh/apache/grails-core/pull/15948/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
</details>
<details><summary> :rocket: New features to boost your workflow: </summary>
- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4929312644
These were resolving locally due to having maven local search on; I need to fix the dependency substitution to fix these resolution issues.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5005227862
@davydotcom everyone understands AI is not perfect, but if it is going to speed up the time in getting PRs reviewed, I am 100% for it. We need to move things forward at a MUCH faster rate.
Grails has notoriously been wayyy behind on the development cycle and in a constant state of catch up. The fact that it took this simple PR a week to get evaluated showcases this problem.
I invite anyone willing to spend money on tokens on a top tier model to spend some on any PR. Those comments were generated with Fable and done a week before anyone else set eyes on it. It was just meant to get things moving forward.
You are literally responding today to something generated a week ago.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3608643049
##########
grails-core/src/main/groovy/org/grails/compiler/injection/FactoriesFileWriter.groovy:
##########
@@ -0,0 +1,136 @@
+/*
+ * 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.compiler.injection
+
+import java.lang.reflect.Modifier
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.ClassNode
+
+import org.springframework.core.CollectionFactory
+
+import org.apache.grails.gradle.common.PropertyFileUtils
+
+/**
+ * Writes factory registrations for compiled classes into a factories file in the compilation
+ * target directory, merging any existing entries from previous compilation runs and from
+ * hand-authored source registrations. The factories file location is supplied by the caller,
+ * so the writer is shared by transformations targeting different registration files
+ * (e.g. {@code META-INF/grails.factories} and {@code META-INF/grails-cli.factories}).
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class FactoriesFileWriter {
+
+ /**
+ * Registers the class as an implementation of the given super type in the factories file
+ * when the class is a non-abstract subtype.
+ *
+ * @param classNode the compiled class
+ * @param superType the factory type to register the class under
+ * @param compilationTargetDirectory the compilation output directory
+ * @param factoriesLocation the factories file path relative to the target directory
+ * @param sourceFactoriesLocations project-relative paths of hand-authored factories files to merge
+ * @return {@code true} when the class was a subtype of the factory type
+ */
+ static boolean updateFactoriesWithType(ClassNode classNode, ClassNode superType, File compilationTargetDirectory,
+ String factoriesLocation, List<String> sourceFactoriesLocations) {
+ if (GrailsASTUtils.isSubclassOfOrImplementsInterface(classNode, superType)) {
+ if (Modifier.isAbstract(classNode.getModifiers())) {
+ return false
+ }
+
+ def classNodeName = classNode.name
+ // Use SortedProperties to ensure a consistent order of entries for reproducible builds
+ def props = CollectionFactory.createSortedProperties(false)
+ def superTypeName = superType.getName()
+
+ File factoriesFile = new File(compilationTargetDirectory, factoriesLocation)
+ if (!factoriesFile.parentFile.exists()) {
+ factoriesFile.parentFile.mkdirs()
+ }
+ loadFromFile(props, factoriesFile)
+
+ File sourceDirectory = findSourceDirectory(compilationTargetDirectory)
+ if (sourceDirectory != null) {
+ for (String sourceFactoriesLocation : sourceFactoriesLocations) {
+ File sourceFactoriesFile = new File(sourceDirectory, sourceFactoriesLocation)
+ loadFromFile(props, sourceFactoriesFile)
+ }
+ }
+
+ addToProps(props, superTypeName, classNodeName)
+
+ factoriesFile.withWriter { Writer writer ->
Review Comment:
I also forgot to mention that gradle already handles "exclusive" locks because the project doesn't parallel compile these files - they're part of a single project and the build system is what handles these locks. We would not want to add exclusivitiy here - it's the role of the build system instead.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3605828408
##########
grails-bom/base/build.gradle:
##########
@@ -97,6 +97,23 @@ dependencies {
}
}
+// Companion cli artifacts (published by the cli-artifact convention plugin) are additional
+// publications of existing projects, so the subproject enumeration above cannot see them. Each
+// applying project exports its companion coordinate via the `cliArtifactId` extra property, which
+// only exists once that project has been evaluated — so the constraints are computed lazily, in
+// the mutation window Gradle provides right before the configuration is first observed.
+configurations.named('api').configure { apiConfiguration ->
+ apiConfiguration.withDependencies {
+ for (Project subproject : rootProject.subprojects) {
+ def cliArtifactId = subproject.findProperty('cliArtifactId')
+ if (cliArtifactId) {
+ apiConfiguration.dependencyConstraints.add(
+ project.dependencies.constraints.create("${subproject.group}:${cliArtifactId}:${projectVersion}"))
Review Comment:
Resolving this since it won't apply as part of the release vote - we can't release snapshots in our builds anyhow.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3608639764
##########
grails-core/src/main/groovy/org/grails/compiler/injection/FactoriesFileWriter.groovy:
##########
@@ -0,0 +1,136 @@
+/*
+ * 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.compiler.injection
+
+import java.lang.reflect.Modifier
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.ClassNode
+
+import org.springframework.core.CollectionFactory
+
+import org.apache.grails.gradle.common.PropertyFileUtils
+
+/**
+ * Writes factory registrations for compiled classes into a factories file in the compilation
+ * target directory, merging any existing entries from previous compilation runs and from
+ * hand-authored source registrations. The factories file location is supplied by the caller,
+ * so the writer is shared by transformations targeting different registration files
+ * (e.g. {@code META-INF/grails.factories} and {@code META-INF/grails-cli.factories}).
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class FactoriesFileWriter {
+
+ /**
+ * Registers the class as an implementation of the given super type in the factories file
+ * when the class is a non-abstract subtype.
+ *
+ * @param classNode the compiled class
+ * @param superType the factory type to register the class under
+ * @param compilationTargetDirectory the compilation output directory
+ * @param factoriesLocation the factories file path relative to the target directory
+ * @param sourceFactoriesLocations project-relative paths of hand-authored factories files to merge
+ * @return {@code true} when the class was a subtype of the factory type
+ */
+ static boolean updateFactoriesWithType(ClassNode classNode, ClassNode superType, File compilationTargetDirectory,
+ String factoriesLocation, List<String> sourceFactoriesLocations) {
+ if (GrailsASTUtils.isSubclassOfOrImplementsInterface(classNode, superType)) {
+ if (Modifier.isAbstract(classNode.getModifiers())) {
+ return false
+ }
+
+ def classNodeName = classNode.name
+ // Use SortedProperties to ensure a consistent order of entries for reproducible builds
+ def props = CollectionFactory.createSortedProperties(false)
+ def superTypeName = superType.getName()
+
+ File factoriesFile = new File(compilationTargetDirectory, factoriesLocation)
+ if (!factoriesFile.parentFile.exists()) {
+ factoriesFile.parentFile.mkdirs()
+ }
+ loadFromFile(props, factoriesFile)
+
+ File sourceDirectory = findSourceDirectory(compilationTargetDirectory)
+ if (sourceDirectory != null) {
+ for (String sourceFactoriesLocation : sourceFactoriesLocations) {
+ File sourceFactoriesFile = new File(sourceDirectory, sourceFactoriesLocation)
+ loadFromFile(props, sourceFactoriesFile)
+ }
+ }
+
+ addToProps(props, superTypeName, classNodeName)
+
+ factoriesFile.withWriter { Writer writer ->
Review Comment:
This code was modeled off the existing factories writer. Yes, this is an issue but it's been a long standing issue that would require tracking the current class set. Incremental compile is considered an incubating feature of Gradle and not a default feature. This is something we should track separately from this change. I've opened https://github.com/apache/grails-core/issues/16007 for this.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "testlens-app[bot] (via GitHub)" <gi...@apache.org>.
testlens-app[bot] commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5005991562
## ✅ All tests passed ✅
🏷️ Commit: 1fd7f7d4055a9c61e70ee743f9f5ee2a04c19da4
▶️ Tests: 30790 executed
⚪️ Checks: 61/61 completed
---
_Learn more about TestLens at [testlens.app](https://testlens.app)._
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3605784303
##########
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
+ for (ResolvedArtifactResult artifact : lenientArtifacts.artifacts) {
+ String companion = findAdvertisedCliArtifact(project, artifact)
+ if (companion) {
+ companions.add(companion)
+ }
+ }
+
+ for (String companion : companions) {
+ List<String> coordinate = companion.tokenize(':')
+ if (coordinate.size() != 2) {
+ project.logger.warn('Ignoring malformed Grails-Cli-Artifact value [{}] found in the dependencies of project {}', companion, project.name)
+ continue
+ }
+ boolean alreadyDeclared = dependencies.any { Dependency existing ->
+ existing.group == coordinate[0] && existing.name == coordinate[1]
+ }
+ if (!alreadyDeclared) {
+ project.logger.info('Detected cli companion artifact {}, adding it to the {} configuration of project {}',
+ companion, GRAILS_CLI_CONFIGURATION, project.name)
+ dependencies.add(project.dependencies.create(companion))
+ }
+ }
+ }
+
+ /**
+ * Returns the companion cli coordinate ({@code group:artifactId}) advertised by the given
+ * resolved artifact, or {@code null}. For artifacts produced by a project of the same build
+ * the (possibly not yet built) jar is not read — the coordinate comes from the project's
+ * {@code cliArtifactId} property exported by the cli-artifact convention.
+ */
+ @CompileDynamic
+ protected String findAdvertisedCliArtifact(Project project, ResolvedArtifactResult artifact) {
+ def componentIdentifier = artifact.id.componentIdentifier
+ if (componentIdentifier instanceof ProjectComponentIdentifier) {
+ Project target = project.rootProject.findProject(((ProjectComponentIdentifier) componentIdentifier).projectPath)
+ def cliArtifactId = target?.findProperty('cliArtifactId')
+ return cliArtifactId ? "${target.group}:${cliArtifactId}" as String : null
+ }
+
+ File file = artifact.file
+ if (file == null || !file.isFile() || !file.name.endsWith('.jar')) {
+ return null
+ }
+ try (JarFile jarFile = new JarFile(file)) {
+ return jarFile.manifest?.mainAttributes?.getValue('Grails-Cli-Artifact')
+ }
+ catch (IOException ignored) {
+ return null
+ }
+ }
+
+ @CompileDynamic
+ protected void configureApplicationCommands(Project project) {
+ def applicationContextCommands = FactoriesLoaderSupport.loadFactoryNames(APPLICATION_CONTEXT_COMMAND_CLASS,
+ FactoriesLoaderSupport.classLoader, FactoriesLoaderSupport.CLI_FACTORIES_RESOURCE_LOCATION)
+ project.afterEvaluate {
+ FileCollection fileCollection = ClasspathUtils.buildClasspath(project, project.configurations.runtimeClasspath, project.configurations.grailsCliClasspath)
+ // commands come from the buildscript classloader (legacy placement) and from the
+ // project's cli tier, so auto-provisioned command artifacts register their tasks
+ // without any buildscript classpath entry
+ Set<String> commandClassNames = new LinkedHashSet<String>()
+ if (applicationContextCommands) {
+ commandClassNames.addAll(applicationContextCommands as List)
+ }
+ GrailsExtension grails = project.extensions.getByType(GrailsExtension)
+ if (grails.cliAutoProvision.get()) {
+ commandClassNames.addAll(loadCommandNamesFromCliClasspath(project))
+ }
+ for (ctxCommand in commandClassNames) {
+ String taskName = GrailsNameUtils.getLogicalPropertyName(ctxCommand, 'Command')
+ String commandName = GrailsNameUtils.getScriptName(GrailsNameUtils.getLogicalName(ctxCommand, 'Command'))
+ if (!project.tasks.names.contains(taskName)) {
+ project.tasks.register(taskName, ApplicationContextCommandTask).configure {
+ it.classpath = fileCollection
+ it.command = commandName
+ it.systemProperty(Environment.KEY, System.getProperty(Environment.KEY, Environment.DEVELOPMENT.getName()))
+ List<Object> args = []
+ def otherArgs = project.findProperty('args')
+ if (otherArgs) {
+ args.addAll(CommandLineParser.translateCommandline(otherArgs as String))
+ }
+
+ def appClassProvider = GrailsGradlePlugin.getMainClassProvider(project)
+
+ it.doFirst {
+ args << appClassProvider.get()
+ it.args(args)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads command class names from the {@code META-INF/grails-cli.factories} files of the
+ * resolved {@code grailsCliClasspath} jars, so per-command Gradle tasks (e.g. {@code dbmUpdate})
+ * are registered for auto-provisioned cli artifacts. Resolution is lenient and jars that are
+ * not built yet (project dependencies within a composite) are skipped — the generic
+ * {@code runCommand} task can always execute those commands regardless.
+ */
+ @CompileDynamic
+ protected Collection<String> loadCommandNamesFromCliClasspath(Project project) {
+ Set<String> names = new LinkedHashSet<String>()
+ Configuration cliClasspath = project.configurations.findByName(GRAILS_CLI_CLASSPATH_CONFIGURATION)
+ if (cliClasspath == null) {
+ return names
+ }
+ for (File file : cliClasspath.incoming.artifactView { it.lenient(true) }.files) {
Review Comment:
I'm not sure how this would work since dependencies can't be added to in an afterEvaluate. For now, this is going to be a feature and I'll move forward with the warning only.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4930618930
This is now fixed. We have to support cases where the library is a regular library imported by another project that uses the requireCapbility - this is only when the projects are defined in the same build. I've created a dedicated plugin for this scenario.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3557184858
##########
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
+ for (ResolvedArtifactResult artifact : lenientArtifacts.artifacts) {
+ String companion = findAdvertisedCliArtifact(project, artifact)
+ if (companion) {
+ companions.add(companion)
+ }
+ }
+
+ for (String companion : companions) {
+ List<String> coordinate = companion.tokenize(':')
+ if (coordinate.size() != 2) {
+ project.logger.warn('Ignoring malformed Grails-Cli-Artifact value [{}] found in the dependencies of project {}', companion, project.name)
+ continue
+ }
+ boolean alreadyDeclared = dependencies.any { Dependency existing ->
+ existing.group == coordinate[0] && existing.name == coordinate[1]
+ }
+ if (!alreadyDeclared) {
+ project.logger.info('Detected cli companion artifact {}, adding it to the {} configuration of project {}',
+ companion, GRAILS_CLI_CONFIGURATION, project.name)
+ dependencies.add(project.dependencies.create(companion))
+ }
+ }
+ }
+
+ /**
+ * Returns the companion cli coordinate ({@code group:artifactId}) advertised by the given
+ * resolved artifact, or {@code null}. For artifacts produced by a project of the same build
+ * the (possibly not yet built) jar is not read — the coordinate comes from the project's
+ * {@code cliArtifactId} property exported by the cli-artifact convention.
+ */
+ @CompileDynamic
+ protected String findAdvertisedCliArtifact(Project project, ResolvedArtifactResult artifact) {
+ def componentIdentifier = artifact.id.componentIdentifier
+ if (componentIdentifier instanceof ProjectComponentIdentifier) {
+ Project target = project.rootProject.findProject(((ProjectComponentIdentifier) componentIdentifier).projectPath)
+ def cliArtifactId = target?.findProperty('cliArtifactId')
+ return cliArtifactId ? "${target.group}:${cliArtifactId}" as String : null
Review Comment:
**Blocking:** companions advertised by a same-build project dependency are wired as unversioned *external module* dependencies, so auto-discovery is effectively broken for the plugin-development workflow.
Both branches of `findAdvertisedCliArtifact` return a bare `group:artifactId` string, and `autoProvisionCliDependencies` always adds it via `dependencies.create(companion)`. For a `ProjectComponentIdentifier` result this means Gradle tries to resolve the companion from a repository instead of binding to the sibling project's `cli` feature variant. Concretely: a multi-project build with `include 'app', 'my-plugin'` where `my-plugin` applies `grails-plugin-cli` and is unpublished — resolving `app`'s classpath fails (or silently picks up a stale previously-published artifact of the same coordinate).
The framework's own build corroborates the gap: root `build.gradle` disables auto-provisioning for every framework module, and `gradle/functional-test-config.gradle` adds a bespoke `dependencySubstitution` rule that performs exactly the project+capability wiring this method should produce itself.
Composite builds (`includeBuild`) hit a related edge: the included build's component is a `ProjectComponentIdentifier` that `project.rootProject.findProject(projectPath)` either fails to resolve (companion silently skipped) or resolves to a same-named project in the *wrong* build.
Suggested fix: when the component is a project of the current build, add `project.dependencies.project(path)` with `capabilities { requireCapability(...) }`; keep the coordinate path only for `ModuleComponentIdentifier` results. Alternatively, if in-build discovery is intentionally out of scope, document that and point at a substitution recipe.
##########
grails-bom/base/build.gradle:
##########
@@ -97,6 +97,23 @@ dependencies {
}
}
+// Companion cli artifacts (published by the cli-artifact convention plugin) are additional
+// publications of existing projects, so the subproject enumeration above cannot see them. Each
+// applying project exports its companion coordinate via the `cliArtifactId` extra property, which
+// only exists once that project has been evaluated — so the constraints are computed lazily, in
+// the mutation window Gradle provides right before the configuration is first observed.
+configurations.named('api').configure { apiConfiguration ->
+ apiConfiguration.withDependencies {
+ for (Project subproject : rootProject.subprojects) {
+ def cliArtifactId = subproject.findProperty('cliArtifactId')
+ if (cliArtifactId) {
+ apiConfiguration.dependencyConstraints.add(
+ project.dependencies.constraints.create("${subproject.group}:${cliArtifactId}:${projectVersion}"))
Review Comment:
Related to the `grails-publish` SNAPSHOT in `dependencies.gradle`: note that `validateNoSnapshotDependencies` (below in this file) only iterates `config.allDependencies` — constraints created here via `dependencies.constraints.create(...)` (and the `constraints { api ... }` block above) live in `allDependencyConstraints`, so the release guard has a blind spot for exactly this kind of leak and would let a SNAPSHOT constraint through a release build unflagged. Worth extending the guard to scan constraints as part of this PR, since the new lazy constraint mechanism widens that surface.
##########
grails-core/src/test/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformationSpec.groovy:
##########
@@ -0,0 +1,45 @@
+/*
+ * 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.compiler
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+/**
+ * Commands shipped in a companion cli artifact compile from the {@code cli} source set
+ * ({@code src/cli/groovy}), which is not a standard project-source location — the transformation
+ * must still register them in {@code META-INF/grails-cli.factories}.
+ */
+class CommandFactoriesTransformationSpec extends Specification {
Review Comment:
This spec only exercises the `isCliSource(URL)` helper. Nothing drives the transformation end-to-end: compiling a direct `ApplicationCommand` implementor, an indirect one via the `GrailsApplicationCommand` trait, and an abstract base class (which must be excluded), then asserting what lands in `META-INF/grails-cli.factories`. `ApplicationContextCommandRegistry` also has no spec pinning the intentional clean-break behavior — that a stale `ApplicationCommand` entry left in a legacy jar's `grails.factories` is silently ignored rather than loaded or erroring. Since the clean break is the headline breaking change, it deserves a test that locks it in.
##########
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
+ for (ResolvedArtifactResult artifact : lenientArtifacts.artifacts) {
+ String companion = findAdvertisedCliArtifact(project, artifact)
+ if (companion) {
+ companions.add(companion)
+ }
+ }
+
+ for (String companion : companions) {
+ List<String> coordinate = companion.tokenize(':')
+ if (coordinate.size() != 2) {
+ project.logger.warn('Ignoring malformed Grails-Cli-Artifact value [{}] found in the dependencies of project {}', companion, project.name)
+ continue
+ }
+ boolean alreadyDeclared = dependencies.any { Dependency existing ->
+ existing.group == coordinate[0] && existing.name == coordinate[1]
+ }
+ if (!alreadyDeclared) {
+ project.logger.info('Detected cli companion artifact {}, adding it to the {} configuration of project {}',
+ companion, GRAILS_CLI_CONFIGURATION, project.name)
+ dependencies.add(project.dependencies.create(companion))
+ }
+ }
+ }
+
+ /**
+ * Returns the companion cli coordinate ({@code group:artifactId}) advertised by the given
+ * resolved artifact, or {@code null}. For artifacts produced by a project of the same build
+ * the (possibly not yet built) jar is not read — the coordinate comes from the project's
+ * {@code cliArtifactId} property exported by the cli-artifact convention.
+ */
+ @CompileDynamic
+ protected String findAdvertisedCliArtifact(Project project, ResolvedArtifactResult artifact) {
+ def componentIdentifier = artifact.id.componentIdentifier
+ if (componentIdentifier instanceof ProjectComponentIdentifier) {
+ Project target = project.rootProject.findProject(((ProjectComponentIdentifier) componentIdentifier).projectPath)
+ def cliArtifactId = target?.findProperty('cliArtifactId')
+ return cliArtifactId ? "${target.group}:${cliArtifactId}" as String : null
+ }
+
+ File file = artifact.file
+ if (file == null || !file.isFile() || !file.name.endsWith('.jar')) {
+ return null
+ }
+ try (JarFile jarFile = new JarFile(file)) {
+ return jarFile.manifest?.mainAttributes?.getValue('Grails-Cli-Artifact')
+ }
+ catch (IOException ignored) {
+ return null
+ }
+ }
+
+ @CompileDynamic
+ protected void configureApplicationCommands(Project project) {
+ def applicationContextCommands = FactoriesLoaderSupport.loadFactoryNames(APPLICATION_CONTEXT_COMMAND_CLASS,
+ FactoriesLoaderSupport.classLoader, FactoriesLoaderSupport.CLI_FACTORIES_RESOURCE_LOCATION)
+ project.afterEvaluate {
+ FileCollection fileCollection = ClasspathUtils.buildClasspath(project, project.configurations.runtimeClasspath, project.configurations.grailsCliClasspath)
+ // commands come from the buildscript classloader (legacy placement) and from the
+ // project's cli tier, so auto-provisioned command artifacts register their tasks
+ // without any buildscript classpath entry
+ Set<String> commandClassNames = new LinkedHashSet<String>()
+ if (applicationContextCommands) {
+ commandClassNames.addAll(applicationContextCommands as List)
+ }
+ GrailsExtension grails = project.extensions.getByType(GrailsExtension)
+ if (grails.cliAutoProvision.get()) {
+ commandClassNames.addAll(loadCommandNamesFromCliClasspath(project))
+ }
+ for (ctxCommand in commandClassNames) {
+ String taskName = GrailsNameUtils.getLogicalPropertyName(ctxCommand, 'Command')
+ String commandName = GrailsNameUtils.getScriptName(GrailsNameUtils.getLogicalName(ctxCommand, 'Command'))
+ if (!project.tasks.names.contains(taskName)) {
+ project.tasks.register(taskName, ApplicationContextCommandTask).configure {
+ it.classpath = fileCollection
+ it.command = commandName
+ it.systemProperty(Environment.KEY, System.getProperty(Environment.KEY, Environment.DEVELOPMENT.getName()))
+ List<Object> args = []
+ def otherArgs = project.findProperty('args')
+ if (otherArgs) {
+ args.addAll(CommandLineParser.translateCommandline(otherArgs as String))
+ }
+
+ def appClassProvider = GrailsGradlePlugin.getMainClassProvider(project)
+
+ it.doFirst {
+ args << appClassProvider.get()
+ it.args(args)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads command class names from the {@code META-INF/grails-cli.factories} files of the
+ * resolved {@code grailsCliClasspath} jars, so per-command Gradle tasks (e.g. {@code dbmUpdate})
+ * are registered for auto-provisioned cli artifacts. Resolution is lenient and jars that are
+ * not built yet (project dependencies within a composite) are skipped — the generic
+ * {@code runCommand} task can always execute those commands regardless.
+ */
+ @CompileDynamic
+ protected Collection<String> loadCommandNamesFromCliClasspath(Project project) {
+ Set<String> names = new LinkedHashSet<String>()
+ Configuration cliClasspath = project.configurations.findByName(GRAILS_CLI_CLASSPATH_CONFIGURATION)
+ if (cliClasspath == null) {
+ return names
+ }
+ for (File file : cliClasspath.incoming.artifactView { it.lenient(true) }.files) {
Review Comment:
This forces full resolution of `grailsCliClasspath` (and, transitively, the `grailsCliDetect` probe via `withDependencies`) inside `project.afterEvaluate` — i.e. at configuration time, on every invocation. `./gradlew help` or an unrelated test run pays for resolving `grails-core-cli`, `grails-console`, and every discovered companion, and an offline/registry failure silently drops per-command task registration rather than failing loudly. It is also likely hostile to the configuration cache.
I realize task names must be known at configuration time to `tasks.register(...)`, so some of this is a genuine design tension — but the resolution result should at least be cached/scoped so it is not repeated for builds that never touch a CLI task, and the lenient-swallow failure mode deserves a warning log at minimum.
##########
grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/commands/CliCompanionPublishingSpec.groovy:
##########
@@ -0,0 +1,83 @@
+/*
+ * 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 groovy.json.JsonSlurper
+
+import org.grails.gradle.plugin.core.GradleSpecification
+
+/**
+ * Verifies that cli-tier dependencies declared with a capability
+ * ({@code project(':owner') { capabilities { requireCapability('g:owner-cli') } }}) are published
+ * as plain dependencies on the companion coordinate ({@code owner-cli}) in both the pom and the
+ * Gradle Module Metadata, and that the published artifacts resolve from a Maven repository.
+ *
+ * A published capability request cannot be resolved against the published component tree: the
+ * owner's cli variant and its {@code available-at} redirect target both provide the capability,
+ * so consumers fail with a capability self-conflict (the grails-forge generated-app failure).
+ */
+class CliCompanionPublishingSpec extends GradleSpecification {
Review Comment:
The publishing side is well covered here (publish → parse `.module`/`.pom` → resolve from a fresh repo — nice), but the discovery/wiring half of the feature has no coverage at all: nothing tests `autoProvisionCliDependencies`/`findAdvertisedCliArtifact`, the guarantee that `grailsCli` never leaks into `runtimeClasspath`/`bootJar`/`bootWar`, per-command task registration, or the `cliAutoProvision` opt-out.
That gap is not hypothetical — the one scenario that would have caught the project-dependency wiring bug (an app resolving a companion advertised by a project dependency, with `cliAutoProvision` at its default) is never exercised anywhere; the framework build globally disables the feature for its own modules. A functional test with an in-build plugin + consuming app would cover both the bug and the regression.
##########
dependencies.gradle:
##########
@@ -30,7 +30,7 @@ ext {
'directory-watcher.version' : '0.19.1',
'gradle-groovy.version' : '4.0.32',
'gradle-spock.version' : '2.4-groovy-4.0',
- 'grails-publish-plugin.version' : '1.0.0-M1',
+ 'grails-publish-plugin.version' : '1.0.0-SNAPSHOT',
Review Comment:
**Blocking for release:** this `1.0.0-SNAPSHOT` flows into `gradleBomDependencies` and from there into the `constraints { api ... }` block of `grails-bom/base`, i.e. into the published `<dependencyManagement>` of `org.apache.grails:grails-bom` that every application consumes. A staged 8.0.x release would ship a BOM recommending a SNAPSHOT of `org.apache.grails.gradle:grails-publish`.
I understand this is pending apache/grails-gradle-publish#34 shipping — but the PR should be explicitly marked blocked on that release, with the version reverted to a GA coordinate before merge (or at latest before the first 8.0.x release candidate).
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/commands/GrailsCliArtifactGradlePlugin.groovy:
##########
@@ -0,0 +1,216 @@
+/*
+ * 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 groovy.transform.CompileStatic
+
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.PublishArtifact
+import org.gradle.api.component.AdhocComponentWithVariants
+import org.gradle.api.component.ConfigurationVariantDetails
+import org.gradle.api.component.SoftwareComponentFactory
+import org.gradle.api.tasks.SourceSet
+import org.gradle.api.tasks.SourceSetContainer
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.api.plugins.JavaPluginExtension
+
+import javax.inject.Inject
+
+import org.apache.grails.gradle.publish.AdditionalPublication
+import org.apache.grails.gradle.publish.GrailsPublishExtension
+
+/**
+ * Configures a Grails plugin project to ship its CLI commands as a companion artifact under its
+ * own Maven coordinate ({@code <artifactId>-cli}), keeping command code and CLI-only dependencies
+ * off the runtime classpath of consuming applications.
+ *
+ * Applying the plugin:
+ * <ul>
+ * <li>creates a {@code cli} source set ({@code src/cli/groovy}) exposed as a feature variant;</li>
+ * <li>adds {@code org.apache.grails:grails-core-cli} (the command contract) and the plugin's own
+ * runtime classes to the cli compile classpath, so commands compile with no additional
+ * configuration and are registered automatically in {@code META-INF/grails-cli.factories};</li>
+ * <li>keeps the cli variants out of the plugin's default publication and exposes them through a
+ * dedicated {@code cli} software component;</li>
+ * <li>stamps the runtime jar with the {@code Grails-Cli-Artifact} manifest attribute, so the
+ * Grails Gradle plugin adds the companion to a consuming application's {@code grailsCli}
+ * configuration automatically;</li>
+ * <li>registers the companion publication with the Grails publish plugin
+ * ({@code org.apache.grails.gradle.grails-publish}) when it is applied.</li>
+ * </ul>
+ *
+ * Configure through the {@code cliArtifact} extension:
+ * <pre>
+ * cliArtifact {
+ * automaticModuleName = 'com.example.myplugin.cli'
+ * }
+ * </pre>
+ *
+ * @since 8.0
+ */
+@CompileStatic
+abstract class GrailsCliArtifactGradlePlugin implements Plugin<Project> {
+
+ public static final String CLI_SOURCE_SET_NAME = 'cli'
+ public static final String CLI_COMPONENT_NAME = 'cli'
+ public static final String CLI_PUBLICATION_NAME = 'cli'
+
+ /** The runtime-jar manifest attribute advertising the companion cli coordinate */
+ public static final String CLI_ARTIFACT_MANIFEST_ATTRIBUTE = 'Grails-Cli-Artifact'
+
+ @Inject
+ abstract SoftwareComponentFactory getSoftwareComponentFactory()
+
+ @Override
+ void apply(Project project) {
+ if (!project.pluginManager.hasPlugin('java')) {
+ throw new GradleException("The Grails cli-artifact plugin requires the `java` plugin (or a plugin that applies it, e.g. the Grails plugin plugin) to be applied to project `${project.name}` first.")
+ }
+
+ CliArtifactExtension extension = project.extensions.create('cliArtifact', CliArtifactExtension)
+ extension.artifactId.convention(project.provider { "${project.name}-cli" as String })
+ extension.defaultDependencies.convention(true)
+
+ SourceSetContainer sourceSets = project.extensions.getByType(SourceSetContainer)
+ SourceSet cliSourceSet = sourceSets.create(CLI_SOURCE_SET_NAME)
+
+ JavaPluginExtension java = project.extensions.getByType(JavaPluginExtension)
+ java.registerFeature(CLI_SOURCE_SET_NAME) { spec ->
+ // no explicit capability: the default (`${group}:${project.name}-cli:${version}`)
Review Comment:
Latent contract mismatch: `registerFeature` here keeps the Gradle-computed default capability (`${group}:${project.name}-cli`), but `CliArtifactExtension.artifactId` is documented as customizable and renames the published jar and advertised coordinate. A consumer who sets `cliArtifact { artifactId = 'custom-name' }` and then requests `requireCapability("$group:custom-name")` in-build will fail to resolve, because the in-build capability never followed the extension.
Nothing in the framework customizes `artifactId` today, so this is unexercised — but either wire `spec.capability(...)` from the extension value or document the limitation on `CliArtifactExtension.artifactId`.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3565226328
##########
dependencies.gradle:
##########
@@ -30,7 +30,7 @@ ext {
'directory-watcher.version' : '0.19.1',
'gradle-groovy.version' : '4.0.32',
'gradle-spock.version' : '2.4-groovy-4.0',
- 'grails-publish-plugin.version' : '1.0.0-M1',
+ 'grails-publish-plugin.version' : '1.0.0-SNAPSHOT',
Review Comment:
@codeconsole in the past, we've merged these type of changes and then prior to shipping the next grails release, we stage and update grails as part of the release.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4929484743
Turns out there's a real build issue separate from this change, micronaut-singleton isn't including our functional test app so it was pulling from upstream instead of testing with the local copy.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5011951547
@jamesfredley Given that:
1. your time is limited right now to respond & this needs to make it into tomorrow's milestone stage
2. your comment wasn't a blocking change (requested changes)
3. all of the comments have been addressed except those that exist with the current code line (gradle task names / incremental compile)
4. this change doesn't require rebuilding every grails plugin (only if you want the commands, which would indicate active development)
5. there are multiple other approvals
6. we can always iterate on this design before a RC or the final release
I'm going to merge this after the tests finish. If you feel this needs reverted, let's revisit.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty merged PR #15948:
URL: https://github.com/apache/grails-core/pull/15948
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jamesfredley (via GitHub)" <gi...@apache.org>.
jamesfredley commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-5005936444
I am for this and the PR looks fine.
But I would want to know the clear path for end apps to quickly upgrade to Grails 8, which they will need to do, when we GA 8.0.0, due to Spring Boot EOL. Will they have to wait on common plugins to re-release and will that bottleneck upgrading to Grails 8.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3608658628
##########
grails-core/src/main/groovy/org/grails/compiler/injection/FactoriesFileWriter.groovy:
##########
@@ -0,0 +1,136 @@
+/*
+ * 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.compiler.injection
+
+import java.lang.reflect.Modifier
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.ClassNode
+
+import org.springframework.core.CollectionFactory
+
+import org.apache.grails.gradle.common.PropertyFileUtils
+
+/**
+ * Writes factory registrations for compiled classes into a factories file in the compilation
+ * target directory, merging any existing entries from previous compilation runs and from
+ * hand-authored source registrations. The factories file location is supplied by the caller,
+ * so the writer is shared by transformations targeting different registration files
+ * (e.g. {@code META-INF/grails.factories} and {@code META-INF/grails-cli.factories}).
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class FactoriesFileWriter {
+
+ /**
+ * Registers the class as an implementation of the given super type in the factories file
+ * when the class is a non-abstract subtype.
+ *
+ * @param classNode the compiled class
+ * @param superType the factory type to register the class under
+ * @param compilationTargetDirectory the compilation output directory
+ * @param factoriesLocation the factories file path relative to the target directory
+ * @param sourceFactoriesLocations project-relative paths of hand-authored factories files to merge
+ * @return {@code true} when the class was a subtype of the factory type
+ */
+ static boolean updateFactoriesWithType(ClassNode classNode, ClassNode superType, File compilationTargetDirectory,
+ String factoriesLocation, List<String> sourceFactoriesLocations) {
+ if (GrailsASTUtils.isSubclassOfOrImplementsInterface(classNode, superType)) {
+ if (Modifier.isAbstract(classNode.getModifiers())) {
+ return false
+ }
+
+ def classNodeName = classNode.name
+ // Use SortedProperties to ensure a consistent order of entries for reproducible builds
+ def props = CollectionFactory.createSortedProperties(false)
+ def superTypeName = superType.getName()
+
+ File factoriesFile = new File(compilationTargetDirectory, factoriesLocation)
+ if (!factoriesFile.parentFile.exists()) {
+ factoriesFile.parentFile.mkdirs()
+ }
+ loadFromFile(props, factoriesFile)
Review Comment:
This code was modeled off the existing factories writer. Yes, this is an issue but it's been a long standing issue that would require tracking the current class set. Incremental compile is considered an incubating feature of Gradle and not a default feature. This is something we should track separately from this change. I've opened https://github.com/apache/grails-core/issues/16007 for this.
##########
grails-core/src/main/groovy/org/grails/compiler/injection/FactoriesFileWriter.groovy:
##########
@@ -0,0 +1,136 @@
+/*
+ * 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.compiler.injection
+
+import java.lang.reflect.Modifier
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.ast.ClassNode
+
+import org.springframework.core.CollectionFactory
+
+import org.apache.grails.gradle.common.PropertyFileUtils
+
+/**
+ * Writes factory registrations for compiled classes into a factories file in the compilation
+ * target directory, merging any existing entries from previous compilation runs and from
+ * hand-authored source registrations. The factories file location is supplied by the caller,
+ * so the writer is shared by transformations targeting different registration files
+ * (e.g. {@code META-INF/grails.factories} and {@code META-INF/grails-cli.factories}).
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class FactoriesFileWriter {
+
+ /**
+ * Registers the class as an implementation of the given super type in the factories file
+ * when the class is a non-abstract subtype.
+ *
+ * @param classNode the compiled class
+ * @param superType the factory type to register the class under
+ * @param compilationTargetDirectory the compilation output directory
+ * @param factoriesLocation the factories file path relative to the target directory
+ * @param sourceFactoriesLocations project-relative paths of hand-authored factories files to merge
+ * @return {@code true} when the class was a subtype of the factory type
+ */
+ static boolean updateFactoriesWithType(ClassNode classNode, ClassNode superType, File compilationTargetDirectory,
+ String factoriesLocation, List<String> sourceFactoriesLocations) {
+ if (GrailsASTUtils.isSubclassOfOrImplementsInterface(classNode, superType)) {
+ if (Modifier.isAbstract(classNode.getModifiers())) {
+ return false
+ }
+
+ def classNodeName = classNode.name
+ // Use SortedProperties to ensure a consistent order of entries for reproducible builds
+ def props = CollectionFactory.createSortedProperties(false)
+ def superTypeName = superType.getName()
+
+ File factoriesFile = new File(compilationTargetDirectory, factoriesLocation)
+ if (!factoriesFile.parentFile.exists()) {
+ factoriesFile.parentFile.mkdirs()
+ }
+ loadFromFile(props, factoriesFile)
+
+ File sourceDirectory = findSourceDirectory(compilationTargetDirectory)
+ if (sourceDirectory != null) {
+ for (String sourceFactoriesLocation : sourceFactoriesLocations) {
+ File sourceFactoriesFile = new File(sourceDirectory, sourceFactoriesLocation)
+ loadFromFile(props, sourceFactoriesFile)
+ }
+ }
+
+ addToProps(props, superTypeName, classNodeName)
+
+ factoriesFile.withWriter { Writer writer ->
Review Comment:
This code was modeled off the existing factories writer. Yes, this is an issue but it's been a long standing issue that would require tracking the current class set. Incremental compile is considered an incubating feature of Gradle and not a default feature. This is something we should track separately from this change. I've opened https://github.com/apache/grails-core/issues/16007 for this.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#issuecomment-4980706420
fixes https://github.com/apache/grails-core/issues/15377
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "davydotcom (via GitHub)" <gi...@apache.org>.
davydotcom commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3604021785
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/commands/GrailsCliArtifactGradlePlugin.groovy:
##########
@@ -0,0 +1,216 @@
+/*
+ * 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 groovy.transform.CompileStatic
+
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.PublishArtifact
+import org.gradle.api.component.AdhocComponentWithVariants
+import org.gradle.api.component.ConfigurationVariantDetails
+import org.gradle.api.component.SoftwareComponentFactory
+import org.gradle.api.tasks.SourceSet
+import org.gradle.api.tasks.SourceSetContainer
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.api.plugins.JavaPluginExtension
+
+import javax.inject.Inject
+
+import org.apache.grails.gradle.publish.AdditionalPublication
+import org.apache.grails.gradle.publish.GrailsPublishExtension
+
+/**
+ * Configures a Grails plugin project to ship its CLI commands as a companion artifact under its
+ * own Maven coordinate ({@code <artifactId>-cli}), keeping command code and CLI-only dependencies
+ * off the runtime classpath of consuming applications.
+ *
+ * Applying the plugin:
+ * <ul>
+ * <li>creates a {@code cli} source set ({@code src/cli/groovy}) exposed as a feature variant;</li>
+ * <li>adds {@code org.apache.grails:grails-core-cli} (the command contract) and the plugin's own
+ * runtime classes to the cli compile classpath, so commands compile with no additional
+ * configuration and are registered automatically in {@code META-INF/grails-cli.factories};</li>
+ * <li>keeps the cli variants out of the plugin's default publication and exposes them through a
+ * dedicated {@code cli} software component;</li>
+ * <li>stamps the runtime jar with the {@code Grails-Cli-Artifact} manifest attribute, so the
+ * Grails Gradle plugin adds the companion to a consuming application's {@code grailsCli}
+ * configuration automatically;</li>
+ * <li>registers the companion publication with the Grails publish plugin
+ * ({@code org.apache.grails.gradle.grails-publish}) when it is applied.</li>
+ * </ul>
+ *
+ * Configure through the {@code cliArtifact} extension:
+ * <pre>
+ * cliArtifact {
+ * automaticModuleName = 'com.example.myplugin.cli'
+ * }
+ * </pre>
+ *
+ * @since 8.0
+ */
+@CompileStatic
+abstract class GrailsCliArtifactGradlePlugin implements Plugin<Project> {
+
+ public static final String CLI_SOURCE_SET_NAME = 'cli'
+ public static final String CLI_COMPONENT_NAME = 'cli'
+ public static final String CLI_PUBLICATION_NAME = 'cli'
+
+ /** The runtime-jar manifest attribute advertising the companion cli coordinate */
+ public static final String CLI_ARTIFACT_MANIFEST_ATTRIBUTE = 'Grails-Cli-Artifact'
+
+ @Inject
+ abstract SoftwareComponentFactory getSoftwareComponentFactory()
+
+ @Override
+ void apply(Project project) {
+ if (!project.pluginManager.hasPlugin('java')) {
+ throw new GradleException("The Grails cli-artifact plugin requires the `java` plugin (or a plugin that applies it, e.g. the Grails plugin plugin) to be applied to project `${project.name}` first.")
+ }
+
+ CliArtifactExtension extension = project.extensions.create('cliArtifact', CliArtifactExtension)
+ extension.artifactId.convention(project.provider { "${project.name}-cli" as String })
+ extension.defaultDependencies.convention(true)
+
+ SourceSetContainer sourceSets = project.extensions.getByType(SourceSetContainer)
+ SourceSet cliSourceSet = sourceSets.create(CLI_SOURCE_SET_NAME)
+
+ JavaPluginExtension java = project.extensions.getByType(JavaPluginExtension)
+ java.registerFeature(CLI_SOURCE_SET_NAME) { spec ->
+ // no explicit capability: the default (`${group}:${project.name}-cli:${version}`)
Review Comment:
minor, can be dealt wiith later.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "davydotcom (via GitHub)" <gi...@apache.org>.
davydotcom commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3604007401
##########
grails-core/src/test/groovy/org/apache/grails/core/cli/compiler/CommandFactoriesTransformationSpec.groovy:
##########
@@ -0,0 +1,45 @@
+/*
+ * 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.compiler
+
+import spock.lang.Specification
+import spock.lang.Unroll
+
+/**
+ * Commands shipped in a companion cli artifact compile from the {@code cli} source set
+ * ({@code src/cli/groovy}), which is not a standard project-source location — the transformation
+ * must still register them in {@code META-INF/grails-cli.factories}.
+ */
+class CommandFactoriesTransformationSpec extends Specification {
Review Comment:
seems like an edge case that isnt really relevant here yea?
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] Split CLI dependencies from the runtime classpath [grails-core]
Posted by "davydotcom (via GitHub)" <gi...@apache.org>.
davydotcom commented on code in PR #15948:
URL: https://github.com/apache/grails-core/pull/15948#discussion_r3604012883
##########
grails-bom/base/build.gradle:
##########
@@ -97,6 +97,23 @@ dependencies {
}
}
+// Companion cli artifacts (published by the cli-artifact convention plugin) are additional
+// publications of existing projects, so the subproject enumeration above cannot see them. Each
+// applying project exports its companion coordinate via the `cliArtifactId` extra property, which
+// only exists once that project has been evaluated — so the constraints are computed lazily, in
+// the mutation window Gradle provides right before the configuration is first observed.
+configurations.named('api').configure { apiConfiguration ->
+ apiConfiguration.withDependencies {
+ for (Project subproject : rootProject.subprojects) {
+ def cliArtifactId = subproject.findProperty('cliArtifactId')
+ if (cliArtifactId) {
+ apiConfiguration.dependencyConstraints.add(
+ project.dependencies.constraints.create("${subproject.group}:${cliArtifactId}:${projectVersion}"))
Review Comment:
out of scope comment.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org