diff --git a/grails-bom/build.gradle b/grails-bom/build.gradle index 260239fcedd..cd38ef236e6 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 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 6c113e1be82..0c64d390e2c 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 31a1ffe6b3e..348689f4a0b 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 cedf995b7aa..88dff223b86 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 00000000000..e022f947cdd --- /dev/null +++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy @@ -0,0 +1,356 @@ +/* + * 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. + * + *

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()}.

+ * + *

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 Gradle #9160).

+ * + * @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 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 bomProperties = new LinkedHashMap<>() + Map> propertyToArtifacts = new LinkedHashMap<>() + + processBom(project, parts[0], parts[1], parts[2], bomProperties, propertyToArtifacts, new HashSet()) + + for (Map.Entry> 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 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 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 bomProperties, Map> 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() }.add(artifactKey) + } + } + } + } + + private static void processBom( + Project project, String group, String artifact, String version, + Map bomProperties, + Map> propertyToArtifacts, + Set 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 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 bomProperties, + Map> propertyToArtifacts, + Set 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() }.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 properties) { + if (value == null || !value.contains('${')) { + return value + } + + String result = value + int maxIterations = MAX_PROPERTY_INTERPOLATION_DEPTH + 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 c216885913d..82ce70830cf 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 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 bdfcc62c318..4f065286f37 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,21 +353,78 @@ ${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: + *
    + *
  1. Imports {@code grails-bom} via Gradle's native {@code platform()} support
  2. + *
  3. Parses the BOM POM chain to discover which Maven properties control which artifact versions
  4. + *
  5. Checks project properties ({@code gradle.properties} or {@code ext['property.name']}) for overrides
  6. + *
  7. Applies any overrides via {@code ResolutionStrategy.eachDependency()}
  8. + *
+ * + *

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}:

+ *
+     * // gradle.properties
+     * slf4j.version=1.7.36
+     *
+     * // or build.gradle
+     * ext['slf4j.version'] = '1.7.36'
+     * 
+ * + * @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 + + // 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. + project.configurations.maybeCreate('developmentOnly') + + // Apply the BOM platform to all declarable project configurations, matching + // the behavior of the Spring Dependency Management plugin which applied version + // constraints globally via configurations.all() + resolutionStrategy.eachDependency(). + // Non-declarable configurations (e.g. apiElements, runtimeElements) inherit + // constraints through their parent configurations. Code quality tool + // configurations (checkstyle, codenarc, etc.) are excluded because adding BOM + // constraints to tool classpaths can upgrade transitive dependencies and break + // the tools - unlike resolutionStrategy hooks, platform() constraints + // participate in version conflict resolution. + project.configurations.configureEach { Configuration conf -> + if (conf.canBeDeclared && !isCodeQualityConfiguration(conf.name)) { + project.dependencies.add(conf.name, 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) + } } } } + /** + * Returns {@code true} if the given configuration name belongs to a code quality + * tool (Checkstyle, CodeNarc, PMD, SpotBugs). These configurations hold tool + * classpaths and must not receive the BOM platform because {@code platform()} + * constraints can upgrade transitive dependencies and break the tools. + */ + private static boolean isCodeQualityConfiguration(String name) { + name == 'checkstyle' || name == 'codenarc' || name == 'pmd' || + name == 'spotbugs' || name == 'spotbugsPlugins' + } + protected void applySpringBootPlugin(Project project) { def springBoot = project.extensions.findByType(SpringBootExtension) if (!springBoot) { @@ -377,13 +432,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 00000000000..6953c1ba3eb --- /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. + * + *

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.

+ * + * @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 bomProperties = [:] + Map> 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 bomProperties = [:] + Map> 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 bomProperties = [:] + Map> 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 00000000000..4e1a5ba3d67 --- /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. + * + *

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.

+ * + * @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 00000000000..cda1a266adb --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom @@ -0,0 +1,51 @@ + + + 4.0.0 + org.test + test-bom + 1.0.0 + pom + + + 2.15.0 + 2.0.9 + 4.0.30 + + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.apache.groovy + groovy + ${groovy.version} + + + org.example + hardcoded-version + 1.0.0 + + + + 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 00000000000..9b58b700622 --- /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 00000000000..35c332fb874 --- /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 00000000000..4706b4393fd --- /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 00000000000..b2a1c27a425 --- /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 e9f5c8cafec..e7cbc1abf44 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 c3187fecfaf..81338eab00d 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" }