Skip to content

Commit c8e9a2a

Browse files
committed
Add support to Bomr for aligning dependency versions
Closes gh-34114
1 parent 5f4d347 commit c8e9a2a

File tree

9 files changed

+220
-29
lines changed

9 files changed

+220
-29
lines changed

buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -62,6 +62,7 @@
6262
import org.springframework.boot.build.bom.Library.LibraryVersion;
6363
import org.springframework.boot.build.bom.Library.Module;
6464
import org.springframework.boot.build.bom.Library.ProhibitedVersion;
65+
import org.springframework.boot.build.bom.Library.VersionAlignment;
6566
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
6667
import org.springframework.boot.build.mavenplugin.MavenExec;
6768
import org.springframework.util.FileCopyUtils;
@@ -113,8 +114,12 @@ public void library(String name, String version, Action<LibraryHandler> action)
113114
LibraryHandler libraryHandler = objects.newInstance(LibraryHandler.class, (version != null) ? version : "");
114115
action.execute(libraryHandler);
115116
LibraryVersion libraryVersion = new LibraryVersion(DependencyVersion.parse(libraryHandler.version));
117+
VersionAlignment versionAlignment = (libraryHandler.alignWithVersion != null)
118+
? new VersionAlignment(libraryHandler.alignWithVersion.from, libraryHandler.alignWithVersion.managedBy,
119+
this.project, this.libraries, libraryHandler.groups)
120+
: null;
116121
addLibrary(new Library(name, libraryHandler.calendarName, libraryVersion, libraryHandler.groups,
117-
libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots));
122+
libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots, versionAlignment));
118123
}
119124

