From c8e9a2a32c590226f6d71ca187c340e4f33845cf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jan 2024 15:20:29 +0000 Subject: [PATCH] Add support to Bomr for aligning dependency versions Closes gh-34114 --- .../boot/build/bom/BomExtension.java | 32 ++++- .../boot/build/bom/CheckBom.java | 28 ++++- .../boot/build/bom/Library.java | 117 +++++++++++++++++- .../MultithreadedLibraryUpdateResolver.java | 18 ++- .../bomr/StandardLibraryUpdateResolver.java | 23 +++- .../build/bom/bomr/UpgradeDependencies.java | 6 +- .../boot/build/bom/bomr/VersionOption.java | 11 +- .../bom/bomr/UpgradeApplicatorTests.java | 10 +- .../spring-boot-dependencies/build.gradle | 4 + 9 files changed, 220 insertions(+), 29 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java index 5291f719099..7fa7ed2f001 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -62,6 +62,7 @@ import org.springframework.boot.build.bom.Library.Group; import org.springframework.boot.build.bom.Library.LibraryVersion; import org.springframework.boot.build.bom.Library.Module; 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; import org.springframework.boot.build.mavenplugin.MavenExec; import org.springframework.util.FileCopyUtils; @@ -113,8 +114,12 @@ public class BomExtension { LibraryHandler libraryHandler = objects.newInstance(LibraryHandler.class, (version != null) ? version : ""); action.execute(libraryHandler); LibraryVersion libraryVersion = new LibraryVersion(DependencyVersion.parse(libraryHandler.version)); + VersionAlignment versionAlignment = (libraryHandler.alignWithVersion != null) + ? new VersionAlignment(libraryHandler.alignWithVersion.from, libraryHandler.alignWithVersion.managedBy, + this.project, this.libraries, libraryHandler.groups) + : null; addLibrary(new Library(name, libraryHandler.calendarName, libraryVersion, libraryHandler.groups, - libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots)); + libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots, versionAlignment)); } public void effectiveBomArtifact() { @@ -220,6 +225,8 @@ public class BomExtension { private String calendarName; + private AlignWithVersionHandler alignWithVersion; + @Inject public LibraryHandler(String version) { this.version = version; @@ -251,6 +258,11 @@ public class BomExtension { handler.endsWith, handler.contains, handler.reason)); } + public void alignWithVersion(Action action) { + this.alignWithVersion = new AlignWithVersionHandler(); + action.execute(this.alignWithVersion); + } + public static class ProhibitedHandler { private String reason; @@ -368,6 +380,22 @@ public class BomExtension { } + public static class AlignWithVersionHandler { + + private String from; + + private String managedBy; + + public void from(String from) { + this.from = from; + } + + public void managedBy(String managedBy) { + this.managedBy = managedBy; + } + + } + } public static class UpgradeHandler { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java index 7c2089e2596..3bd688c59ec 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -35,6 +35,7 @@ import org.gradle.api.tasks.TaskAction; import org.springframework.boot.build.bom.Library.Group; import org.springframework.boot.build.bom.Library.Module; 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; /** @@ -69,6 +70,7 @@ public class CheckBom extends DefaultTask { List libraryErrors = new ArrayList<>(); checkExclusions(library, libraryErrors); checkProhibitedVersions(library, libraryErrors); + checkVersionAlignment(library, libraryErrors); if (!libraryErrors.isEmpty()) { errors.add(library.getName()); for (String libraryError : libraryErrors) { @@ -148,4 +150,28 @@ public class CheckBom extends DefaultTask { } } + private void checkVersionAlignment(Library library, List errors) { + VersionAlignment versionAlignment = library.getVersionAlignment(); + if (versionAlignment == null) { + return; + } + Set alignedVersions = versionAlignment.resolve(); + if (alignedVersions.size() == 1) { + String alignedVersion = alignedVersions.iterator().next(); + if (!alignedVersion.equals(library.getVersion().getVersion().toString())) { + errors.add("Version " + library.getVersion().getVersion() + " is misaligned. It should be " + + alignedVersion + "."); + } + } + else { + if (alignedVersions.isEmpty()) { + errors.add("Version alignment requires a single version but none were found."); + } + else { + errors.add("Version alignment requires a single version but " + alignedVersions.size() + " were found: " + + alignedVersions + "."); + } + } + } + } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java index a2d6f4517cb..8fa5f37f2f6 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -16,12 +16,22 @@ package org.springframework.boot.build.bom; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Set; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.VersionRange; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.artifacts.result.DependencyResult; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; @@ -47,6 +57,8 @@ public class Library { private final boolean considerSnapshots; + private final VersionAlignment versionAlignment; + /** * Create a new {@code Library} with the given {@code name}, {@code version}, and * {@code groups}. @@ -57,9 +69,10 @@ public class Library { * @param groups groups in the library * @param prohibitedVersions version of the library that are prohibited * @param considerSnapshots whether to consider snapshots + * @param versionAlignment version alignment, if any, for the library */ public Library(String name, String calendarName, LibraryVersion version, List groups, - List prohibitedVersions, boolean considerSnapshots) { + List prohibitedVersions, boolean considerSnapshots, VersionAlignment versionAlignment) { this.name = name; this.calendarName = (calendarName != null) ? calendarName : name; this.version = version; @@ -68,6 +81,7 @@ public class Library { : name.toLowerCase(Locale.ENGLISH).replace(' ', '-') + ".version"; this.prohibitedVersions = prohibitedVersions; this.considerSnapshots = considerSnapshots; + this.versionAlignment = versionAlignment; } public String getName() { @@ -98,6 +112,10 @@ public class Library { return this.considerSnapshots; } + public VersionAlignment getVersionAlignment() { + return this.versionAlignment; + } + /** * A version or range of versions that are prohibited from being used in a bom. */ @@ -280,4 +298,99 @@ public class Library { } + /** + * Version alignment for a library. + */ + public static class VersionAlignment { + + private final String from; + + private final String managedBy; + + private final Project project; + + private final List libraries; + + private final List groups; + + private Set alignedVersions; + + VersionAlignment(String from, String managedBy, Project project, List libraries, List groups) { + this.from = from; + this.managedBy = managedBy; + this.project = project; + this.libraries = libraries; + this.groups = groups; + } + + public Set resolve() { + if (this.managedBy == null) { + throw new IllegalStateException("Version alignment without managedBy is not supported"); + } + if (this.alignedVersions != null) { + return this.alignedVersions; + } + Library managingLibrary = this.libraries.stream() + .filter((candidate) -> this.managedBy.equals(candidate.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Managing library '" + this.managedBy + "' not found.")); + Map versions = resolveAligningDependencies(managingLibrary); + Set 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; + return this.alignedVersions; + } + + private Map resolveAligningDependencies(Library manager) { + DependencyHandler dependencyHandler = this.project.getDependencies(); + List boms = manager.getGroups() + .stream() + .flatMap((group) -> group.getBoms() + .stream() + .map((bom) -> dependencyHandler + .platform(group.getId() + ":" + bom + ":" + manager.getVersion().getVersion()))) + .toList(); + List dependencies = new ArrayList<>(); + dependencies.addAll(boms); + dependencies.add(dependencyHandler.create(this.from)); + Configuration alignmentConfiguration = this.project.getConfigurations() + .detachedConfiguration(dependencies.toArray(new Dependency[0])); + Map versions = new HashMap<>(); + for (DependencyResult dependency : alignmentConfiguration.getIncoming() + .getResolutionResult() + .getAllDependencies()) { + versions.put(dependency.getFrom().getModuleVersion().getModule().toString(), + dependency.getFrom().getModuleVersion().getVersion()); + } + return versions; + } + + String getFrom() { + return this.from; + } + + String getManagedBy() { + return this.managedBy; + } + + @Override + public String toString() { + return "version from dependencies of " + this.from + " that is managed by " + this.managedBy; + } + + } + } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MultithreadedLibraryUpdateResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MultithreadedLibraryUpdateResolver.java index 18a5490b631..032ad59b1fb 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MultithreadedLibraryUpdateResolver.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MultithreadedLibraryUpdateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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,6 +20,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -57,11 +58,16 @@ class MultithreadedLibraryUpdateResolver implements LibraryUpdateResolver { LOGGER.info("Looking for updates using {} threads", this.threads); ExecutorService executorService = Executors.newFixedThreadPool(this.threads); try { - return librariesToUpgrade.stream() - .map((library) -> executorService.submit( - () -> this.delegate.findLibraryUpdates(Collections.singletonList(library), librariesByName))) - .flatMap(this::getResult) - .toList(); + return librariesToUpgrade.stream().map((library) -> { + if (library.getVersionAlignment() == null) { + return executorService.submit(() -> this.delegate + .findLibraryUpdates(Collections.singletonList(library), librariesByName)); + } + else { + return CompletableFuture.completedFuture( + this.delegate.findLibraryUpdates(Collections.singletonList(library), librariesByName)); + } + }).flatMap(this::getResult).toList(); } finally { executorService.shutdownNow(); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java index 34288672a14..ac354fb0f09 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.SortedSet; import java.util.function.BiPredicate; import java.util.stream.Collectors; @@ -33,6 +34,7 @@ import org.slf4j.LoggerFactory; import org.springframework.boot.build.bom.Library; import org.springframework.boot.build.bom.Library.Group; import org.springframework.boot.build.bom.Library.Module; +import org.springframework.boot.build.bom.Library.VersionAlignment; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; /** @@ -65,7 +67,7 @@ class StandardLibraryUpdateResolver implements LibraryUpdateResolver { } LOGGER.info("Looking for updates for {}", library.getName()); long start = System.nanoTime(); - List versionOptions = getVersionOptions(library, librariesByName); + List versionOptions = getVersionOptions(library); result.add(new LibraryWithVersionOptions(library, versionOptions)); LOGGER.info("Found {} updates for {}, took {}", versionOptions.size(), library.getName(), Duration.ofNanos(System.nanoTime() - start)); @@ -77,8 +79,21 @@ class StandardLibraryUpdateResolver implements LibraryUpdateResolver { return library.getName().equals("Spring Boot"); } - protected List getVersionOptions(Library library, Map libraries) { - return determineResolvedVersionOptions(library); + protected List getVersionOptions(Library library) { + VersionOption option = determineAlignedVersionOption(library); + return (option != null) ? List.of(option) : determineResolvedVersionOptions(library); + } + + private VersionOption determineAlignedVersionOption(Library library) { + VersionAlignment versionAlignment = library.getVersionAlignment(); + if (versionAlignment != null) { + Set alignedVersions = versionAlignment.resolve(); + if (alignedVersions != null && alignedVersions.size() == 1) { + return new VersionOption.AlignedVersionOption( + DependencyVersion.parse(alignedVersions.iterator().next()), versionAlignment); + } + } + return null; } private List determineResolvedVersionOptions(Library library) { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java index 81511606b71..04934445cd8 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -226,13 +226,13 @@ public abstract class UpgradeDependencies extends DefaultTask { protected List> determineUpdatePredicates(Milestone milestone) { List> updatePredicates = new ArrayList<>(); - updatePredicates.add(this::compilesWithUpgradePolicy); + updatePredicates.add(this::compliesWithUpgradePolicy); updatePredicates.add(this::isAnUpgrade); updatePredicates.add(this::isNotProhibited); return updatePredicates; } - private boolean compilesWithUpgradePolicy(Library library, DependencyVersion candidate) { + private boolean compliesWithUpgradePolicy(Library library, DependencyVersion candidate) { return this.bom.getUpgrade().getPolicy().test(candidate, library.getVersion().getVersion()); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/VersionOption.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/VersionOption.java index def90fe4506..d1d33fd8238 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/VersionOption.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/VersionOption.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -18,7 +18,7 @@ package org.springframework.boot.build.bom.bomr; import java.util.List; -import org.springframework.boot.build.bom.Library; +import org.springframework.boot.build.bom.Library.VersionAlignment; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; import org.springframework.util.StringUtils; @@ -46,17 +46,16 @@ class VersionOption { static final class AlignedVersionOption extends VersionOption { - private final Library alignedWith; + private final VersionAlignment alignedWith; - AlignedVersionOption(DependencyVersion version, Library alignedWith) { + AlignedVersionOption(DependencyVersion version, VersionAlignment alignedWith) { super(version); this.alignedWith = alignedWith; } @Override public String toString() { - return super.toString() + " (aligned with " + this.alignedWith.getName() + " " - + this.alignedWith.getVersion().getVersion() + ")"; + return super.toString() + " (aligned with " + this.alignedWith + ")"; } } diff --git a/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/UpgradeApplicatorTests.java b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/UpgradeApplicatorTests.java index af184a2be53..7ef6061fb11 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/UpgradeApplicatorTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/UpgradeApplicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -53,7 +53,7 @@ class UpgradeApplicatorTests { FileCopyUtils.copy(new File("src/test/resources/gradle.properties"), gradleProperties); new UpgradeApplicator(bom.toPath(), gradleProperties.toPath()) .apply(new Upgrade(new Library("ActiveMQ", null, new LibraryVersion(DependencyVersion.parse("5.15.11")), - null, null, false), DependencyVersion.parse("5.16"))); + null, null, false, null), DependencyVersion.parse("5.16"))); String bomContents = Files.readString(bom.toPath()); assertThat(bomContents).hasSize(originalContents.length() - 3); } @@ -64,9 +64,9 @@ class UpgradeApplicatorTests { FileCopyUtils.copy(new File("src/test/resources/bom.gradle"), bom); File gradleProperties = new File(this.temp, "gradle.properties"); FileCopyUtils.copy(new File("src/test/resources/gradle.properties"), gradleProperties); - new UpgradeApplicator(bom.toPath(), gradleProperties.toPath()).apply(new Upgrade( - new Library("Kotlin", null, new LibraryVersion(DependencyVersion.parse("1.3.70")), null, null, false), - DependencyVersion.parse("1.4"))); + new UpgradeApplicator(bom.toPath(), gradleProperties.toPath()) + .apply(new Upgrade(new Library("Kotlin", null, new LibraryVersion(DependencyVersion.parse("1.3.70")), null, + null, false, null), DependencyVersion.parse("1.4"))); Properties properties = new Properties(); try (InputStream in = new FileInputStream(gradleProperties)) { properties.load(in); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d6556b06d0a..ec7053a579a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1051,6 +1051,10 @@ bom { } } library("Neo4j Java Driver", "5.13.0") { + alignWithVersion { + from "org.springframework.data:spring-data-neo4j" + managedBy "Spring Data Bom" + } group("org.neo4j.driver") { modules = [ "neo4j-java-driver"