diff --git a/gradle/plugins/config/checkstyle/checkstyle.xml b/gradle/plugins/config/checkstyle/checkstyle.xml new file mode 100644 index 00000000000..1ad50d8fcb8 --- /dev/null +++ b/gradle/plugins/config/checkstyle/checkstyle.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/gradle/plugins/cycle-detection-plugin/build.gradle b/gradle/plugins/cycle-detection-plugin/build.gradle new file mode 100644 index 00000000000..51de7cb5dca --- /dev/null +++ b/gradle/plugins/cycle-detection-plugin/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java-gradle-plugin' + id "checkstyle" + id "io.spring.javaformat" version "$javaFormatVersion" + +} + +repositories { + mavenCentral() +} + +checkstyle { + toolVersion = "${checkstyleToolVersion}" +} + +dependencies { + checkstyle("com.puppycrawl.tools:checkstyle:${checkstyle.toolVersion}") + checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}") + + implementation("org.jgrapht:jgrapht-core:1.5.2") +} + +gradlePlugin { + plugins { + cycleDetectionPlugin { + id = "org.springframework.boot.cycle-detection" + implementationClass = "org.springframework.boot.build.cycledetection.CycleDetectionPlugin" + } + } +} diff --git a/gradle/plugins/cycle-detection-plugin/src/main/java/org/springframework/boot/build/cycledetection/CycleDetectionPlugin.java b/gradle/plugins/cycle-detection-plugin/src/main/java/org/springframework/boot/build/cycledetection/CycleDetectionPlugin.java new file mode 100644 index 00000000000..84482819702 --- /dev/null +++ b/gradle/plugins/cycle-detection-plugin/src/main/java/org/springframework/boot/build/cycledetection/CycleDetectionPlugin.java @@ -0,0 +1,90 @@ +/* + * 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.cycledetection; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.execution.TaskExecutionGraph; +import org.gradle.api.initialization.Settings; +import org.jgrapht.Graph; +import org.jgrapht.alg.cycle.TarjanSimpleCycles; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; + +/** + * A {@link Settings} {@link Plugin plugin} to detect cycles between a build's projects. + * + * @author Andy Wilkinson + */ +public class CycleDetectionPlugin implements Plugin { + + @Override + public void apply(Settings settings) { + settings.getGradle().getTaskGraph().whenReady(this::detectCycles); + } + + private void detectCycles(TaskExecutionGraph taskGraph) { + Map> dependenciesByProject = getProjectsAndDependencies(taskGraph); + Graph graph = createGraph(dependenciesByProject); + List> cycles = findCycles(graph); + if (!cycles.isEmpty()) { + StringBuilder message = new StringBuilder("Cycles detected:\n"); + for (List cycle : cycles) { + cycle.add(cycle.get(0)); + message.append(" " + String.join(" -> ", cycle) + "\n"); + } + throw new GradleException(message.toString()); + } + } + + private Map> getProjectsAndDependencies(TaskExecutionGraph taskGraph) { + Map> dependenciesByProject = new HashMap<>(); + for (Task task : taskGraph.getAllTasks()) { + Project project = task.getProject(); + Set dependencies = dependenciesByProject.computeIfAbsent(project, (p) -> new LinkedHashSet<>()); + taskGraph.getDependencies(task) + .stream() + .map(Task::getProject) + .filter((taskProject) -> !taskProject.equals(project)) + .forEach(dependencies::add); + } + return dependenciesByProject; + } + + private Graph createGraph(Map> dependenciesByProject) { + Graph graph = new DefaultDirectedGraph<>(DefaultEdge.class); + dependenciesByProject.keySet().forEach((project) -> graph.addVertex(project.getName())); + dependenciesByProject.forEach((project, dependencies) -> dependencies + .forEach((dependency) -> graph.addEdge(project.getName(), dependency.getName()))); + return graph; + } + + private List> findCycles(Graph graph) { + TarjanSimpleCycles simpleCycles = new TarjanSimpleCycles<>(graph); + List> cycles = simpleCycles.findSimpleCycles(); + return cycles; + } + +} diff --git a/gradle/plugins/settings.gradle b/gradle/plugins/settings.gradle new file mode 100644 index 00000000000..31c79396773 --- /dev/null +++ b/gradle/plugins/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + new File(rootDir.parentFile.parentFile, "gradle.properties").withInputStream { + def properties = new Properties() + properties.load(it) + properties.forEach(settings.ext::set) + gradle.rootProject { + properties.forEach(project.ext::set) + } + } +} + +include 'cycle-detection-plugin' diff --git a/settings.gradle b/settings.gradle index bb6851f0388..2d9d18ef35a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,10 +15,12 @@ pluginManagement { } } } + includeBuild("gradle/plugins") } plugins { id "io.spring.develocity.conventions" version "0.0.22" + id "org.springframework.boot.cycle-detection" } rootProject.name="spring-boot-build"