120125
public void effectiveBomArtifact() {
@@ -220,6 +225,8 @@ public static class LibraryHandler {
220225

221226
private String calendarName;
222227

228+
private AlignWithVersionHandler alignWithVersion;
229+
223230
@Inject
224231
public LibraryHandler(String version) {
225232
this.version = version;
@@ -251,6 +258,11 @@ public void prohibit(Action<ProhibitedHandler> action) {
251258
handler.endsWith, handler.contains, handler.reason));
252259
}
253260

261+
public void alignWithVersion(Action<AlignWithVersionHandler> action) {
262+
this.alignWithVersion = new AlignWithVersionHandler();
263+
action.execute(this.alignWithVersion);
264+
}
265+
254266
public static class ProhibitedHandler {
255267

256268
private String reason;
@@ -368,6 +380,22 @@ public void setClassifier(String classifier) {
368380

369381
}
370382

383+
public static class AlignWithVersionHandler {
384+
385+
private String from;
386+
387+
private String managedBy;
388+
389+
public void from(String from) {
390+
this.from = from;
391+
}
392+
393+
public void managedBy(String managedBy) {
394+
this.managedBy = managedBy;
395+
}
396+
397+
}
398+
371399
}
372400

373401
public static class UpgradeHandler {

buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@
3535
import org.springframework.boot.build.bom.Library.Group;
3636
import org.springframework.boot.build.bom.Library.Module;
3737
import org.springframework.boot.build.bom.Library.ProhibitedVersion;
38+
import org.springframework.boot.build.bom.Library.VersionAlignment;
3839
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
3940

4041
/**
@@ -69,6 +70,7 @@ private void checkLibrary(Library library, List<String> errors) {
6970
List<String> libraryErrors = new ArrayList<>();
7071
checkExclusions(library, libraryErrors);
7172
checkProhibitedVersions(library, libraryErrors);
73+
checkVersionAlignment(library, libraryErrors);
7274
if (!libraryErrors.isEmpty()) {
7375
errors.add(library.getName());
7476
for (String libraryError : libraryErrors) {
@@ -148,4 +150,28 @@ private void checkProhibitedVersions(Library library, List<String> errors) {
148150
}
149151
}
150152

153+
private void checkVersionAlignment(Library library, List<String> errors) {
154+
VersionAlignment versionAlignment = library.getVersionAlignment();
155+
if (versionAlignment == null) {
156+
return;
157+
}
158+
Set<String> alignedVersions = versionAlignment.resolve();
159+
if (alignedVersions.size() == 1) {
160+
String alignedVersion = alignedVersions.iterator().next();
161+
if (!alignedVersion.equals(library.getVersion().getVersion().toString())) {
162+
errors.add("Version " + library.getVersion().getVersion() + " is misaligned. It should be "
163+
+ alignedVersion + ".");
164+
}
165+
}
166+
else {
167+
if (alignedVersions.isEmpty()) {
168+
errors.add("Version alignment requires a single version but none were found.");
169+
}
170+
else {
171+
errors.add("Version alignment requires a single version but " + alignedVersions.size() + " were found: "
172+
+ alignedVersions + ".");
173+
}
174+
}
175+
}
176+
151177
}

buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,12 +16,22 @@
1616

1717
package org.springframework.boot.build.bom;
1818

19+
import java.util.ArrayList;
1920
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.HashSet;
2023
import java.util.List;
2124
import java.util.Locale;
25+
import java.util.Map;
26+
import java.util.Set;
2227

2328
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
2429
import org.apache.maven.artifact.versioning.VersionRange;
30+
import org.gradle.api.Project;
31+
import org.gradle.api.artifacts.Configuration;
32+
import org.gradle.api.artifacts.Dependency;
33+
import org.gradle.api.artifacts.dsl.DependencyHandler;
34+
import org.gradle.api.artifacts.result.DependencyResult;
2535

2636
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
2737

@@ -47,6 +57,8 @@ public class Library {
4757

4858
private final boolean considerSnapshots;
4959

60+
private final VersionAlignment versionAlignment;
61+
5062
/**
5163
* Create a new {@code Library} with the given {@code name}, {@code version}, and
5264
* {@code groups}.
@@ -57,9 +69,10 @@ public class Library {
5769
* @param groups groups in the library
5870
* @param prohibitedVersions version of the library that are prohibited
5971
* @param considerSnapshots whether to consider snapshots
72+
* @param versionAlignment version alignment, if any, for the library
6073
*/
6174
public Library(String name, String calendarName, LibraryVersion version, List<Group> groups,
62-
List<ProhibitedVersion> prohibitedVersions, boolean considerSnapshots) {
75+
List<ProhibitedVersion> prohibitedVersions, boolean considerSnapshots, VersionAlignment versionAlignment) {
6376
this.name = name;
6477
this.calendarName = (calendarName != null) ? calendarName : name;
6578
this.version = version;
@@ -68,6 +81,7 @@ public Library(String name, String calendarName, LibraryVersion version, List<Gr
6881
: name.toLowerCase(Locale.ENGLISH).replace(' ', '-') + ".version";
6982
this.prohibitedVersions = prohibitedVersions;
7083
this.considerSnapshots = considerSnapshots;
84+
this.versionAlignment = versionAlignment;
7185
}
7286

7387
public String getName() {
@@ -98,6 +112,10 @@ public boolean isConsiderSnapshots() {
98112
return this.considerSnapshots;
99113
}
100114

115+
public VersionAlignment getVersionAlignment() {
116+
return this.versionAlignment;
117+
}
118+
101119
/**
102120
* A version or range of versions that are prohibited from being used in a bom.
103121
*/
@@ -280,4 +298,99 @@ public String getArtifactId() {
280298

281299
}
282300

301+
/**
302+
* Version alignment for a library.
303+
*/
304+
public static class VersionAlignment {
305+
306+
private final String from;
307+
308+
private final String managedBy;
309+
310+
private final Project project;
311+
312+
private final List<Library> libraries;
313+
314+
private final List<Group> groups;
315+
316+
private Set<String> alignedVersions;
317+
318+
VersionAlignment(String from, String managedBy, Project project, List<Library> libraries, List<Group> groups) {
319+
this.from = from;
320+
this.managedBy = managedBy;
321+
this.project = project;
322+
this.libraries = libraries;
323+
this.groups = groups;
324+
}
325+
326+
public Set<String> resolve() {
327+
if (this.managedBy == null) {
328+
throw new IllegalStateException("Version alignment without managedBy is not supported");
329+
}
330+
if (this.alignedVersions != null) {
331+
return this.alignedVersions;
332+
}
333+
Library managingLibrary = this.libraries.stream()
334+
.filter((candidate) -> this.managedBy.equals(candidate.getName()))
335+
.findFirst()
336+
.orElseThrow(() -> new IllegalStateException("Managing library '" + this.managedBy + "' not found."));
337+
Map<String, String> versions = resolveAligningDependencies(managingLibrary);
338+
Set<String> versionsInLibrary = new HashSet<>();
339+
for (Group group : this.groups) {
340+
for (Module module : group.getModules()) {
341+
String version = versions.get(group.getId() + ":" + module.getName());
342+
if (version != null) {
343+
versionsInLibrary.add(version);
344+
}
345+
}
346+
for (String plugin : group.getPlugins()) {
347+
String version = versions.get(group.getId() + ":" + plugin);
348+
if (version != null) {
349+
versionsInLibrary.add(version);
350+
}
351+
}
352+
}
353+
this.alignedVersions = versionsInLibrary;
354+
return this.alignedVersions;
355+
}
356+
357+
private Map<String, String> resolveAligningDependencies(Library manager) {
358+
DependencyHandler dependencyHandler = this.project.getDependencies();
359+
List<Dependency> boms = manager.getGroups()
360+
.stream()
361+
.flatMap((group) -> group.getBoms()
362+
.stream()
363+
.map((bom) -> dependencyHandler
364+
.platform(group.getId() + ":" + bom + ":" + manager.getVersion().getVersion())))
365+
.toList();
366+
List<Dependency> dependencies = new ArrayList<>();
367+
dependencies.addAll(boms);
368+
dependencies.add(dependencyHandler.create(this.from));
369+
Configuration alignmentConfiguration = this.project.getConfigurations()
370+
.detachedConfiguration(dependencies.toArray(new Dependency[0]));
371+
Map<String, String> versions = new HashMap<>();
372+
for (DependencyResult dependency : alignmentConfiguration.getIncoming()
373+
.getResolutionResult()
374+
.getAllDependencies()) {
375+
versions.put(dependency.getFrom().getModuleVersion().getModule().toString(),
376+
dependency.getFrom().getModuleVersion().getVersion());
377+
}
378+
return versions;
379+
}
380+
381+
String getFrom() {
382+
return this.from;
383+
}
384+
385+
String getManagedBy() {
386+
return this.managedBy;
387+
}
388+
389+
@Override
390+
public String toString() {
391+
return "version from dependencies of " + this.from + " that is managed by " + this.managedBy;
392+
}
393+
394+
}
395+
283396
}

buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MultithreadedLibraryUpdateResolver.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.util.Collections;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.concurrent.CompletableFuture;
2324
import java.util.concurrent.ExecutionException;
2425
import java.util.concurrent.ExecutorService;
2526
import java.util.concurrent.Executors;
@@ -57,11 +58,16 @@ public List<LibraryWithVersionOptions> findLibraryUpdates(Collection<Library> li
5758
LOGGER.info("Looking for updates using {} threads", this.threads);
5859
ExecutorService executorService = Executors.newFixedThreadPool(this.threads);
5960
try {
60-
return librariesToUpgrade.stream()
61-
.map((library) -> executorService.submit(
62-
() -> this.delegate.findLibraryUpdates(Collections.singletonList(library), librariesByName)))
63-
.flatMap(this::getResult)
64-
.toList();
61+
return librariesToUpgrade.stream().map((library) -> {
62+
if (library.getVersionAlignment() == null) {
63+
return executorService.submit(() -> this.delegate
64+
.findLibraryUpdates(Collections.singletonList(library), librariesByName));
65+
}
66+
else {
67+
return CompletableFuture.completedFuture(
68+
this.delegate.findLibraryUpdates(Collections.singletonList(library), librariesByName));
69+
}
70+
}).flatMap(this::getResult).toList();
6571
}
6672
finally {
6773
executorService.shutdownNow();

buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import java.util.LinkedHashMap;
2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.Set;
2627
import java.util.SortedSet;
2728
import java.util.function.BiPredicate;
2829
import java.util.stream.Collectors;
@@ -33,6 +34,7 @@
3334
import org.springframework.boot.build.bom.Library;
3435
import org.springframework.boot.build.bom.Library.Group;
3536
import org.springframework.boot.build.bom.Library.Module;
37+
import org.springframework.boot.build.bom.Library.VersionAlignment;
3638
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
3739

3840
/**
@@ -65,7 +67,7 @@ public List<LibraryWithVersionOptions> findLibraryUpdates(Collection<Library> li
6567
}
6668
LOGGER.info("Looking for updates for {}", library.getName());
6769
long start = System.nanoTime();
68-
List<VersionOption> versionOptions = getVersionOptions(library, librariesByName);
70+
List<VersionOption> versionOptions = getVersionOptions(library);
6971
result.add(new LibraryWithVersionOptions(library, versionOptions));
7072
LOGGER.info("Found {} updates for {}, took {}", versionOptions.size(), library.getName(),
7173
Duration.ofNanos(System.nanoTime() - start));
@@ -77,8 +79,21 @@ protected boolean isLibraryExcluded(Library library) {
7779
return library.getName().equals("Spring Boot");
7880
}
7981

80-
protected List<VersionOption> getVersionOptions(Library library, Map<String, Library> libraries) {
81-
return determineResolvedVersionOptions(library);
82+
protected List<VersionOption> getVersionOptions(Library library) {
83+
VersionOption option = determineAlignedVersionOption(library);
84+
return (option != null) ? List.of(option) : determineResolvedVersionOptions(library);
85+
}
86+
87+
private VersionOption determineAlignedVersionOption(Library library) {
88+
VersionAlignment versionAlignment = library.getVersionAlignment();
89+
if (versionAlignment != null) {
90+
Set<String> alignedVersions = versionAlignment.resolve();
91+
if (alignedVersions != null && alignedVersions.size() == 1) {
92+
return new VersionOption.AlignedVersionOption(
93+
DependencyVersion.parse(alignedVersions.iterator().next()), versionAlignment);
94+
}
95+
}
96+
return null;
8297
}
8398

8499
private List<VersionOption> determineResolvedVersionOptions(Library library) {

0 commit comments

Comments
 (0)