diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java index f6189567d61..bb100034e16 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java @@ -65,6 +65,8 @@ public class BomPlugin implements Plugin { TaskProvider createResolvedBom = project.getTasks() .register("createResolvedBom", CreateResolvedBom.class, bom); TaskProvider checkBom = project.getTasks().register("bomrCheck", CheckBom.class, bom); + checkBom.configure( + (task) -> task.getResolvedBomFile().set(createResolvedBom.flatMap(CreateResolvedBom::getOutputFile))); project.getTasks().named("check").configure((check) -> check.dependsOn(checkBom)); project.getTasks().register("bomrUpgrade", UpgradeBom.class, bom); project.getTasks().register("moveToSnapshots", MoveToSnapshots.class, bom); 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 250aa5d559e..7f2d73ea8d7 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-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. @@ -16,9 +16,9 @@ package org.springframework.boot.build.bom; -import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; @@ -32,15 +32,22 @@ import org.apache.maven.artifact.versioning.VersionRange; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.artifacts.ConfigurationContainer; -import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.file.RegularFile; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; 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.ManagedDependencies.Difference; +import org.springframework.boot.build.bom.ResolvedBom.Bom; +import org.springframework.boot.build.bom.ResolvedBom.Id; +import org.springframework.boot.build.bom.ResolvedBom.ResolvedLibrary; import org.springframework.boot.build.bom.bomr.version.DependencyVersion; /** @@ -51,19 +58,29 @@ import org.springframework.boot.build.bom.bomr.version.DependencyVersion; */ public abstract class CheckBom extends DefaultTask { + private final Provider resolvedBom; + private final ConfigurationContainer configurations; private final DependencyHandler dependencies; private final BomExtension bom; + private final BomResolver bomResolver; + @Inject public CheckBom(BomExtension bom) { - this.bom = bom; this.configurations = getProject().getConfigurations(); this.dependencies = getProject().getDependencies(); + this.bom = bom; + this.resolvedBom = getResolvedBomFile().map(RegularFile::getAsFile).map(ResolvedBom::readFrom); + this.bomResolver = new BomResolver(this.configurations, this.dependencies); } + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + abstract RegularFileProperty getResolvedBomFile(); + @TaskAction void checkBom() { List errors = new ArrayList<>(); @@ -191,35 +208,52 @@ public abstract class CheckBom extends DefaultTask { if (alignsWithBom == null) { return; } - File bom = resolveBom(library, alignsWithBom); - ManagedDependencies managedByBom = ManagedDependencies.ofBom(bom); - ManagedDependencies managedByLibrary = ManagedDependencies.ofLibrary(library); - Difference diff = managedByBom.diff(managedByLibrary); - if (!diff.isEmpty()) { - String error = "Dependency management does not align with " + library.getAlignsWithBom() + ":"; - if (!diff.missing().isEmpty()) { - error = error + "%n - Missing:%n %s" - .formatted(String.join("\n ", diff.missing())); - } - if (!diff.unexpected().isEmpty()) { - error = error + "%n - Unexpected:%n %s" - .formatted(String.join("\n ", diff.unexpected())); - } - errors.add(error); + Bom mavenBom = this.bomResolver.resolveMavenBom(alignsWithBom + ":" + library.getVersion().getVersion()); + ResolvedBom resolvedBom = this.resolvedBom.get(); + Optional resolvedLibrary = resolvedBom.libraries() + .stream() + .filter((candidate) -> candidate.name().equals(library.getName())) + .findFirst(); + if (!resolvedLibrary.isPresent()) { + throw new RuntimeException("Library '%s' not found in resolved bom".formatted(library.getName())); } + checkDependencyManagementAlignment(resolvedLibrary.get(), mavenBom, errors); } - private File resolveBom(Library library, String alignsWithBom) { - String coordinates = alignsWithBom + ":" + library.getVersion().getVersion() + "@pom"; - Set artifacts = this.configurations - .detachedConfiguration(this.dependencies.create(coordinates)) - .getResolvedConfiguration() - .getResolvedArtifacts(); - if (artifacts.size() != 1) { - throw new IllegalStateException("Expected a single file but '%s' resolved to %d artifacts" - .formatted(coordinates, artifacts.size())); + private void checkDependencyManagementAlignment(ResolvedLibrary library, Bom mavenBom, List errors) { + List managedByLibrary = library.managedDependencies(); + List managedByBom = managedDependenciesOf(mavenBom); + + List missing = new ArrayList<>(managedByBom); + missing.removeAll(managedByLibrary); + + List unexpected = new ArrayList<>(managedByLibrary); + unexpected.removeAll(managedByBom); + if (missing.isEmpty() && unexpected.isEmpty()) { + return; + } + String error = "Dependency management does not align with " + mavenBom.id() + ":"; + if (!missing.isEmpty()) { + error = error + "%n - Missing:%n %s".formatted(String.join("\n ", + missing.stream().map((dependency) -> dependency.toString()).toList())); + } + if (!unexpected.isEmpty()) { + error = error + "%n - Unexpected:%n %s".formatted(String.join("\n ", + unexpected.stream().map((dependency) -> dependency.toString()).toList())); + } + errors.add(error); + } + + private List managedDependenciesOf(Bom mavenBom) { + List managedDependencies = new ArrayList<>(); + managedDependencies.addAll(mavenBom.managedDependencies()); + if (mavenBom.parent() != null) { + managedDependencies.addAll(managedDependenciesOf(mavenBom.parent())); + } + for (Bom importedBom : mavenBom.importedBoms()) { + managedDependencies.addAll(managedDependenciesOf(importedBom)); } - return artifacts.iterator().next().getFile(); + return managedDependencies; } } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/CreateResolvedBom.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/CreateResolvedBom.java index 5dd62a0e102..f84c69ffa92 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/CreateResolvedBom.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/CreateResolvedBom.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 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. @@ -40,6 +40,7 @@ public abstract class CreateResolvedBom extends DefaultTask { @Inject public CreateResolvedBom(BomExtension bomExtension) { + getOutputs().upToDateWhen((spec) -> false); this.bomExtension = bomExtension; this.bomResolver = new BomResolver(getProject().getConfigurations(), getProject().getDependencies()); getOutputFile().convention(getProject().getLayout().getBuildDirectory().file(getName() + "/resolved-bom.json")); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/ManagedDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/ManagedDependencies.java deleted file mode 100644 index c9066aa053b..00000000000 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/ManagedDependencies.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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. - * 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.springframework.boot.build.bom; - -import java.io.File; -import java.io.FileReader; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathFactory; - -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import org.springframework.boot.build.bom.Library.Group; -import org.springframework.boot.build.bom.Library.Module; - -/** - * Managed dependencies from a bom or library. - * - * @author Andy Wilkinson - */ -class ManagedDependencies { - - private final Set ids; - - ManagedDependencies(Set ids) { - this.ids = ids; - } - - Set getIds() { - return this.ids; - } - - Difference diff(ManagedDependencies other) { - Set missing = new HashSet<>(this.ids); - missing.removeAll(other.ids); - Set unexpected = new HashSet<>(other.ids); - unexpected.removeAll(this.ids); - return new Difference(missing, unexpected); - } - - static ManagedDependencies ofBom(File bom) { - try { - Document bomDocument = DocumentBuilderFactory.newInstance() - .newDocumentBuilder() - .parse(new InputSource(new FileReader(bom))); - XPath xpath = XPathFactory.newInstance().newXPath(); - NodeList dependencyNodes = (NodeList) xpath - .evaluate("/project/dependencyManagement/dependencies/dependency", bomDocument, XPathConstants.NODESET); - NodeList propertyNodes = (NodeList) xpath.evaluate("/project/properties/*", bomDocument, - XPathConstants.NODESET); - Map properties = new HashMap<>(); - for (int i = 0; i < propertyNodes.getLength(); i++) { - Node property = propertyNodes.item(i); - String name = property.getNodeName(); - String value = property.getTextContent(); - properties.put("${%s}".formatted(name), value); - } - Set managedDependencies = new HashSet<>(); - for (int i = 0; i < dependencyNodes.getLength(); i++) { - Node dependency = dependencyNodes.item(i); - String groupId = (String) xpath.evaluate("groupId/text()", dependency, XPathConstants.STRING); - String artifactId = (String) xpath.evaluate("artifactId/text()", dependency, XPathConstants.STRING); - String version = (String) xpath.evaluate("version/text()", dependency, XPathConstants.STRING); - String classifier = (String) xpath.evaluate("classifier/text()", dependency, XPathConstants.STRING); - if (version.startsWith("${") && version.endsWith("}")) { - version = properties.get(version); - } - managedDependencies.add(asId(groupId, artifactId, version, classifier)); - } - return new ManagedDependencies(managedDependencies); - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - static String asId(String groupId, String artifactId, String version, String classifier) { - String id = groupId + ":" + artifactId + ":" + version; - if (classifier != null && classifier.length() > 0) { - id = id + ":" + classifier; - } - return id; - } - - static ManagedDependencies ofLibrary(Library library) { - Set managedByLibrary = new HashSet<>(); - for (Group group : library.getGroups()) { - for (Module module : group.getModules()) { - managedByLibrary.add(asId(group.getId(), module.getName(), library.getVersion().getVersion().toString(), - module.getClassifier())); - } - } - return new ManagedDependencies(managedByLibrary); - } - - record Difference(Set missing, Set unexpected) { - - boolean isEmpty() { - return this.missing.isEmpty() && this.unexpected.isEmpty(); - } - - } - -}