This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch feat/gradle-managed-version-overrides in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit ca495ab5287aeba5d2300a997a6c24145e866bc4 Author: James Fredley <[email protected]> AuthorDate: Thu Feb 26 11:25:11 2026 -0500 feat: replace Spring Dependency Management plugin with Gradle platform and lightweight BOM property overrides Replace the Spring Dependency Management Gradle plugin with Gradle's native platform() support plus a lightweight BomManagedVersions utility that preserves the ability to override BOM-managed dependency versions via project properties (ext[] or gradle.properties). This allows Grails to standardize on Gradle platforms - the modern dependency management solution - while retaining the one feature Gradle platforms lack: property-based version overrides from BOMs. Changes: - Add BomManagedVersions: parses BOM POM XML to extract property-to- artifact mappings, applies version overrides via eachDependency() - Update GrailsGradlePlugin to use platform() + BomManagedVersions instead of Spring DM plugin - Deprecate GrailsExtension.springDependencyManagement flag - Remove Spring DM plugin from plugins/build.gradle dependency - Remove Spring DM plugin from example projects - Update documentation to reflect Gradle platform approach - Add unit tests (BomManagedVersionsSpec) and functional test (BomPlatformFunctionalSpec) Note: build-logic/docs-core/ExtractDependenciesTask still uses Spring DM's shaded Maven model classes and should be addressed in a follow-up. Assisted-by: Claude Code <[email protected]> --- grails-bom/build.gradle | 2 +- .../examples/spring-boot-app/build.gradle | 1 - .../gradleBuild/gradleDependencies.adoc | 26 +- grails-gradle/plugins/build.gradle | 1 - .../gradle/plugin/core/BomManagedVersions.groovy | 354 +++++++++++++++++++++ .../gradle/plugin/core/GrailsExtension.groovy | 7 +- .../gradle/plugin/core/GrailsGradlePlugin.groovy | 55 ++-- .../plugin/core/BomManagedVersionsSpec.groovy | 97 ++++++ .../plugin/core/BomPlatformFunctionalSpec.groovy | 45 +++ .../src/test/resources/test-poms/test-bom.pom | 51 +++ .../test-projects/bom-platform-basic/build.gradle | 16 + .../bom-platform-basic/gradle.properties | 1 + .../grails-app/conf/application.yml | 2 + .../bom-platform-basic/settings.gradle | 1 + .../templates/grailsCentralPublishing.gradle | 2 +- .../gsp-spring-boot/app/build.gradle | 1 - 16 files changed, 624 insertions(+), 38 deletions(-) diff --git a/grails-bom/build.gradle b/grails-bom/build.gradle index 260239fced..cd38ef236e 100644 --- a/grails-bom/build.gradle +++ b/grails-bom/build.gradle @@ -210,7 +210,7 @@ ext { ExtractedDependencyConstraint extractedConstraint = propertyNameCalculator.calculate(groupId, artifactId, inlineVersion, isBom) if (extractedConstraint?.versionPropertyReference) { // use the property reference instead of the hard coded version so that it can be - // overriden by the spring boot dependency management plugin + // overridden by project properties (gradle.properties or ext['property.name']) dep.version[0].value = extractedConstraint.versionPropertyReference // Add an entry in the <properties> node with the actual version number diff --git a/grails-data-graphql/examples/spring-boot-app/build.gradle b/grails-data-graphql/examples/spring-boot-app/build.gradle index 6c113e1be8..0c64d390e2 100644 --- a/grails-data-graphql/examples/spring-boot-app/build.gradle +++ b/grails-data-graphql/examples/spring-boot-app/build.gradle @@ -30,7 +30,6 @@ buildscript { apply plugin: 'groovy' apply plugin: 'idea' apply plugin: 'org.springframework.boot' -apply plugin: 'io.spring.dependency-management' dependencies { implementation platform(project(':grails-bom')) diff --git a/grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc b/grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc index 31a1ffe6b3..348689f4a0 100644 --- a/grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc +++ b/grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc @@ -60,13 +60,22 @@ dependencies { Note that version numbers are not present in the majority of the dependencies. -This is thanks to the Spring dependency management plugin which automatically configures `grails-bom` as a Maven BOM via the Grails Gradle Plugin. This defines the default dependency versions for most commonly used dependencies and plugins. +This is thanks to Gradle's platform support which automatically imports `grails-bom` as a managed dependency platform via the Grails Gradle Plugin. This defines the default dependency versions for most commonly used dependencies and plugins. + +To override a managed version, set the corresponding property in `gradle.properties` or `build.gradle`: +[source,groovy] +---- +// gradle.properties +slf4j.version=1.7.36 + +// or build.gradle +ext['slf4j.version'] = '1.7.36' +---- For a Grails App, applying `org.apache.grails.gradle.grails-web` will automatically configure the `grails-bom`. No other steps required. -For Plugins and Projects which do not use `org.apache.grails.gradle.grails-web`, you can apply the `grails-bom` in one of the following two ways. +For Plugins and Projects which do not use `org.apache.grails.gradle.grails-web`, you can apply the `grails-bom` using Gradle Platforms: -build.gradle, using Gradle Platforms: [source,groovy] ---- dependencies { @@ -74,14 +83,3 @@ dependencies { //... } ---- - -build.gradle, using Spring dependency management plugin: -[source,groovy] ----- -dependencyManagement { - imports { - mavenBom 'org.apache.grails:grails-bom:{GrailsVersion}' - } - applyMavenExclusions false -} ----- diff --git a/grails-gradle/plugins/build.gradle b/grails-gradle/plugins/build.gradle index cedf995b7a..88dff223b8 100644 --- a/grails-gradle/plugins/build.gradle +++ b/grails-gradle/plugins/build.gradle @@ -55,7 +55,6 @@ dependencies { implementation "${gradleBomDependencies['grails-publish-plugin']}" implementation 'org.springframework.boot:spring-boot-gradle-plugin' implementation 'org.springframework.boot:spring-boot-loader-tools' - implementation 'io.spring.gradle:dependency-management-plugin' // Testing - Gradle TestKit is auto-added by java-gradle-plugin testImplementation('org.spockframework:spock-core') { transitive = false } diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy new file mode 100644 index 0000000000..b1f708478c --- /dev/null +++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy @@ -0,0 +1,354 @@ +/* + * 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.core + +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> + * + * @since 8.0 + */ +@CompileStatic +class BomManagedVersions { + + private static final Logger LOG = Logging.getLogger(BomManagedVersions) + + 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) { + BomManagedVersions instance = new BomManagedVersions() + + String[] parts = bomCoordinates.split(':') + if (parts.length != 3) { + LOG.warn('Invalid BOM coordinates: {}', bomCoordinates) + return instance + } + + Map<String, String> bomProperties = new LinkedHashMap<>() + Map<String, List<String>> propertyToArtifacts = new LinkedHashMap<>() + + processBom(project, parts[0], parts[1], parts[2], bomProperties, propertyToArtifacts, new HashSet<String>()) + + 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( + 'Grails BOM version override: {} = {} (BOM default: {})', + propertyName, overrideVersion, defaultVersion ?: 'unknown' + ) + } + } + } + + if (!instance.versionOverrides.isEmpty()) { + LOG.info('Grails BOM: {} 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('Grails 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.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('${')) { + return value + } + + String result = value + int maxIterations = 10 + while (result.contains('${') && maxIterations-- > 0) { + String propertyName = extractPropertyName(result) + if (propertyName == null) { + break + } + String resolved = properties.get(propertyName) + if (resolved == null) { + break + } + result = result.replace("\${${propertyName}}" as String, resolved) + } + return result + } + + private static String getChildText(Element parent, String childTagName) { + NodeList children = parent.getElementsByTagName(childTagName) + if (children.length == 0) { + return null + } + return children.item(0).textContent?.trim() + } +} diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy index c216885913..82ce70830c 100644 --- a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy +++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy @@ -83,8 +83,13 @@ class GrailsExtension { List<String> starImports = [] /** - * Whether the spring dependency management plugin should be applied by default + * @deprecated The Spring Dependency Management plugin has been replaced with Gradle's native + * {@code platform()} support plus lightweight property-based version overrides. + * This property is no longer used. Set version overrides in {@code gradle.properties} + * or via {@code ext['property.name']} instead. + * @see BomManagedVersions */ + @Deprecated boolean springDependencyManagement = true /** diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy index bdfcc62c31..72ac323722 100644 --- a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy +++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy @@ -26,8 +26,6 @@ import grails.util.GrailsNameUtils import grails.util.Metadata import groovy.transform.CompileDynamic import groovy.transform.CompileStatic -import io.spring.gradle.dependencymanagement.DependencyManagementPlugin -import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension import org.apache.grails.gradle.common.PropertyFileUtils import org.apache.tools.ant.filters.EscapeUnicode import org.apache.tools.ant.filters.ReplaceTokens @@ -355,17 +353,45 @@ ${importStatements} protected void applyDefaultPlugins(Project project) { applySpringBootPlugin(project) - project.afterEvaluate { - GrailsExtension ge = project.extensions.getByType(GrailsExtension) - if (ge.springDependencyManagement) { - Plugin dependencyManagementPlugin = project.plugins.findPlugin(DependencyManagementPlugin) - if (dependencyManagementPlugin == null) { - project.plugins.apply(DependencyManagementPlugin) - } + applyGrailsBom(project) + } - DependencyManagementExtension dme = project.extensions.findByType(DependencyManagementExtension) + /** + * Applies the Grails BOM as a Gradle platform and configures property-based + * version overrides. This replaces the Spring Dependency Management plugin with + * a lightweight mechanism that: + * <ol> + * <li>Imports {@code grails-bom} via Gradle's native {@code platform()} support</li> + * <li>Parses the BOM POM chain to discover which Maven properties control which artifact versions</li> + * <li>Checks project properties ({@code gradle.properties} or {@code ext['property.name']}) for overrides</li> + * <li>Applies any overrides via {@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 BomManagedVersions + * @since 8.0 + */ + protected void applyGrailsBom(Project project) { + String grailsVersion = (project.findProperty('grailsVersion') ?: BuildSettings.grailsVersion) as String + String bomCoordinates = "org.apache.grails:grails-bom:${grailsVersion}" as String + + project.dependencies.add('implementation', project.dependencies.platform(bomCoordinates)) - applyBomImport(dme, project) + project.afterEvaluate { + BomManagedVersions managedVersions = BomManagedVersions.resolve(project, bomCoordinates) + if (managedVersions.hasOverrides()) { + project.configurations.configureEach { Configuration conf -> + managedVersions.applyTo(conf) + } } } } @@ -377,13 +403,6 @@ ${importStatements} } } - @CompileDynamic - private void applyBomImport(DependencyManagementExtension dme, project) { - dme.imports({ - mavenBom("org.apache.grails:grails-bom:${project.properties['grailsVersion']}") - }) - } - protected String getDefaultProfile() { 'web' } diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy new file mode 100644 index 0000000000..6953c1ba3e --- /dev/null +++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy @@ -0,0 +1,97 @@ +/* + * 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.core + +import spock.lang.Specification + +/** + * Tests for the Grails BOM platform integration that replaced the + * Spring Dependency Management plugin. + * + * <p>Verifies that {@link BomManagedVersions} correctly parses BOM POM + * files, extracts property-to-artifact mappings, and that the Grails + * Gradle plugin applies {@code grails-bom} as a Gradle + * {@code platform()} dependency.</p> + * + * @since 8.0 + * @see BomManagedVersions + * @see GrailsGradlePlugin#applyGrailsBom + */ +class BomManagedVersionsSpec extends Specification { + + def "parseBomFile extracts properties from BOM POM"() { + given: + File pomFile = new File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI()) + Map<String, String> bomProperties = [:] + Map<String, List<String>> propertyToArtifacts = [:] + + when: + BomManagedVersions.parseBomFile(pomFile, bomProperties, propertyToArtifacts) + + then: + bomProperties['jackson.version'] == '2.15.0' + bomProperties['slf4j.version'] == '2.0.9' + bomProperties['groovy.version'] == '4.0.30' + } + + def "parseBomFile maps property references to artifact coordinates"() { + given: + File pomFile = new File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI()) + Map<String, String> bomProperties = [:] + Map<String, List<String>> propertyToArtifacts = [:] + + when: + BomManagedVersions.parseBomFile(pomFile, bomProperties, propertyToArtifacts) + + then: "jackson.version maps to all three jackson artifacts" + propertyToArtifacts['jackson.version'].containsAll([ + 'com.fasterxml.jackson.core:jackson-databind', + 'com.fasterxml.jackson.core:jackson-core', + 'com.fasterxml.jackson.core:jackson-annotations' + ]) + + and: "slf4j.version maps to slf4j-api" + propertyToArtifacts['slf4j.version'] == ['org.slf4j:slf4j-api'] + + and: "groovy.version maps to groovy" + propertyToArtifacts['groovy.version'] == ['org.apache.groovy:groovy'] + } + + def "parseBomFile ignores dependencies with hardcoded versions"() { + given: + File pomFile = new File(getClass().getClassLoader().getResource('test-poms/test-bom.pom').toURI()) + Map<String, String> bomProperties = [:] + Map<String, List<String>> propertyToArtifacts = [:] + + when: + BomManagedVersions.parseBomFile(pomFile, bomProperties, propertyToArtifacts) + + then: "hardcoded-version artifact is not in any property mapping" + !propertyToArtifacts.values().flatten().contains('org.example:hardcoded-version') + } + + def "BomManagedVersions with no overrides reports hasOverrides false"() { + given: + def instance = new BomManagedVersions() + + expect: + !instance.hasOverrides() + instance.overrides.isEmpty() + } +} diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy new file mode 100644 index 0000000000..4e1a5ba3d6 --- /dev/null +++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.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.grails.gradle.plugin.core + +/** + * Functional tests for the Gradle platform-based BOM integration. + * + * <p>Uses Gradle TestKit to verify that the Grails Gradle plugin correctly + * applies {@code grails-bom} as a Gradle {@code platform()} dependency + * and no longer depends on the Spring Dependency Management plugin.</p> + * + * @since 8.0 + * @see BomManagedVersions + * @see GrailsGradlePlugin#applyGrailsBom + */ +class BomPlatformFunctionalSpec extends GradleSpecification { + + def "plugin applies grails-bom as Gradle platform and does not apply Spring DM plugin"() { + given: + setupTestResourceProject('bom-platform-basic') + + when: + def result = executeTask('inspectBomSetup') + + then: + result.output.contains('HAS_PLATFORM_BOM=true') + result.output.contains('HAS_SPRING_DM=false') + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom b/grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom new file mode 100644 index 0000000000..cda1a266ad --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.test</groupId> + <artifactId>test-bom</artifactId> + <version>1.0.0</version> + <packaging>pom</packaging> + + <properties> + <jackson.version>2.15.0</jackson.version> + <slf4j.version>2.0.9</slf4j.version> + <groovy.version>4.0.30</groovy.version> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>${jackson.version}</version> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <version>${jackson.version}</version> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + <version>${jackson.version}</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>${slf4j.version}</version> + </dependency> + <dependency> + <groupId>org.apache.groovy</groupId> + <artifactId>groovy</artifactId> + <version>${groovy.version}</version> + </dependency> + <dependency> + <groupId>org.example</groupId> + <artifactId>hardcoded-version</artifactId> + <version>1.0.0</version> + </dependency> + </dependencies> + </dependencyManagement> +</project> diff --git a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle new file mode 100644 index 0000000000..9b58b70062 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +tasks.register('inspectBomSetup') { + doLast { + def implDeps = configurations.implementation.allDependencies + def hasPlatform = implDeps.any { dep -> + dep.group == 'org.apache.grails' && dep.name == 'grails-bom' + } + println "HAS_PLATFORM_BOM=${hasPlatform}" + + def hasSpringDm = project.plugins.findPlugin('io.spring.dependency-management') != null + println "HAS_SPRING_DM=${hasSpringDm}" + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/gradle.properties new file mode 100644 index 0000000000..35c332fb87 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/grails-app/conf/application.yml new file mode 100644 index 0000000000..4706b4393f --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/grails-app/conf/application.yml @@ -0,0 +1,2 @@ +grails: + profile: web diff --git a/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/settings.gradle new file mode 100644 index 0000000000..b2a1c27a42 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-bom-platform' diff --git a/grails-profiles/plugin/templates/grailsCentralPublishing.gradle b/grails-profiles/plugin/templates/grailsCentralPublishing.gradle index e9f5c8cafe..e7cbc1abf4 100644 --- a/grails-profiles/plugin/templates/grailsCentralPublishing.gradle +++ b/grails-profiles/plugin/templates/grailsCentralPublishing.gradle @@ -7,7 +7,7 @@ publishing { // simply remove dependencies without a version // version-less dependencies are handled with dependencyManagement - // see https://github.com/spring-gradle-plugins/dependency-management-plugin/issues/8 for more complete solutions + // remove version-less dependencies since versions are managed by the Grails BOM platform pomNode.dependencies.dependency.findAll { it.version.text().isEmpty() }.each { diff --git a/grails-test-examples/gsp-spring-boot/app/build.gradle b/grails-test-examples/gsp-spring-boot/app/build.gradle index c3187fecfa..81338eab00 100644 --- a/grails-test-examples/gsp-spring-boot/app/build.gradle +++ b/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' id "groovy" }
