Merge branch 'main' into 4.0.x

This commit is contained in:
Andy Wilkinson
2025-05-27 14:24:09 +01:00
8 changed files with 227 additions and 36 deletions

View File

@@ -38,6 +38,9 @@ import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.JavaPlatformPlugin;
import org.springframework.boot.build.bom.BomExtension.LibraryHandler.AlignWithHandler.PropertyHandler;
import org.springframework.boot.build.bom.BomExtension.LibraryHandler.AlignWithHandler.VersionHandler;
import org.springframework.boot.build.bom.Library.DependencyVersionAlignment;
import org.springframework.boot.build.bom.Library.Exclusion;
import org.springframework.boot.build.bom.Library.Group;
import org.springframework.boot.build.bom.Library.ImportedBom;
@@ -45,6 +48,7 @@ import org.springframework.boot.build.bom.Library.LibraryVersion;
import org.springframework.boot.build.bom.Library.Link;
import org.springframework.boot.build.bom.Library.Module;
import org.springframework.boot.build.bom.Library.PermittedDependency;
import org.springframework.boot.build.bom.Library.PomPropertyVersionAlignment;
import org.springframework.boot.build.bom.Library.ProhibitedVersion;
import org.springframework.boot.build.bom.Library.VersionAlignment;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
@@ -106,16 +110,26 @@ public class BomExtension {
(version != null) ? version : "");
action.execute(libraryHandler);
LibraryVersion libraryVersion = new LibraryVersion(DependencyVersion.parse(libraryHandler.version));
VersionAlignment versionAlignment = (libraryHandler.alignWith.version != null)
? new VersionAlignment(libraryHandler.alignWith.version.from,
libraryHandler.alignWith.version.managedBy, this.project, this.libraries, libraryHandler.groups)
: null;
addLibrary(new Library(name, libraryHandler.calendarName, libraryVersion, libraryHandler.groups,
libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots, versionAlignment,
libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots, versionAlignment(libraryHandler),
libraryHandler.alignWith.dependencyManagementDeclaredIn, libraryHandler.linkRootName,
libraryHandler.links));
}
private VersionAlignment versionAlignment(LibraryHandler libraryHandler) {
VersionHandler version = libraryHandler.alignWith.version;
if (version != null) {
return new DependencyVersionAlignment(version.of, version.from, version.managedBy, this.project,
this.libraries, libraryHandler.groups);
}
PropertyHandler property = libraryHandler.alignWith.property;
if (property != null) {
return new PomPropertyVersionAlignment(property.name, property.of, property.managedBy, this.project,
this.libraries);
}
return null;
}
private String createDependencyNotation(String groupId, String artifactId, DependencyVersion version) {
return groupId + ":" + artifactId + ":" + version;
}
@@ -382,6 +396,8 @@ public class BomExtension {
private VersionHandler version;
private PropertyHandler property;
private String dependencyManagementDeclaredIn;
public void version(Action<VersionHandler> action) {
@@ -389,16 +405,27 @@ public class BomExtension {
action.execute(this.version);
}
public void property(Action<PropertyHandler> action) {
this.property = new PropertyHandler();
action.execute(this.property);
}
public void dependencyManagementDeclaredIn(String bomCoordinates) {
this.dependencyManagementDeclaredIn = bomCoordinates;
}
public static class VersionHandler {
private String of;
private String from;
private String managedBy;
public void of(String of) {
this.of = of;
}
public void from(String from) {
this.from = from;
}
@@ -409,6 +436,28 @@ public class BomExtension {
}
public static class PropertyHandler {
private String name;
private String of;
private String managedBy;
public void name(String name) {
this.name = name;
}
public void of(String dependency) {
this.of = dependency;
}
public void managedBy(String managedBy) {
this.managedBy = managedBy;
}
}
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.boot.build.bom;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -31,6 +32,11 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.VersionRange;
import org.gradle.api.Project;
@@ -38,6 +44,7 @@ import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.result.DependencyResult;
import org.gradle.api.artifacts.result.ResolutionResult;
import org.w3c.dom.Document;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
@@ -406,10 +413,18 @@ public class Library {
}
public interface VersionAlignment {
Set<String> resolve();
}
/**
* Version alignment for a library.
* Version alignment for a library based on a dependency of another module.
*/
public static class VersionAlignment {
public static class DependencyVersionAlignment implements VersionAlignment {
private final String dependency;
private final String from;
@@ -423,7 +438,9 @@ public class Library {
private Set<String> alignedVersions;
VersionAlignment(String from, String managedBy, Project project, List<Library> libraries, List<Group> groups) {
DependencyVersionAlignment(String dependency, String from, String managedBy, Project project,
List<Library> libraries, List<Group> groups) {
this.dependency = dependency;
this.from = from;
this.managedBy = managedBy;
this.project = project;
@@ -431,27 +448,34 @@ public class Library {
this.groups = groups;
}
@Override
public Set<String> resolve() {
if (this.alignedVersions != null) {
return this.alignedVersions;
}
Map<String, String> versions = resolveAligningDependencies();
Set<String> versionsInLibrary = new HashSet<>();
for (Group group : this.groups) {
for (Module module : group.getModules()) {
String version = versions.get(group.getId() + ":" + module.getName());
if (version != null) {
versionsInLibrary.add(version);
}
}
for (String plugin : group.getPlugins()) {
String version = versions.get(group.getId() + ":" + plugin);
if (version != null) {
versionsInLibrary.add(version);
}
}
if (this.dependency != null) {
String version = versions.get(this.dependency);
this.alignedVersions = (version != null) ? Set.of(version) : Collections.emptySet();
}
else {
Set<String> versionsInLibrary = new HashSet<>();
for (Group group : this.groups) {
for (Module module : group.getModules()) {
String version = versions.get(group.getId() + ":" + module.getName());
if (version != null) {
versionsInLibrary.add(version);
}
}
for (String plugin : group.getPlugins()) {
String version = versions.get(group.getId() + ":" + plugin);
if (version != null) {
versionsInLibrary.add(version);
}
}
}
this.alignedVersions = versionsInLibrary;
}
this.alignedVersions = versionsInLibrary;
return this.alignedVersions;
}
@@ -539,6 +563,100 @@ public class Library {
}
/**
* Version alignment for a library based on a property in the pom of another module.
*/
public static class PomPropertyVersionAlignment implements VersionAlignment {
private final String name;
private final String from;
private final String managedBy;
private final Project project;
private final List<Library> libraries;
private Set<String> alignedVersions;
PomPropertyVersionAlignment(String name, String from, String managedBy, Project project,
List<Library> libraries) {
this.name = name;
this.from = from;
this.managedBy = managedBy;
this.project = project;
this.libraries = libraries;
}
@Override
public Set<String> resolve() {
if (this.alignedVersions != null) {
return this.alignedVersions;
}
Configuration alignmentConfiguration = this.project.getConfigurations()
.detachedConfiguration(getAligningDependencies().toArray(new Dependency[0]));
Set<File> files = alignmentConfiguration.resolve();
if (files.size() != 1) {
throw new IllegalStateException(
"Expected a single file when resolving the pom of " + this.from + " but found " + files.size());
}
File pomFile = files.iterator().next();
return Set.of(propertyFrom(pomFile));
}
private List<Dependency> getAligningDependencies() {
Library managingLibrary = findManagingLibrary();
List<Dependency> boms = getBomDependencies(managingLibrary);
List<Dependency> dependencies = new ArrayList<>();
dependencies.addAll(boms);
dependencies.add(this.project.getDependencies().create(this.from + "@pom"));
return dependencies;
}
private Library findManagingLibrary() {
if (this.managedBy == null) {
return null;
}
return this.libraries.stream()
.filter((candidate) -> this.managedBy.equals(candidate.getName()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Managing library '" + this.managedBy + "' not found."));
}
private List<Dependency> getBomDependencies(Library manager) {
return manager.getGroups()
.stream()
.flatMap((group) -> group.getBoms()
.stream()
.map((bom) -> this.project.getDependencies()
.platform(group.getId() + ":" + bom.name() + ":" + manager.getVersion().getVersion())))
.toList();
}
private String propertyFrom(File pomFile) {
try {
DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document document = documentBuilder.parse(pomFile);
XPath xpath = XPathFactory.newInstance().newXPath();
return xpath.evaluate("/project/properties/" + this.name + "/text()", document);
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Override
public String toString() {
String result = "version from properties of " + this.from;
if (this.managedBy != null) {
result += " that is managed by " + this.managedBy;
}
return result;
}
}
public record Link(String rootName, Function<LibraryVersion, String> factory, List<String> packages) {
private static final Pattern PACKAGE_EXPAND = Pattern.compile("^(.*)\\[(.*)\\]$");

View File

@@ -974,9 +974,12 @@ bom {
}
}
library("Jedis", "6.0.0") {
prohibit {
contains "-beta"
because "we don't want beta dependencies"
alignWith {
property {
name "jedis"
of "org.springframework.data:spring-data-redis"
managedBy "Spring Data Bom"
}
}
group("redis.clients") {
modules = [
@@ -1545,6 +1548,13 @@ bom {
}
}
library("MongoDB", "5.5.0") {
alignWith {
version {
of "org.mongodb:mongodb-driver-core"
from "org.springframework.data:spring-data-mongodb"
managedBy "Spring Data Bom"
}
}
group("org.mongodb") {
bom("mongodb-driver-bom")
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,9 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.gradle.testkit.runner.BuildResult;
@@ -61,9 +63,13 @@ class WarPluginActionIntegrationTests {
assertThat(result.task(":bootWar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.task(":war").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs");
assertThat(buildLibs.listFiles()).containsExactlyInAnyOrder(
new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war"),
new File(buildLibs, this.gradleBuild.getProjectDir().getName() + "-plain.war"));
List<File> expected = new ArrayList<>();
expected.add(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war"));
expected.add(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + "-plain.war"));
if (this.gradleBuild.gradleVersionIsAtLeast("9.0-milestone-2")) {
expected.add(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + "-plain.jar"));
}
assertThat(buildLibs.listFiles()).containsExactlyInAnyOrderElementsOf(expected);
}
@TestTemplate

View File

@@ -50,6 +50,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.loader.tools.FileUtils;
@@ -597,6 +598,7 @@ abstract class AbstractBootArchiveIntegrationTests {
@TestTemplate
void dirModeAndFileModeAreApplied() throws IOException {
Assumptions.assumeTrue(this.gradleBuild.gradleVersionIsLessThan("9.0-milestone-1"));
BuildResult result = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.8-rc-1")
.expectDeprecationMessages("The CopyProcessingSpec.setDirMode(Integer) method has been deprecated",
"The CopyProcessingSpec.setFileMode(Integer) method has been deprecated",

View File

@@ -25,6 +25,7 @@ import java.util.jar.JarFile;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.TestTemplate;
import org.springframework.boot.gradle.junit.GradleCompatibility;
@@ -56,6 +57,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
@TestTemplate
void whenAResolvableCopyOfAnUnresolvableConfigurationIsResolvedThenResolutionSucceeds() {
Assumptions.assumeTrue(this.gradleBuild.gradleVersionIsLessThan("9.0-milestone-1"));
this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.0").build("build");
}

View File

@@ -134,6 +134,10 @@ public class GradleBuild {
return GradleVersion.version(this.gradleVersion).compareTo(GradleVersion.version(version)) >= 0;
}
public boolean gradleVersionIsLessThan(String version) {
return GradleVersion.version(this.gradleVersion).compareTo(GradleVersion.version(version)) < 0;
}
public BuildResult build(String... arguments) {
try {
BuildResult result = prepareRunner(arguments).build();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,18 +34,18 @@ public final class GradleVersions {
public static List<String> allCompatible() {
if (isJavaVersion(JavaVersion.VERSION_24)) {
return Arrays.asList(GradleVersion.current().getVersion());
return Arrays.asList(GradleVersion.current().getVersion(), "9.0.0-milestone-9");
}
if (isJavaVersion(JavaVersion.VERSION_23)) {
return Arrays.asList("8.10", GradleVersion.current().getVersion());
return Arrays.asList(GradleVersion.current().getVersion(), "9.0.0-milestone-9");
}
if (isJavaVersion(JavaVersion.VERSION_22)) {
return Arrays.asList("8.8", GradleVersion.current().getVersion());
return Arrays.asList("8.8", GradleVersion.current().getVersion(), "9.0.0-milestone-9");
}
if (isJavaVersion(JavaVersion.VERSION_21)) {
return Arrays.asList("8.5", GradleVersion.current().getVersion());
return Arrays.asList("8.5", GradleVersion.current().getVersion(), "9.0.0-milestone-9");
}
return Arrays.asList("7.6.4", "8.4", GradleVersion.current().getVersion());
return Arrays.asList("7.6.4", "8.4", GradleVersion.current().getVersion(), "9.0.0-milestone-9");
}
public static String minimumCompatible() {