jamesfredley commented on code in PR #15467:
URL: https://github.com/apache/grails-core/pull/15467#discussion_r3356426663
##########
build-logic/docs-core/build.gradle:
##########
@@ -48,7 +48,6 @@ dependencies {
api 'org.yaml:snakeyaml:2.4'
api
"org.asciidoctor:asciidoctorj:${gradleBomDependencyVersions['asciidoctorj.version']}"
- implementation
"org.springframework.boot:spring-boot-gradle-plugin:${gradleBomDependencyVersions['spring-boot.version']}"
Review Comment:
Added in 59dcb7e - `grails-test-examples/spring-dependency-management`, a
Grails app that applies `io.spring.dependency-management` by hand and imports
`grails-bom`. Its integration test boots the app and serves a request.
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomManagedVersions.groovy:
##########
@@ -0,0 +1,504 @@
+/*
+ * 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.bom
+
+import java.util.function.Function
+
+import groovy.transform.CompileStatic
+import org.gradle.api.Project
+import org.gradle.api.artifacts.ConfigurationContainer
+import org.gradle.api.artifacts.dsl.DependencyHandler
+import org.gradle.api.logging.Logger
+import org.gradle.api.logging.Logging
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+
+import javax.xml.parsers.DocumentBuilderFactory
+
+/**
+ * Lightweight replacement for the Spring Dependency Management plugin's
+ * version property override feature.
+ *
+ * <p>Parses BOM POM files to determine the version every managed artifact
+ * resolves to, both with the BOM's default {@code <properties>} values and
+ * with the project's overrides applied (via {@code ext['property.name']} in
+ * {@code build.gradle} or via {@code gradle.properties}). Any artifact whose
+ * effective version differs from the BOM default becomes a version
override.</p>
+ *
+ * <p>Overrides are applied as <strong>strict</strong> dependency constraints
+ * (see {@link #applyTo(DependencyHandler, String)}). A strict constraint wins
+ * over the {@code require} constraints contributed by Gradle's native
+ * {@code platform()} mechanism, so an override is honored even when it
+ * <em>downgrades</em> a managed version - a plain
+ * {@code ResolutionStrategy.eachDependency()} / {@code useVersion()} hook
+ * would lose to the platform's higher version during conflict resolution.</p>
+ *
+ * <p>Because the effective version is computed by re-resolving imported
+ * ({@code <scope>import</scope>}) BOMs with the project's property overrides
+ * applied, overriding a property that selects an imported BOM's version
+ * (for example {@code spring-boot.version}) re-imports that BOM and pulls in
+ * its updated managed-dependency set.</p>
+ *
+ * <p>Gradle's native {@code platform()} mechanism handles the base BOM import
+ * and default version management. This class only adds the one feature Gradle
+ * lacks: property-based version customization
+ * (see <a href="https://github.com/gradle/gradle/issues/9160">Gradle
#9160</a>).</p>
+ *
+ * <p>This is the underlying utility used by the
+ * {@code org.apache.grails.gradle.bom-property-overrides} plugin
+ * (registered in {@code grails-gradle-plugins}). It is BOM-agnostic and
+ * can be used directly with any BOM that follows the Maven
+ * {@code <properties>} convention for managed versions.</p>
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class BomManagedVersions {
+
+ private static final Logger LOG = Logging.getLogger(BomManagedVersions)
+ private static final int MAX_PROPERTY_INTERPOLATION_DEPTH = 10
+
+ /** A property resolver that never overrides anything (BOM defaults only).
*/
+ private static final Function<String, String> NO_OVERRIDES = { String name
-> null } as Function<String, String>
+
+ private final Map<String, String> versionOverrides = new LinkedHashMap<>()
+
+ /**
+ * Resolves a single BOM via captured Gradle services rather than a
+ * {@link Project} reference. Preferred for config-cache discipline:
+ * callers capture services once (typically inside a single
+ * {@code afterEvaluate} block) and never leak a {@link Project}
+ * reference into the override map that lives on past configuration time.
+ *
+ * @param configurations the project's configuration container, captured
at apply/afterEvaluate time
+ * @param dependencies the project's dependency handler, captured at
apply/afterEvaluate time
+ * @param propertyLookup function returning the project's property value
as a String, or {@code null} if unset
+ * @param bomCoordinates the BOM coordinates in {@code
group:artifact:version} format
+ * @return a BomManagedVersions instance containing any version overrides
to apply
+ */
+ static BomManagedVersions resolve(ConfigurationContainer configurations,
+ DependencyHandler dependencies,
+ Function<String, String> propertyLookup,
+ String bomCoordinates) {
+ resolve(configurations, dependencies, propertyLookup, [bomCoordinates])
+ }
+
+ /**
+ * Resolves multiple BOMs via captured Gradle services. The result is a
+ * plain data carrier (a {@code Map<String, String>} of version overrides
+ * inside {@link BomManagedVersions}) that holds no {@link Project}
+ * reference, so it can be safely captured by per-configuration
+ * constraint declarations and survive configuration-cache serialization.
+ *
+ * <p>The override set is computed as the difference between two
resolutions
+ * of the BOM tree: one using the BOM's default property values, and one
+ * using the project's property overrides. Any managed artifact whose
+ * effective version differs from its default version is recorded as an
+ * override.</p>
+ *
+ * @param configurations the project's configuration container, captured
at apply/afterEvaluate time
+ * @param dependencies the project's dependency handler, captured at
apply/afterEvaluate time
+ * @param propertyLookup function returning the project's property value
as a String, or {@code null} if unset
+ * @param bomCoordinatesList list of BOM coordinates in {@code
group:artifact:version} format
+ * @return a BomManagedVersions instance containing any version overrides
to apply
+ */
+ static BomManagedVersions resolve(ConfigurationContainer configurations,
+ DependencyHandler dependencies,
+ Function<String, String> propertyLookup,
+ Collection<String> bomCoordinatesList) {
+ def instance = new BomManagedVersions()
+
+ def defaultVersions = computeManagedVersions(
+ configurations, dependencies, bomCoordinatesList, NO_OVERRIDES)
+ def effectiveVersions = computeManagedVersions(
+ configurations, dependencies, bomCoordinatesList,
propertyLookup)
+
+ for (def entry : effectiveVersions.entrySet()) {
+ def artifactKey = entry.key
+ def effectiveVersion = entry.value
+ def defaultVersion = defaultVersions.get(artifactKey)
+
+ if (effectiveVersion != null && effectiveVersion !=
defaultVersion) {
+ instance.versionOverrides.put(artifactKey, effectiveVersion)
+ LOG.info(
+ 'BOM version override: {} = {} (BOM default: {})',
+ artifactKey, effectiveVersion, defaultVersion ?: 'unknown'
+ )
+ }
+ }
+
+ if (!instance.versionOverrides.isEmpty()) {
+ LOG.lifecycle(
+ 'BOM property overrides: {} version override(s) will be
applied',
+ instance.versionOverrides.size()
+ )
+ }
+
+ instance
+ }
+
+ /**
+ * Convenience overload that captures services from the given {@link
Project}.
+ * Production callers should prefer the services-based overload above so
the
+ * resolve path never sees a {@link Project} reference. This overload is
+ * primarily useful for tests and ad-hoc usage.
+ *
+ * @param project the Gradle project (services are extracted at call time)
+ * @param bomCoordinates the BOM coordinates in {@code
group:artifact:version} format
+ */
+ static BomManagedVersions resolve(Project project, String bomCoordinates) {
+ resolve(project, [bomCoordinates])
+ }
+
+ /**
+ * Convenience overload that captures services from the given {@link
Project}.
+ * Production callers should prefer the services-based overload above so
the
+ * resolve path never sees a {@link Project} reference. This overload is
+ * primarily useful for tests and ad-hoc usage.
+ *
+ * @param project the Gradle project (services are extracted at call time)
+ * @param bomCoordinatesList list of BOM coordinates in {@code
group:artifact:version} format
+ */
+ static BomManagedVersions resolve(Project project, Collection<String>
bomCoordinatesList) {
+ resolve(
+ project.configurations,
+ project.dependencies,
+ { String name -> project.hasProperty(name) ?
project.property(name)?.toString() : null } as Function<String, String>,
+ bomCoordinatesList
+ )
+ }
+
+ /**
+ * Applies the detected version overrides to the given configuration as
+ * strict dependency constraints.
+ *
+ * <p>Strict constraints are used deliberately: a plain {@code platform()}
+ * contributes {@code require} constraints, and a soft override (e.g.
+ * {@code useVersion()}) would lose to a higher {@code require} version
+ * during conflict resolution. A strict constraint overrides {@code
require},
+ * so the project's chosen version wins in both directions (upgrade and
+ * downgrade).</p>
+ *
+ * @param dependencies the project's dependency handler
+ * @param configurationName the name of the configuration to add
constraints to
+ */
+ void applyTo(DependencyHandler dependencies, String configurationName) {
+ if (versionOverrides.isEmpty()) {
+ return
+ }
+
+ versionOverrides.each { String coordinate, String version ->
+ dependencies.constraints.add(configurationName, coordinate) {
+ it.version { it.strictly(version) }
+ it.because('BOM version override via project property')
+ }
+ }
+ }
+
+ /**
+ * Returns whether any version overrides were detected.
+ */
+ boolean hasOverrides() {
+ !versionOverrides.isEmpty()
+ }
+
+ /**
+ * Returns an unmodifiable view of the version overrides.
+ * Keys are {@code group:artifact}, values are the override version
strings.
+ */
+ Map<String, String> getOverrides() {
+ Collections.unmodifiableMap(versionOverrides)
+ }
+
+ /**
+ * Parses a BOM POM file and extracts the property-to-artifact mapping.
+ * This method does not follow imported BOMs recursively - it only
processes
+ * the given file. Intended for testing and direct POM inspection.
+ *
+ * @param pomFile the BOM POM file to parse
+ * @param bomProperties output map to receive property name to default
value mappings
+ * @param propertyToArtifacts output map to receive property name to
artifact coordinate mappings
+ */
+ static void parseBomFile(File pomFile, Map<String, String> bomProperties,
Map<String, List<String>> propertyToArtifacts) {
+ def doc = parseXml(pomFile)
Review Comment:
Done in bc933fa. `BomManagedVersions` now parses POMs with
`org.apache.maven:maven-model` (`MavenXpp3Reader` into a `Model`), matching
`ExtractDependenciesTask`.
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy:
##########
@@ -121,6 +146,32 @@ class GrailsExtension {
*/
final Property<Boolean> preserveParameterNames
+ /**
+ * Whether the Grails Gradle plugin should automatically apply the {@code
grails-bom}
+ * as a Gradle {@code platform()} on every declarable project configuration
+ * (and apply the {@code org.apache.grails.gradle.bom-property-overrides}
plugin
+ * for property-based version overrides).
+ *
+ * <p>Defaults to {@code true}, which matches the behaviour of every
Grails 7 release:
+ * the BOM is always applied so that the framework's curated
managed-dependency set
+ * is the source of truth for the application.</p>
+ *
+ * <p>Disable this only when you intentionally want to manage Grails
dependencies
+ * yourself - for example, when consuming Grails modules from a different
curated
+ * platform and you need to declare the BOM by hand (and apply
+ * {@code org.apache.grails.gradle.bom-property-overrides} explicitly if
you still
+ * want {@code gradle.properties} / {@code ext['...']} overrides).</p>
+ *
+ * <pre>
+ * grails {
+ * autoApplyBom = false
Review Comment:
Addressed in 6ef4ec2. The hardcoded `grails-bom` assumption is gone -
`grails { bom = ... }` selects the variant (e.g. `grails-hibernate5-bom`,
`grails-micronaut-bom`), defaulting to `grails-bom`.
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -367,36 +366,111 @@ ${importStatements}
protected void applyDefaultPlugins(Project project) {
applySpringBootPlugin(project)
+ applyGrailsBom(project)
+ }
+ /**
+ * Applies the Grails BOM as a Gradle platform and enables property-based
+ * version overrides via the standalone
+ * {@code org.apache.grails.gradle.bom-property-overrides} plugin.
+ *
+ * <p>This replaces the Spring Dependency Management plugin with two
+ * orthogonal pieces:</p>
+ * <ol>
+ * <li><strong>BOM import</strong>: {@code grails-bom} is added as a
+ * Gradle {@code platform()} dependency on every declarable
+ * configuration, mirroring the global behaviour Spring DM provided
+ * via {@code configurations.all() +
resolutionStrategy.eachDependency()}.</li>
+ * <li><strong>Property overrides</strong>: the BOM-agnostic
+ * {@link BomPropertyOverridesPlugin} reads the BOM's
+ * {@code <properties>} block and applies any project-level
+ * overrides via Gradle's
+ * {@code ResolutionStrategy.eachDependency()}.</li>
+ * </ol>
+ *
+ * <p>Usage: to override a version managed by the Grails or Spring Boot
BOM, set the
+ * corresponding property in {@code gradle.properties} or {@code
build.gradle}:</p>
+ * <pre>
+ * // gradle.properties
+ * slf4j.version=1.7.36
+ *
+ * // or build.gradle
+ * ext['slf4j.version'] = '1.7.36'
+ * </pre>
+ *
+ * @see BomPropertyOverridesPlugin
+ * @since 8.0
+ */
+ protected void applyGrailsBom(Project project) {
+ // Ensure the developmentOnly configuration exists. Spring Boot's
plugin
+ // normally creates this, but using maybeCreate guarantees it is
available
+ // even if plugin ordering changes or Spring Boot is not applied. We do
+ // this outside afterEvaluate so that other plugins applied during the
+ // same configuration phase can rely on the configuration existing.
+ project.configurations.maybeCreate('developmentOnly')
+
+ // The opt-out flag `grails { autoApplyBom = false }` is set in the
user's
+ // build.gradle, which runs AFTER plugin apply. We therefore wait until
+ // afterEvaluate to read the flag and apply the BOM accordingly. By
that
+ // point all declarable configurations exist (java-base creates them
+ // during apply), so iterating them eagerly via .each is sufficient -
+ // any plugin that adds a configuration later is responsible for
+ // declaring its own BOM coordination if it needs it.
project.afterEvaluate {
- GrailsExtension ge = project.extensions.getByType(GrailsExtension)
- if (ge.springDependencyManagement) {
- Plugin dependencyManagementPlugin =
project.plugins.findPlugin(DependencyManagementPlugin)
- if (dependencyManagementPlugin == null) {
- project.plugins.apply(DependencyManagementPlugin)
- }
-
- DependencyManagementExtension dme =
project.extensions.findByType(DependencyManagementExtension)
+ def grailsExtension =
project.extensions.findByType(GrailsExtension)
+ boolean autoApply = grailsExtension == null ||
grailsExtension.autoApplyBom.getOrElse(true)
+ if (!autoApply) {
+ project.logger.info(
+ 'grails.autoApplyBom is false; skipping automatic
application of platform(grails-bom) and bom-property-overrides plugin for
project {}',
+ project.path
+ )
+ return
+ }
- applyBomImport(dme, project)
+ def grailsVersion = (project.findProperty('grailsVersion') ?:
BuildSettings.grailsVersion) as String
+ def bomCoordinates =
"org.apache.grails:grails-bom:${grailsVersion}" as String
Review Comment:
Done in 6ef4ec2 - replaced the `autoApplyBom` boolean with a
`Property<String> bom` (the BOM artifact name, default `grails-bom`, `null` to
opt out), exactly as suggested. The plugin resolves it to
`org.apache.grails:$bom:$grailsVersion`.
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/bom/BomManagedVersions.groovy:
##########
@@ -0,0 +1,378 @@
+/*
+ * 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.bom
+
+import groovy.transform.CompileStatic
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.DependencyResolveDetails
+import org.gradle.api.logging.Logger
+import org.gradle.api.logging.Logging
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+import org.w3c.dom.NodeList
+
+import javax.xml.parsers.DocumentBuilderFactory
+
+/**
+ * Lightweight replacement for the Spring Dependency Management plugin's
+ * version property override feature.
+ *
+ * <p>Parses BOM POM files to build a mapping of Maven property names
+ * (e.g., {@code slf4j.version}) to the artifacts they control. At
+ * dependency resolution time, checks whether the user has overridden
+ * any of these properties via {@code ext['property.name']} in
+ * {@code build.gradle} or via {@code gradle.properties}, and applies
+ * those overrides using Gradle's {@code
ResolutionStrategy.eachDependency()}.</p>
+ *
+ * <p>Gradle's native {@code platform()} mechanism handles the base
+ * BOM import and default version management. This class only adds the
+ * one feature Gradle lacks: property-based version customization
+ * (see <a href="https://github.com/gradle/gradle/issues/9160">Gradle
#9160</a>).</p>
+ *
+ * <p>This is the underlying utility used by the
+ * {@code org.apache.grails.gradle.bom-property-overrides} plugin. It is
+ * BOM-agnostic and can be used directly with any BOM that follows the
+ * Maven {@code <properties>} convention for managed versions.</p>
+ *
+ * @since 8.0
+ */
+@CompileStatic
+class BomManagedVersions {
+
+ private static final Logger LOG = Logging.getLogger(BomManagedVersions)
+ private static final int MAX_PROPERTY_INTERPOLATION_DEPTH = 10
+
+ private final Map<String, String> versionOverrides = new LinkedHashMap<>()
+
+ /**
+ * Resolves a BOM, parses its POM chain, and determines which managed
+ * dependency versions need to be overridden based on project properties.
+ *
+ * @param project the Gradle project (used for artifact resolution and
property lookup)
+ * @param bomCoordinates the BOM coordinates in {@code
group:artifact:version} format
+ * @return a BomManagedVersions instance containing any version overrides
to apply
+ */
+ static BomManagedVersions resolve(Project project, String bomCoordinates) {
+ return resolve(project, [bomCoordinates])
+ }
+
+ /**
+ * Resolves multiple BOMs, parses their POM chains, and determines which
+ * managed dependency versions need to be overridden based on project
+ * properties. Useful when a project applies several platforms (e.g., a
+ * Grails BOM plus a Micronaut BOM) and any of them may declare overridable
+ * properties.
+ *
+ * @param project the Gradle project (used for artifact resolution and
property lookup)
+ * @param bomCoordinatesList list of BOM coordinates in {@code
group:artifact:version} format
+ * @return a BomManagedVersions instance containing any version overrides
to apply
+ */
+ static BomManagedVersions resolve(Project project, Collection<String>
bomCoordinatesList) {
+ BomManagedVersions instance = new BomManagedVersions()
+
+ Map<String, String> bomProperties = new LinkedHashMap<>()
+ Map<String, List<String>> propertyToArtifacts = new LinkedHashMap<>()
+ Set<String> processed = new HashSet<>()
+
+ for (String bomCoordinates : bomCoordinatesList) {
+ String[] parts = bomCoordinates?.split(':')
+ if (parts == null || parts.length != 3) {
+ LOG.warn('Invalid BOM coordinates: {}', bomCoordinates)
+ continue
+ }
+ processBom(project, parts[0], parts[1], parts[2], bomProperties,
propertyToArtifacts, processed)
+ }
+
+ for (Map.Entry<String, List<String>> entry :
propertyToArtifacts.entrySet()) {
+ String propertyName = entry.key
+ if (project.hasProperty(propertyName)) {
+ String overrideVersion =
project.property(propertyName).toString()
+ String defaultVersion = bomProperties.get(propertyName)
+
+ if (overrideVersion != defaultVersion) {
+ for (String artifactKey : entry.value) {
+ instance.versionOverrides.put(artifactKey,
overrideVersion)
+ }
+ LOG.lifecycle(
+ 'BOM version override: {} = {} (BOM default: {})',
+ propertyName, overrideVersion, defaultVersion ?:
'unknown'
+ )
+ }
+ }
+ }
+
+ if (!instance.versionOverrides.isEmpty()) {
+ LOG.info('BOM property overrides: {} version override(s) will be
applied', instance.versionOverrides.size())
+ }
+
+ return instance
+ }
+
+ /**
+ * Applies version overrides to a Gradle configuration's resolution
strategy.
+ *
+ * @param configuration the configuration to apply overrides to
+ */
+ void applyTo(Configuration configuration) {
+ if (versionOverrides.isEmpty()) {
+ return
+ }
+
+ Map<String, String> overrides = this.versionOverrides
+ configuration.resolutionStrategy.eachDependency {
DependencyResolveDetails details ->
+ String key =
"${details.requested.group}:${details.requested.name}" as String
+ String override = overrides.get(key)
+ if (override != null) {
+ details.useVersion(override)
+ details.because('BOM version override via project property')
+ }
+ }
+ }
+
+ /**
+ * Returns whether any version overrides were detected.
+ */
+ boolean hasOverrides() {
+ return !versionOverrides.isEmpty()
+ }
+
+ /**
+ * Returns an unmodifiable view of the version overrides.
+ * Keys are {@code group:artifact}, values are the override version
strings.
+ */
+ Map<String, String> getOverrides() {
+ return Collections.unmodifiableMap(versionOverrides)
+ }
+
+ /**
+ * Parses a BOM POM file and extracts the property-to-artifact mapping.
+ * This method does not follow imported BOMs recursively - it only
processes
+ * the given file. Intended for testing and direct POM inspection.
+ *
+ * @param pomFile the BOM POM file to parse
+ * @param bomProperties output map to receive property name to default
value mappings
+ * @param propertyToArtifacts output map to receive property name to
artifact coordinate mappings
+ */
+ static void parseBomFile(File pomFile, Map<String, String> bomProperties,
Map<String, List<String>> propertyToArtifacts) {
+ Document doc = parseXml(pomFile)
+ if (doc == null) {
+ return
+ }
+ extractProperties(doc, bomProperties)
+
+ NodeList depMgmtNodes =
doc.getElementsByTagName('dependencyManagement')
+ if (depMgmtNodes.length == 0) {
+ return
+ }
+ Element depMgmt = (Element) depMgmtNodes.item(0)
+ NodeList dependenciesNodes =
depMgmt.getElementsByTagName('dependencies')
+ if (dependenciesNodes.length == 0) {
+ return
+ }
+ Element dependenciesElement = (Element) dependenciesNodes.item(0)
+ NodeList depNodes =
dependenciesElement.getElementsByTagName('dependency')
+
+ for (int i = 0; i < depNodes.length; i++) {
+ Element dep = (Element) depNodes.item(i)
+ String depGroupId = getChildText(dep, 'groupId')
+ String depArtifactId = getChildText(dep, 'artifactId')
+ String depVersion = getChildText(dep, 'version')
+
+ if (!depGroupId || !depArtifactId || !depVersion) {
+ continue
+ }
+
+ if (depVersion.contains('${')) {
+ String propertyName = extractPropertyName(depVersion)
+ if (propertyName) {
+ String artifactKey = "${depGroupId}:${depArtifactId}" as
String
+ propertyToArtifacts.computeIfAbsent(propertyName) { new
ArrayList<String>() }.add(artifactKey)
+ }
+ }
+ }
+ }
+
+ private static void processBom(
+ Project project, String group, String artifact, String version,
+ Map<String, String> bomProperties,
+ Map<String, List<String>> propertyToArtifacts,
+ Set<String> processed
+ ) {
+ String bomKey = "${group}:${artifact}:${version}" as String
+ if (!processed.add(bomKey)) {
+ return
+ }
+
+ File pomFile = resolvePomFile(project, group, artifact, version)
+ if (pomFile == null) {
+ return
+ }
+
+ Document doc = parseXml(pomFile)
+ if (doc == null) {
+ return
+ }
+
+ extractProperties(doc, bomProperties)
+ processManagedDependencies(doc, project, bomProperties,
propertyToArtifacts, processed)
+ }
+
+ private static File resolvePomFile(Project project, String group, String
artifact, String version) {
+ try {
+ Configuration detached =
project.configurations.detachedConfiguration(
+
project.dependencies.create("${group}:${artifact}:${version}@pom" as String)
+ )
+ detached.transitive = false
+ return detached.singleFile
+ }
+ catch (Exception e) {
+ LOG.info('Could not resolve BOM POM: {}:{}:{} - {}', group,
artifact, version, e.message)
+ return null
+ }
+ }
+
+ private static Document parseXml(File pomFile) {
+ try {
+ DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance()
+ factory.setNamespaceAware(false)
+ factory.setValidating(false)
+ factory.setXIncludeAware(false)
+
factory.setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd',
false)
+
factory.setFeature('http://xml.org/sax/features/external-general-entities',
false)
+
factory.setFeature('http://xml.org/sax/features/external-parameter-entities',
false)
+ return factory.newDocumentBuilder().parse(pomFile)
+ }
+ catch (Exception e) {
+ LOG.warn('Failed to parse BOM POM: {} - {}', pomFile.name,
e.message)
+ return null
+ }
+ }
+
+ private static void extractProperties(Document doc, Map<String, String>
bomProperties) {
+ NodeList propertiesNodes = doc.getElementsByTagName('properties')
+ if (propertiesNodes.length == 0) {
+ return
+ }
+
+ Element propertiesElement = (Element) propertiesNodes.item(0)
+ NodeList children = propertiesElement.childNodes
+ for (int i = 0; i < children.length; i++) {
+ if (children.item(i) instanceof Element) {
+ Element prop = (Element) children.item(i)
+ String name = prop.tagName
+ String value = prop.textContent?.trim()
+ if (name && value) {
+ bomProperties.put(name, value)
+ }
+ }
+ }
+ }
+
+ private static void processManagedDependencies(
+ Document doc, Project project,
+ Map<String, String> bomProperties,
+ Map<String, List<String>> propertyToArtifacts,
+ Set<String> processed
+ ) {
+ NodeList depMgmtNodes =
doc.getElementsByTagName('dependencyManagement')
+ if (depMgmtNodes.length == 0) {
+ return
+ }
+
+ Element depMgmt = (Element) depMgmtNodes.item(0)
+ NodeList dependenciesNodes =
depMgmt.getElementsByTagName('dependencies')
+ if (dependenciesNodes.length == 0) {
+ return
+ }
+
+ Element dependenciesElement = (Element) dependenciesNodes.item(0)
+ NodeList depNodes =
dependenciesElement.getElementsByTagName('dependency')
+
+ for (int i = 0; i < depNodes.length; i++) {
+ Element dep = (Element) depNodes.item(i)
+ String depGroupId = getChildText(dep, 'groupId')
+ String depArtifactId = getChildText(dep, 'artifactId')
+ String depVersion = getChildText(dep, 'version')
+ String depScope = getChildText(dep, 'scope')
+
+ if (!depGroupId || !depArtifactId) {
+ continue
+ }
+
+ if ('import' == depScope) {
+ String resolvedVersion = interpolateProperties(depVersion,
bomProperties)
+ if (resolvedVersion) {
+ processBom(project, depGroupId, depArtifactId,
resolvedVersion,
+ bomProperties, propertyToArtifacts, processed)
+ }
+ continue
+ }
+
+ if (depVersion && depVersion.contains('${')) {
+ String propertyName = extractPropertyName(depVersion)
+ if (propertyName) {
+ String artifactKey = "${depGroupId}:${depArtifactId}" as
String
+ propertyToArtifacts.computeIfAbsent(propertyName) { new
ArrayList<String>() }.add(artifactKey)
+ }
+ }
+ }
+ }
+
+ private static String extractPropertyName(String versionStr) {
+ if (versionStr == null) {
+ return null
+ }
+ int start = versionStr.indexOf('${')
+ int end = versionStr.indexOf('}', start)
+ if (start >= 0 && end > start) {
+ return versionStr.substring(start + 2, end)
+ }
+ return null
+ }
+
+ private static String interpolateProperties(String value, Map<String,
String> properties) {
+ if (value == null || !value.contains('${')) {
Review Comment:
Resolved in bc933fa. The maven-model rewrite walks the parent-POM chain and
scopes `<properties>` per BOM (parent first, child overrides), so properties
defined on parent BOMs are now picked up.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsDependencyValidatorPlugin.groovy:
##########
@@ -159,19 +161,49 @@ class GrailsDependencyValidatorPlugin implements
Plugin<Project> {
/**
* Scans the project's configurations to find which BOM project is in use.
+ *
+ * <p>When multiple known BOMs are declared on the same project (for
example,
+ * the {@code grails-app} plugin auto-injects {@code platform(grails-bom)}
on
Review Comment:
Done in 6ef4ec2. `detectBomPath` no longer prefers an enforcedPlatform among
several - it expects exactly one Grails BOM and fails if it finds more than one
distinct BOM.
##########
grails-test-examples/gsp-spring-boot/app/build.gradle:
##########
@@ -21,7 +21,6 @@ plugins {
id 'java'
id 'war'
id 'org.springframework.boot'
- id 'io.spring.dependency-management'
Review Comment:
Done in 59dcb7e. Re-enabled `grails-test-examples/gsp-spring-boot` - a
non-Grails Spring Boot app that renders Grails GSP and manages versions with
`io.spring.dependency-management` importing `grails-bom`. (Its runtime test is
`@Disabled` for a pre-existing GSP-on-Spring-Boot-4 auto-config bean cycle,
unrelated to dependency management.)
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy:
##########
@@ -478,6 +552,14 @@ ${importStatements}
return
}
+ // The Grails Gradle Plugin injects a regular platform(grails-bom)
into each
+ // declarable configuration via applyGrailsBom(), excluding
code-quality and
+ // annotation-processor classpaths (see isExcludedFromBomPlatform).
For Micronaut
+ // projects the user must additionally declare an enforcedPlatform on
a Micronaut BOM
Review Comment:
Done in 6ef4ec2 (hardened in 8cba523). Exactly one Grails BOM is applied
now: the plugin applies a single `platform()`/`enforcedPlatform()`, skips
injection on any configuration that already declares a Grails BOM by hand, and
fails the build if more than one distinct Grails BOM is declared.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]