Rewrite process for documenting managed dependencies
Previously, managed dependencies were documented using Gradle's dependency constraints. This has proven to be non-deterministic for reasons that are not fully understood. The working theory is that the constraints that are documented vary depending on the tasks that the build has run at the point at which the constraints are being examined and documented. This commit replaces approach with one that builds a model of a resolved bom by examining the configured bom extension and the XML of the Maven boms that it imports. This model is written to disk from where it can then be consumed as a dependency on other projects. The existing tasks for documenting the constrained versions and version properties have been rewritten to use the resolved bom model instead. Closes gh-44855
This commit is contained in:
parent
b67a6551f9
commit
7b2d90811d
|
|
@ -82,6 +82,8 @@ import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
|||
*/
|
||||
public class BomExtension {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final Project project;
|
||||
|
||||
private final UpgradeHandler upgradeHandler;
|
||||
|
|
@ -95,6 +97,11 @@ public class BomExtension {
|
|||
public BomExtension(Project project) {
|
||||
this.project = project;
|
||||
this.upgradeHandler = project.getObjects().newInstance(UpgradeHandler.class, project);
|
||||
this.id = "%s:%s:%s".formatted(project.getGroup(), project.getName(), project.getVersion());
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public List<Library> getLibraries() {
|
||||
|
|
|
|||
|
|
@ -62,11 +62,17 @@ public class BomPlugin implements Plugin<Project> {
|
|||
javaPlatform.allowDependencies();
|
||||
createApiEnforcedConfiguration(project);
|
||||
BomExtension bom = project.getExtensions().create("bom", BomExtension.class, project);
|
||||
TaskProvider<CreateResolvedBom> createResolvedBom = project.getTasks()
|
||||
.register("createResolvedBom", CreateResolvedBom.class, bom);
|
||||
TaskProvider<CheckBom> checkBom = project.getTasks().register("bomrCheck", CheckBom.class, bom);
|
||||
project.getTasks().named("check").configure((check) -> check.dependsOn(checkBom));
|
||||
project.getTasks().register("bomrUpgrade", UpgradeBom.class, bom);
|
||||
project.getTasks().register("moveToSnapshots", MoveToSnapshots.class, bom);
|
||||
project.getTasks().register("checkLinks", CheckLinks.class, bom);
|
||||
Configuration resolvedBomConfiguration = project.getConfigurations().create("resolvedBom");
|
||||
project.getArtifacts()
|
||||
.add(resolvedBomConfiguration.getName(), createResolvedBom.map(CreateResolvedBom::getOutputFile),
|
||||
(artifact) -> artifact.builtBy(createResolvedBom));
|
||||
new PublishingCustomizer(project, bom).customize();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* 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.
|
||||
* 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.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import org.gradle.api.artifacts.ConfigurationContainer;
|
||||
import org.gradle.api.artifacts.ResolvedArtifact;
|
||||
import org.gradle.api.artifacts.dsl.DependencyHandler;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import org.springframework.boot.build.bom.Library.Group;
|
||||
import org.springframework.boot.build.bom.Library.Module;
|
||||
import org.springframework.boot.build.bom.ResolvedBom.Bom;
|
||||
import org.springframework.boot.build.bom.ResolvedBom.Id;
|
||||
import org.springframework.boot.build.bom.ResolvedBom.ResolvedLibrary;
|
||||
|
||||
/**
|
||||
* Creates a {@link ResolvedBom resolved bom}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class BomResolver {
|
||||
|
||||
private final ConfigurationContainer configurations;
|
||||
|
||||
private final DependencyHandler dependencies;
|
||||
|
||||
private final DocumentBuilder documentBuilder;
|
||||
|
||||
BomResolver(ConfigurationContainer configurations, DependencyHandler dependencies) {
|
||||
this.configurations = configurations;
|
||||
this.dependencies = dependencies;
|
||||
try {
|
||||
this.documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
}
|
||||
catch (ParserConfigurationException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
ResolvedBom resolve(BomExtension bomExtension) {
|
||||
List<ResolvedLibrary> libraries = new ArrayList<>();
|
||||
for (Library library : bomExtension.getLibraries()) {
|
||||
List<Id> managedDependencies = new ArrayList<>();
|
||||
List<Bom> imports = new ArrayList<>();
|
||||
for (Group group : library.getGroups()) {
|
||||
for (Module module : group.getModules()) {
|
||||
Id id = new Id(group.getId(), module.getName(), library.getVersion().getVersion().toString());
|
||||
managedDependencies.add(id);
|
||||
}
|
||||
for (String imported : group.getBoms()) {
|
||||
Bom bom = bomFrom(resolveBom(
|
||||
"%s:%s:%s".formatted(group.getId(), imported, library.getVersion().getVersion())));
|
||||
imports.add(bom);
|
||||
}
|
||||
}
|
||||
ResolvedLibrary resolvedLibrary = new ResolvedLibrary(library.getName(),
|
||||
library.getVersion().getVersion().toString(), library.getVersionProperty(), managedDependencies,
|
||||
imports);
|
||||
libraries.add(resolvedLibrary);
|
||||
}
|
||||
String[] idComponents = bomExtension.getId().split(":");
|
||||
return new ResolvedBom(new Id(idComponents[0], idComponents[1], idComponents[2]), libraries);
|
||||
}
|
||||
|
||||
Bom resolveMavenBom(String coordinates) {
|
||||
return bomFrom(resolveBom(coordinates));
|
||||
}
|
||||
|
||||
private File resolveBom(String coordinates) {
|
||||
Set<ResolvedArtifact> artifacts = this.configurations
|
||||
.detachedConfiguration(this.dependencies.create(coordinates + "@pom"))
|
||||
.getResolvedConfiguration()
|
||||
.getResolvedArtifacts();
|
||||
if (artifacts.size() != 1) {
|
||||
throw new IllegalStateException("Expected a single artifact but '%s' resolved to %d artifacts"
|
||||
.formatted(coordinates, artifacts.size()));
|
||||
}
|
||||
return artifacts.iterator().next().getFile();
|
||||
}
|
||||
|
||||
private Bom bomFrom(File bomFile) {
|
||||
try {
|
||||
Node bom = nodeFrom(bomFile);
|
||||
File parentBomFile = parentBomFile(bom);
|
||||
Bom parent = null;
|
||||
if (parentBomFile != null) {
|
||||
parent = bomFrom(parentBomFile);
|
||||
}
|
||||
Properties properties = Properties.from(bom, this::nodeFrom);
|
||||
List<Node> dependencyNodes = bom.nodesAt("/project/dependencyManagement/dependencies/dependency");
|
||||
List<Id> managedDependencies = new ArrayList<>();
|
||||
List<Bom> imports = new ArrayList<>();
|
||||
for (Node dependency : dependencyNodes) {
|
||||
String groupId = properties.replace(dependency.textAt("groupId"));
|
||||
String artifactId = properties.replace(dependency.textAt("artifactId"));
|
||||
String version = properties.replace(dependency.textAt("version"));
|
||||
String classifier = properties.replace(dependency.textAt("classifier"));
|
||||
String scope = properties.replace(dependency.textAt("scope"));
|
||||
Bom importedBom = null;
|
||||
if ("import".equals(scope)) {
|
||||
String type = properties.replace(dependency.textAt("type"));
|
||||
if ("pom".equals(type)) {
|
||||
importedBom = bomFrom(resolveBom(groupId + ":" + artifactId + ":" + version));
|
||||
}
|
||||
}
|
||||
if (importedBom != null) {
|
||||
imports.add(importedBom);
|
||||
}
|
||||
else {
|
||||
managedDependencies.add(new Id(groupId, artifactId, version, classifier));
|
||||
}
|
||||
}
|
||||
String groupId = bom.textAt("/project/groupId");
|
||||
if ((groupId == null || groupId.isEmpty()) && parent != null) {
|
||||
groupId = parent.id().groupId();
|
||||
}
|
||||
String artifactId = bom.textAt("/project/artifactId");
|
||||
String version = bom.textAt("/project/version");
|
||||
if ((version == null || version.isEmpty()) && parent != null) {
|
||||
version = parent.id().version();
|
||||
}
|
||||
return new Bom(new Id(groupId, artifactId, version), parent, managedDependencies, imports);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Node nodeFrom(String coordinates) {
|
||||
return nodeFrom(resolveBom(coordinates));
|
||||
}
|
||||
|
||||
private Node nodeFrom(File bomFile) {
|
||||
try {
|
||||
Document document = this.documentBuilder.parse(bomFile);
|
||||
return new Node(document);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private File parentBomFile(Node bom) {
|
||||
Node parent = bom.nodeAt("/project/parent");
|
||||
if (parent != null) {
|
||||
String parentGroupId = parent.textAt("groupId");
|
||||
String parentArtifactId = parent.textAt("artifactId");
|
||||
String parentVersion = parent.textAt("version");
|
||||
return resolveBom(parentGroupId + ":" + parentArtifactId + ":" + parentVersion);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class Node {
|
||||
|
||||
protected final XPath xpath;
|
||||
|
||||
private final org.w3c.dom.Node delegate;
|
||||
|
||||
private Node(org.w3c.dom.Node delegate) {
|
||||
this(delegate, XPathFactory.newInstance().newXPath());
|
||||
}
|
||||
|
||||
private Node(org.w3c.dom.Node delegate, XPath xpath) {
|
||||
this.delegate = delegate;
|
||||
this.xpath = xpath;
|
||||
}
|
||||
|
||||
private String textAt(String expression) {
|
||||
String text = (String) evaluate(expression + "/text()", XPathConstants.STRING);
|
||||
return (text != null && !text.isBlank()) ? text : null;
|
||||
}
|
||||
|
||||
private Node nodeAt(String expression) {
|
||||
org.w3c.dom.Node result = (org.w3c.dom.Node) evaluate(expression, XPathConstants.NODE);
|
||||
return (result != null) ? new Node(result, this.xpath) : null;
|
||||
}
|
||||
|
||||
private List<Node> nodesAt(String expression) {
|
||||
NodeList nodes = (NodeList) evaluate(expression, XPathConstants.NODESET);
|
||||
List<Node> things = new ArrayList<>(nodes.getLength());
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
things.add(new Node(nodes.item(i), this.xpath));
|
||||
}
|
||||
return things;
|
||||
}
|
||||
|
||||
private Object evaluate(String expression, QName type) {
|
||||
try {
|
||||
return this.xpath.evaluate(expression, this.delegate, type);
|
||||
}
|
||||
catch (XPathExpressionException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String name() {
|
||||
return this.delegate.getNodeName();
|
||||
}
|
||||
|
||||
private String textContent() {
|
||||
return this.delegate.getTextContent();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class Properties {
|
||||
|
||||
private final Map<String, String> properties;
|
||||
|
||||
private Properties(Map<String, String> properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
private static Properties from(Node bom, Function<String, Node> resolver) {
|
||||
try {
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
Node current = bom;
|
||||
while (current != null) {
|
||||
String groupId = current.textAt("/project/groupId");
|
||||
if (groupId != null && !groupId.isEmpty()) {
|
||||
properties.putIfAbsent("${project.groupId}", groupId);
|
||||
}
|
||||
String version = current.textAt("/project/version");
|
||||
if (version != null && !version.isEmpty()) {
|
||||
properties.putIfAbsent("${project.version}", version);
|
||||
}
|
||||
List<Node> propertyNodes = current.nodesAt("/project/properties/*");
|
||||
for (Node property : propertyNodes) {
|
||||
properties.putIfAbsent("${%s}".formatted(property.name()), property.textContent());
|
||||
}
|
||||
current = parent(current, resolver);
|
||||
}
|
||||
return new Properties(properties);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Node parent(Node current, Function<String, Node> resolver) {
|
||||
Node parent = current.nodeAt("/project/parent");
|
||||
if (parent != null) {
|
||||
String parentGroupId = parent.textAt("groupId");
|
||||
String parentArtifactId = parent.textAt("artifactId");
|
||||
String parentVersion = parent.textAt("version");
|
||||
return resolver.apply(parentGroupId + ":" + parentArtifactId + ":" + parentVersion);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String replace(String input) {
|
||||
if (input != null && input.startsWith("${") && input.endsWith("}")) {
|
||||
String value = this.properties.get(input);
|
||||
if (value != null) {
|
||||
return replace(value);
|
||||
}
|
||||
throw new IllegalStateException("No replacement for " + input);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 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.
|
||||
* 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.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
/**
|
||||
* {@link Task} to create a {@link ResolvedBom resolved bom}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class CreateResolvedBom extends DefaultTask {
|
||||
|
||||
private final BomExtension bomExtension;
|
||||
|
||||
private final BomResolver bomResolver;
|
||||
|
||||
@Inject
|
||||
public CreateResolvedBom(BomExtension bomExtension) {
|
||||
this.bomExtension = bomExtension;
|
||||
this.bomResolver = new BomResolver(getProject().getConfigurations(), getProject().getDependencies());
|
||||
getOutputFile().convention(getProject().getLayout().getBuildDirectory().file(getName() + "/resolved-bom.json"));
|
||||
}
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
@TaskAction
|
||||
void describeDependencyManagement() throws IOException {
|
||||
ResolvedBom dependencyManagement = this.bomResolver.resolve(this.bomExtension);
|
||||
try (FileWriter writer = new FileWriter(getOutputFile().get().getAsFile())) {
|
||||
dependencyManagement.writeTo(writer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 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.
|
||||
* 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.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.Writer;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||
import com.fasterxml.jackson.annotation.Nulls;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
/**
|
||||
* A resolved bom.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @param id the ID of the resolved bom
|
||||
* @param libraries the libraries declared in the bom
|
||||
*/
|
||||
public record ResolvedBom(Id id, List<ResolvedLibrary> libraries) {
|
||||
|
||||
private static final ObjectMapper objectMapper;
|
||||
|
||||
static {
|
||||
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)
|
||||
.setDefaultPropertyInclusion(Include.NON_EMPTY);
|
||||
mapper.configOverride(List.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
|
||||
objectMapper = mapper;
|
||||
}
|
||||
|
||||
public static ResolvedBom readFrom(File file) {
|
||||
try (FileReader reader = new FileReader(file)) {
|
||||
return objectMapper.readValue(reader, ResolvedBom.class);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeTo(Writer writer) {
|
||||
try {
|
||||
objectMapper.writeValue(writer, this);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public record ResolvedLibrary(String name, String version, String versionProperty, List<Id> managedDependencies,
|
||||
List<Bom> importedBoms) {
|
||||
|
||||
}
|
||||
|
||||
public record Id(String groupId, String artifactId, String version, String classifier) implements Comparable<Id> {
|
||||
|
||||
Id(String groupId, String artifactId, String version) {
|
||||
this(groupId, artifactId, version, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Id o) {
|
||||
int result = this.groupId.compareTo(o.groupId);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = this.artifactId.compareTo(o.artifactId);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return this.version.compareTo(o.version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(this.groupId);
|
||||
builder.append(":");
|
||||
builder.append(this.artifactId);
|
||||
builder.append(":");
|
||||
builder.append(this.version);
|
||||
if (this.classifier != null) {
|
||||
builder.append(this.classifier);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public record Bom(Id id, Bom parent, List<Id> managedDependencies, List<Bom> importedBoms) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,63 +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.constraints;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import org.springframework.boot.build.constraints.ExtractVersionConstraints.ConstrainedVersion;
|
||||
|
||||
/**
|
||||
* Task for documenting a platform's constrained versions.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class DocumentConstrainedVersions extends DefaultTask {
|
||||
|
||||
@Input
|
||||
public abstract SetProperty<ConstrainedVersion> getConstrainedVersions();
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
@TaskAction
|
||||
public void documentConstrainedVersions() throws IOException {
|
||||
File outputFile = getOutputFile().get().getAsFile();
|
||||
outputFile.getParentFile().mkdirs();
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) {
|
||||
writer.println("|===");
|
||||
writer.println("| Group ID | Artifact ID | Version");
|
||||
for (ConstrainedVersion constrainedVersion : getConstrainedVersions().get()) {
|
||||
writer.println();
|
||||
writer.printf("| `%s`%n", constrainedVersion.getGroup());
|
||||
writer.printf("| `%s`%n", constrainedVersion.getArtifact());
|
||||
writer.printf("| `%s`%n", constrainedVersion.getVersion());
|
||||
}
|
||||
writer.println("|===");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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.
|
||||
* 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.docs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.InputFiles;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.PathSensitive;
|
||||
import org.gradle.api.tasks.PathSensitivity;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import org.springframework.boot.build.bom.ResolvedBom;
|
||||
import org.springframework.boot.build.bom.ResolvedBom.Bom;
|
||||
import org.springframework.boot.build.bom.ResolvedBom.Id;
|
||||
import org.springframework.boot.build.bom.ResolvedBom.ResolvedLibrary;
|
||||
|
||||
/**
|
||||
* Task for documenting {@link ResolvedBom boms'} managed dependencies.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class DocumentManagedDependencies extends DefaultTask {
|
||||
|
||||
private FileCollection resolvedBoms;
|
||||
|
||||
@InputFiles
|
||||
@PathSensitive(PathSensitivity.RELATIVE)
|
||||
public FileCollection getResolvedBoms() {
|
||||
return this.resolvedBoms;
|
||||
}
|
||||
|
||||
public void setResolvedBoms(FileCollection resolvedBoms) {
|
||||
this.resolvedBoms = resolvedBoms;
|
||||
}
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
@TaskAction
|
||||
public void documentConstrainedVersions() throws IOException {
|
||||
File outputFile = getOutputFile().get().getAsFile();
|
||||
outputFile.getParentFile().mkdirs();
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) {
|
||||
writer.println("|===");
|
||||
writer.println("| Group ID | Artifact ID | Version");
|
||||
Set<Id> managedCoordinates = new TreeSet<>((id1, id2) -> {
|
||||
int result = id1.groupId().compareTo(id2.groupId());
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return id1.artifactId().compareTo(id2.artifactId());
|
||||
});
|
||||
for (File file : getResolvedBoms().getFiles()) {
|
||||
managedCoordinates.addAll(process(ResolvedBom.readFrom(file)));
|
||||
}
|
||||
for (Id id : managedCoordinates) {
|
||||
writer.println();
|
||||
writer.printf("| `%s`%n", id.groupId());
|
||||
writer.printf("| `%s`%n", id.artifactId());
|
||||
writer.printf("| `%s`%n", id.version());
|
||||
}
|
||||
writer.println("|===");
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Id> process(ResolvedBom resolvedBom) {
|
||||
TreeSet<Id> managedCoordinates = new TreeSet<>();
|
||||
for (ResolvedLibrary library : resolvedBom.libraries()) {
|
||||
for (Id managedDependency : library.managedDependencies()) {
|
||||
managedCoordinates.add(managedDependency);
|
||||
}
|
||||
for (Bom importedBom : library.importedBoms()) {
|
||||
managedCoordinates.addAll(process(importedBom));
|
||||
}
|
||||
}
|
||||
return managedCoordinates;
|
||||
}
|
||||
|
||||
private Set<Id> process(Bom bom) {
|
||||
TreeSet<Id> managedCoordinates = new TreeSet<>();
|
||||
bom.managedDependencies().stream().forEach(managedCoordinates::add);
|
||||
Bom parent = bom.parent();
|
||||
if (parent != null) {
|
||||
managedCoordinates.addAll(process(parent));
|
||||
}
|
||||
return managedCoordinates;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -14,46 +14,66 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.build.constraints;
|
||||
package org.springframework.boot.build.docs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.InputFiles;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.PathSensitive;
|
||||
import org.gradle.api.tasks.PathSensitivity;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import org.springframework.boot.build.constraints.ExtractVersionConstraints.VersionProperty;
|
||||
import org.springframework.boot.build.bom.ResolvedBom;
|
||||
import org.springframework.boot.build.bom.ResolvedBom.ResolvedLibrary;
|
||||
|
||||
/**
|
||||
* Task for documenting available version properties.
|
||||
* Task for documenting {@link ResolvedBom boms'} version properties.
|
||||
*
|
||||
* @author Christoph Dreis
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class DocumentVersionProperties extends DefaultTask {
|
||||
|
||||
@Input
|
||||
public abstract SetProperty<VersionProperty> getVersionProperties();
|
||||
private FileCollection resolvedBoms;
|
||||
|
||||
@InputFiles
|
||||
@PathSensitive(PathSensitivity.RELATIVE)
|
||||
public FileCollection getResolvedBoms() {
|
||||
return this.resolvedBoms;
|
||||
}
|
||||
|
||||
public void setResolvedBoms(FileCollection resolvedBoms) {
|
||||
this.resolvedBoms = resolvedBoms;
|
||||
}
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
@TaskAction
|
||||
public void documentVersionProperties() throws IOException {
|
||||
List<ResolvedLibrary> libraries = this.resolvedBoms.getFiles()
|
||||
.stream()
|
||||
.map(ResolvedBom::readFrom)
|
||||
.flatMap((resolvedBom) -> resolvedBom.libraries().stream())
|
||||
.sorted((l1, l2) -> l1.name().compareToIgnoreCase(l2.name()))
|
||||
.toList();
|
||||
File outputFile = getOutputFile().getAsFile().get();
|
||||
outputFile.getParentFile().mkdirs();
|
||||
try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) {
|
||||
writer.println("|===");
|
||||
writer.println("| Library | Version Property");
|
||||
for (VersionProperty versionProperty : getVersionProperties().get()) {
|
||||
for (ResolvedLibrary library : libraries) {
|
||||
writer.println();
|
||||
writer.printf("| `%s`%n", versionProperty.getLibraryName());
|
||||
writer.printf("| `%s`%n", versionProperty.getVersionProperty());
|
||||
writer.printf("| `%s`%n", library.name());
|
||||
writer.printf("| `%s`%n", library.versionProperty());
|
||||
}
|
||||
writer.println("|===");
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ configurations {
|
|||
autoConfiguration
|
||||
configurationProperties
|
||||
remoteSpringApplicationExample
|
||||
resolvedBom
|
||||
springApplicationExample
|
||||
testSlices
|
||||
all {
|
||||
|
|
@ -177,6 +178,8 @@ dependencies {
|
|||
remoteSpringApplicationExample(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-logging"))
|
||||
remoteSpringApplicationExample("org.springframework:spring-web")
|
||||
|
||||
resolvedBom(project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "resolvedBom"))
|
||||
|
||||
springApplicationExample(platform(project(":spring-boot-project:spring-boot-dependencies")))
|
||||
springApplicationExample(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
|
||||
|
||||
|
|
@ -254,16 +257,14 @@ task documentAutoConfigurationClasses(type: org.springframework.boot.build.autoc
|
|||
outputDir = layout.buildDirectory.dir("generated/docs/auto-configuration-classes/documented-auto-configuration-classes/")
|
||||
}
|
||||
|
||||
task documentDependencyVersionCoordinates(type: org.springframework.boot.build.constraints.DocumentConstrainedVersions) {
|
||||
dependsOn dependencyVersions
|
||||
constrainedVersions.set(providers.provider { dependencyVersions.constrainedVersions })
|
||||
task documentDependencyVersionCoordinates(type: org.springframework.boot.build.docs.DocumentManagedDependencies) {
|
||||
outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-coordinates.adoc")
|
||||
resolvedBoms = configurations.resolvedBom
|
||||
}
|
||||
|
||||
task documentDependencyVersionProperties(type: org.springframework.boot.build.constraints.DocumentVersionProperties) {
|
||||
dependsOn dependencyVersions
|
||||
versionProperties.set(providers.provider { dependencyVersions.versionProperties})
|
||||
task documentDependencyVersionProperties(type: org.springframework.boot.build.docs.DocumentVersionProperties) {
|
||||
outputFile = layout.buildDirectory.file("generated/docs/dependency-versions/documented-properties.adoc")
|
||||
resolvedBoms = configurations.resolvedBom
|
||||
}
|
||||
|
||||
task documentConfigurationProperties(type: org.springframework.boot.build.context.properties.DocumentConfigurationProperties) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue