Skip to content

Commit 3be255b

Browse files
authored
Merge pull request #15467 from apache/feat/gradle-managed-version-overrides
feat: Replace Spring Dependency Management plugin with Gradle platform + lightweight BOM property overrides
2 parents 5a8c4dd + 179ae7d commit 3be255b

60 files changed

Lines changed: 2915 additions & 128 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build-logic/docs-core/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ dependencies {
4747
api 'org.grails:grails-gdoc-engine:1.0.1'
4848
api 'org.yaml:snakeyaml:2.4'
4949

50+
// Maven's own model library, used by ExtractDependenciesTask to parse BOM POMs the
51+
// Maven-standard way instead of with hand-rolled XML parsing.
52+
api "org.apache.maven:maven-model:${gradleBomDependencyVersions['maven-model.version']}"
53+
5054
api "org.asciidoctor:asciidoctorj:${gradleBomDependencyVersions['asciidoctorj.version']}"
51-
implementation "org.springframework.boot:spring-boot-gradle-plugin:${gradleBomDependencyVersions['spring-boot.version']}"
5255

5356
testImplementation platform("org.spockframework:spock-bom:${gradleBomDependencyVersions['gradle-spock.version']}")
5457
testImplementation('org.spockframework:spock-core') {

build-logic/docs-core/src/main/groovy/org/apache/grails/gradle/tasks/bom/ExtractDependenciesTask.groovy

Lines changed: 68 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ package org.apache.grails.gradle.tasks.bom
2121

2222
import java.util.regex.Pattern
2323

24-
import io.spring.gradle.dependencymanagement.org.apache.maven.model.Model
25-
import io.spring.gradle.dependencymanagement.org.apache.maven.model.io.xpp3.MavenXpp3Reader
24+
import org.apache.maven.model.Model
25+
import org.apache.maven.model.io.xpp3.MavenXpp3Reader
2626
import org.gradle.api.DefaultTask
2727
import org.gradle.api.GradleException
2828
import org.gradle.api.NamedDomainObjectProvider
@@ -257,17 +257,19 @@ abstract class ExtractDependenciesTask extends DefaultTask {
257257
}
258258

259259
Properties populatePlatformDependencies(CoordinateVersionHolder bomCoordinates, List<CoordinateHolder> exclusionRules, Map<CoordinateHolder, ExtractedDependencyConstraint> constraints, boolean error = true, int level = 0) {
260-
Dependency bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom")
261-
Configuration dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency)
260+
def bomDependency = dependencyHandler.create("${bomCoordinates.coordinates}@pom")
261+
def dependencyConfiguration = configurationContainer.detachedConfiguration(bomDependency).tap {
262+
transitive = false
263+
}
262264
File bomPomFile = dependencyConfiguration.singleFile
263265

264-
MavenXpp3Reader reader = new MavenXpp3Reader()
265-
Model model = reader.read(new FileReader(bomPomFile))
266+
// Parse the BOM POM with Maven's own model library so resolution mirrors upstream Maven.
267+
Model model = bomPomFile.withInputStream { InputStream input -> new MavenXpp3Reader().read(input) }
268+
def versionProperties = new Properties()
266269

267-
Properties versionProperties = new Properties()
270+
// Parent POM populated first so its properties can be overridden by the child
268271
if (model.parent) {
269-
// Need to populate the parent bom if it's present first
270-
CoordinateVersionHolder parentBom = new CoordinateVersionHolder(
272+
def parentBom = new CoordinateVersionHolder(
271273
groupId: model.parent.groupId,
272274
artifactId: model.parent.artifactId,
273275
version: model.parent.version
@@ -276,69 +278,74 @@ abstract class ExtractDependenciesTask extends DefaultTask {
276278
versionProperties.put(entry.key, entry.value)
277279
}
278280
}
281+
279282
model.properties.entrySet().each { Map.Entry<Object, Object> entry ->
280283
versionProperties.put(entry.key, entry.value)
281284
}
282285
versionProperties.put('project.groupId', bomCoordinates.groupId)
283286
versionProperties.put('project.version', bomCoordinates.version)
284287

285-
if (model.dependencyManagement && model.dependencyManagement.dependencies) {
286-
for (io.spring.gradle.dependencymanagement.org.apache.maven.model.Dependency depItem : model.dependencyManagement.dependencies) {
287-
CoordinateHolder baseCoordinates = new CoordinateHolder(
288-
groupId: depItem.groupId,
289-
artifactId: depItem.artifactId
290-
)
291-
292-
CoordinateHolder resolvedCoordinates = new CoordinateHolder(
293-
groupId: resolveMavenProperty(baseCoordinates.coordinatesWithoutVersion, depItem.groupId, versionProperties),
294-
artifactId: resolveMavenProperty(baseCoordinates.coordinatesWithoutVersion, depItem.artifactId, versionProperties)
295-
)
296-
297-
if (!constraints.containsKey(resolvedCoordinates)) {
298-
boolean isExcluded = exclusionRules.any { CoordinateHolder excludedCoordinate ->
299-
if (excludedCoordinate.groupId && excludedCoordinate.artifactId) {
300-
return resolvedCoordinates == excludedCoordinate
301-
}
302-
303-
if (excludedCoordinate.groupId && !excludedCoordinate.artifactId) {
304-
return depItem.groupId == excludedCoordinate.groupId
305-
}
306-
307-
if (!excludedCoordinate.groupId && excludedCoordinate.artifactId) {
308-
return depItem.artifactId == excludedCoordinate.artifactId
309-
}
310-
311-
false
312-
}
313-
314-
if (!isExcluded) {
315-
String resolvedVersion = resolveMavenProperty(resolvedCoordinates.coordinatesWithoutVersion, depItem.version, versionProperties)
316-
String propertyName = depItem.version.contains('$') ? depItem.version : null
317-
ExtractedDependencyConstraint constraint = new ExtractedDependencyConstraint(
318-
groupId: resolvedCoordinates.groupId, artifactId: resolvedCoordinates.artifactId,
319-
version: resolvedVersion, versionPropertyReference: propertyName, source: bomCoordinates.artifactId
320-
)
321-
if (depItem.scope == 'import') {
322-
constraints.put(resolvedCoordinates, constraint)
323-
324-
CoordinateVersionHolder resolvedBomCoordinates = new CoordinateVersionHolder(
325-
groupId: resolvedCoordinates.groupId,
326-
artifactId: resolvedCoordinates.artifactId,
327-
version: resolvedVersion
328-
)
329-
populatePlatformDependencies(resolvedBomCoordinates, exclusionRules, constraints, error, level + 1)
330-
} else {
331-
constraints.put(resolvedCoordinates, constraint)
332-
}
333-
}
334-
}
335-
}
336-
} else {
288+
def managedDependencies = model.dependencyManagement?.dependencies ?: []
289+
if (managedDependencies.isEmpty()) {
337290
if (error) {
338291
// only the boms we directly include need to error since we expect a dependency management;
339292
// parent boms are sometimes use to share properties so we need to not error on these cases
340293
throw new GradleException("BOM ${bomCoordinates.coordinates} has no dependencyManagement section.")
341294
}
295+
return versionProperties
296+
}
297+
298+
for (def depItem : managedDependencies) {
299+
def baseCoordinates = new CoordinateHolder(
300+
groupId: depItem.groupId,
301+
artifactId: depItem.artifactId
302+
)
303+
304+
def resolvedCoordinates = new CoordinateHolder(
305+
groupId: resolveMavenProperty(baseCoordinates.coordinatesWithoutVersion, depItem.groupId, versionProperties),
306+
artifactId: resolveMavenProperty(baseCoordinates.coordinatesWithoutVersion, depItem.artifactId, versionProperties)
307+
)
308+
309+
if (constraints.containsKey(resolvedCoordinates)) {
310+
continue
311+
}
312+
313+
boolean isExcluded = exclusionRules.any { CoordinateHolder excludedCoordinate ->
314+
if (excludedCoordinate.groupId && excludedCoordinate.artifactId) {
315+
return resolvedCoordinates == excludedCoordinate
316+
}
317+
318+
if (excludedCoordinate.groupId && !excludedCoordinate.artifactId) {
319+
return depItem.groupId == excludedCoordinate.groupId
320+
}
321+
322+
if (!excludedCoordinate.groupId && excludedCoordinate.artifactId) {
323+
return depItem.artifactId == excludedCoordinate.artifactId
324+
}
325+
326+
false
327+
}
328+
329+
if (isExcluded) {
330+
continue
331+
}
332+
333+
def resolvedVersion = resolveMavenProperty(resolvedCoordinates.coordinatesWithoutVersion, depItem.version, versionProperties)
334+
def propertyName = depItem.version?.contains('$') ? depItem.version : null
335+
def constraint = new ExtractedDependencyConstraint(
336+
groupId: resolvedCoordinates.groupId, artifactId: resolvedCoordinates.artifactId,
337+
version: resolvedVersion, versionPropertyReference: propertyName, source: bomCoordinates.artifactId
338+
)
339+
constraints.put(resolvedCoordinates, constraint)
340+
341+
if (depItem.scope == 'import') {
342+
def resolvedBomCoordinates = new CoordinateVersionHolder(
343+
groupId: resolvedCoordinates.groupId,
344+
artifactId: resolvedCoordinates.artifactId,
345+
version: resolvedVersion
346+
)
347+
populatePlatformDependencies(resolvedBomCoordinates, exclusionRules, constraints, error, level + 1)
348+
}
342349
}
343350

344351
versionProperties

build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsDependencyValidatorPlugin.groovy

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ class GrailsDependencyValidatorPlugin implements Plugin<Project> {
5757

5858
private static final Set<String> BOM_PROJECT_NAMES = ['grails-bom', 'grails-gradle-bom', 'grails-base-bom', 'grails-hibernate5-bom', 'grails-hibernate7-bom', 'grails-micronaut-bom', 'grails-hibernate5-micronaut-bom', 'grails-hibernate7-micronaut-bom'].toSet()
5959

60+
/**
61+
* Configuration names that pull in a Grails BOM purely as build tooling rather than as part
62+
* of the project's published dependency graph, and are therefore ignored when detecting the
63+
* project's single Grails BOM. The shared {@code gradle/docs-dependencies.gradle} script adds
64+
* {@code platform(grails-bom)} to the {@code documentation} configuration only to resolve the
65+
* groovydoc tooling versions; a project that selects a non-default BOM variant (e.g.
66+
* {@code grails-micronaut-bom} or {@code grails-hibernate7-bom}) for its real configurations
67+
* still receives {@code grails-bom} here, which must not be misreported as a second,
68+
* conflicting BOM.
69+
*/
70+
private static final Set<String> BOM_DETECTION_EXCLUDED_CONFIGURATIONS = ['documentation'].toSet()
71+
6072
@Override
6173
void apply(Project project) {
6274
project.plugins.withId('java') {
@@ -159,19 +171,50 @@ class GrailsDependencyValidatorPlugin implements Plugin<Project> {
159171

160172
/**
161173
* Scans the project's configurations to find which BOM project is in use.
174+
*
175+
* <p>Exactly one Grails BOM is expected on a project: the BOMs are split by
176+
* integration (default / hibernate5 / micronaut), so a project selects a single
177+
* variant. This method returns the path of the one declared Grails BOM, {@code null}
178+
* when none is declared, and fails the build when more than one distinct Grails BOM
179+
* is found (which indicates a misconfiguration - e.g. layering grails-bom and
180+
* grails-micronaut-bom on the same project).</p>
181+
*
182+
* <p>Build-tooling configurations that pull in a BOM purely to resolve their own tool
183+
* versions (see {@link #BOM_DETECTION_EXCLUDED_CONFIGURATIONS}) are skipped, so the shared
184+
* {@code documentation} configuration's {@code grails-bom} does not conflict with a variant
185+
* BOM a project selects for its real dependencies.</p>
162186
*/
163187
static String detectBomPath(Project project) {
188+
Set<String> bomPaths = new LinkedHashSet<>()
189+
164190
for (Configuration config : project.configurations) {
191+
if (BOM_DETECTION_EXCLUDED_CONFIGURATIONS.contains(config.name)) {
192+
continue
193+
}
165194
for (Dependency dep : config.dependencies) {
166-
if (BOM_PROJECT_NAMES.contains(dep.name)) {
167-
Project bomProject = project.rootProject.findProject(":${dep.name}" as String)
168-
if (bomProject != null) {
169-
return bomProject.path
170-
}
195+
if (!BOM_PROJECT_NAMES.contains(dep.name)) {
196+
continue
171197
}
198+
def bomProject = project.rootProject.findProject(":${dep.name}" as String)
199+
if (bomProject == null) {
200+
continue
201+
}
202+
bomPaths.add(bomProject.path)
172203
}
173204
}
174-
null
205+
206+
if (bomPaths.isEmpty()) {
207+
return null
208+
}
209+
if (bomPaths.size() > 1) {
210+
throw new GradleException(
211+
"Project '${project.name}' declares more than one Grails BOM (${bomPaths.join(', ')}). " +
212+
'Exactly one Grails BOM may be applied; the BOMs are split by integration ' +
213+
'(default / hibernate5 / micronaut), so a project must select a single variant.'
214+
)
215+
}
216+
217+
bomPaths.first()
175218
}
176219

177220
/**
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.grails.buildsrc
20+
21+
import org.gradle.api.GradleException
22+
import org.gradle.api.Project
23+
import org.gradle.testfixtures.ProjectBuilder
24+
import spock.lang.Specification
25+
26+
class GrailsDependencyValidatorPluginSpec extends Specification {
27+
28+
private static Project rootWithBoms() {
29+
Project root = ProjectBuilder.builder().withName('root').build()
30+
ProjectBuilder.builder().withName('grails-bom').withParent(root).build()
31+
ProjectBuilder.builder().withName('grails-micronaut-bom').withParent(root).build()
32+
ProjectBuilder.builder().withName('grails-hibernate7-bom').withParent(root).build()
33+
root
34+
}
35+
36+
private static void addBomPlatform(Project project, String configuration, String bomPath) {
37+
project.configurations.maybeCreate(configuration)
38+
project.dependencies.add(configuration,
39+
project.dependencies.platform(project.dependencies.project(path: bomPath)))
40+
}
41+
42+
void "detectBomPath ignores the documentation configuration when a variant BOM is used elsewhere"() {
43+
given: "a project that selects grails-micronaut-bom but inherits grails-bom on the shared documentation config"
44+
Project root = rootWithBoms()
45+
Project project = ProjectBuilder.builder().withName('grails-micronaut').withParent(root).build()
46+
addBomPlatform(project, 'api', ':grails-micronaut-bom')
47+
addBomPlatform(project, 'documentation', ':grails-bom')
48+
49+
expect: "the variant BOM wins and no conflict is reported"
50+
GrailsDependencyValidatorPlugin.detectBomPath(project) == ':grails-micronaut-bom'
51+
}
52+
53+
void "detectBomPath returns the single declared BOM"() {
54+
given: "a default-variant project with grails-bom on both a real config and the documentation config"
55+
Project root = rootWithBoms()
56+
Project project = ProjectBuilder.builder().withName('grails-core').withParent(root).build()
57+
addBomPlatform(project, 'implementation', ':grails-bom')
58+
addBomPlatform(project, 'documentation', ':grails-bom')
59+
60+
expect:
61+
GrailsDependencyValidatorPlugin.detectBomPath(project) == ':grails-bom'
62+
}
63+
64+
void "detectBomPath returns null when no Grails BOM is declared"() {
65+
given:
66+
Project root = rootWithBoms()
67+
Project project = ProjectBuilder.builder().withName('plain').withParent(root).build()
68+
project.configurations.maybeCreate('implementation')
69+
70+
expect:
71+
GrailsDependencyValidatorPlugin.detectBomPath(project) == null
72+
}
73+
74+
void "detectBomPath returns null when only the documentation tooling configuration declares a BOM"() {
75+
given: "a project whose sole BOM is the doc-tooling grails-bom on the documentation config"
76+
Project root = rootWithBoms()
77+
Project project = ProjectBuilder.builder().withName('docs-only').withParent(root).build()
78+
addBomPlatform(project, 'documentation', ':grails-bom')
79+
80+
expect: "the doc-tooling BOM is ignored, so no project BOM is detected"
81+
GrailsDependencyValidatorPlugin.detectBomPath(project) == null
82+
}
83+
84+
void "detectBomPath fails when two distinct Grails BOMs are declared on real configurations"() {
85+
given: "a genuine misconfiguration layering two variant BOMs on real dependency configurations"
86+
Project root = rootWithBoms()
87+
Project project = ProjectBuilder.builder().withName('misconfigured').withParent(root).build()
88+
addBomPlatform(project, 'api', ':grails-micronaut-bom')
89+
addBomPlatform(project, 'implementation', ':grails-hibernate7-bom')
90+
91+
when:
92+
GrailsDependencyValidatorPlugin.detectBomPath(project)
93+
94+
then:
95+
GradleException e = thrown(GradleException)
96+
e.message.contains('declares more than one Grails BOM')
97+
}
98+
}

dependencies.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ ext {
3838
'jline2.version' : '2.14.6',
3939
'jna.version' : '5.18.1',
4040
'jquery.version' : '3.7.1',
41+
'maven-model.version' : '3.9.16',
4142
'objenesis.version' : '3.4',
4243
'spring-boot.version' : '4.1.0-RC1',
4344
]

grails-bom/base/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ ext {
209209
ExtractedDependencyConstraint extractedConstraint = propertyNameCalculator.calculate(groupId, artifactId, inlineVersion, isBom)
210210
if (extractedConstraint?.versionPropertyReference) {
211211
// use the property reference instead of the hard coded version so that it can be
212-
// overriden by the spring boot dependency management plugin
212+
// overridden by project properties (gradle.properties or ext['property.name'])
213213
dep.version[0].value = extractedConstraint.versionPropertyReference
214214

215215
// Add an entry in the <properties> node with the actual version number

0 commit comments

Comments
 (0)