Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions p2-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,17 @@
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
250 changes: 250 additions & 0 deletions p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/DotDump.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/*******************************************************************************
* Copyright (c) 2025 Christoph Läubrich and others.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Christoph Läubrich - initial API and implementation
*******************************************************************************/
package org.eclipse.tycho.p2maven;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.maven.project.MavenProject;
import org.eclipse.equinox.internal.p2.metadata.IRequiredCapability;
import org.eclipse.equinox.p2.metadata.IRequirement;
import org.eclipse.equinox.spi.p2.publisher.PublisherHelper;
import org.eclipse.tycho.p2maven.ProjectDependencyClosureGraph.Edge;
import org.eclipse.tycho.p2maven.tmp.BundlesAction;

/**
* Utility class to dump dependencies graphs as dot files for visualization
*/
@SuppressWarnings("restriction")
public class DotDump {

/**
* Dump the graph to a DOT file for visualization
*
* @param file the file to write the DOT representation to
* @param graph the graph to dump
* @throws IOException if writing fails
*/
public static void dump(File file, ProjectDependencyClosureGraph graph) throws IOException {
// Detect cycles
Set<Set<MavenProject>> cycles = graph.detectCycles();

try (PrintWriter writer = new PrintWriter(new FileWriter(file))) {
writer.println("digraph ProjectDependencies {");
writer.println(" rankdir=LR;");
writer.println(" node [shape=box];");
writer.println();

// Create a mapping of projects to short names for the graph
Map<MavenProject, String> projectNames = new HashMap<>();
int counter = 0;
for (MavenProject project : graph.projectIUMap.keySet()) {
String nodeName = "p" + counter++;
projectNames.put(project, nodeName);
String label = project.getArtifactId();
writer.println(" " + nodeName + " [label=\"" + escapeLabel(label) + "\"];");
}
writer.println();

// Write edges with color coding based on cycle type and requirement labels
for (var entry : graph.projectEdgesMap.entrySet()) {
MavenProject sourceProject = entry.getKey();
String sourceName = projectNames.get(sourceProject);

// Group edges by target project to collect all requirements for each dependency
Map<MavenProject, EdgeInfo> targetProjectEdges = new HashMap<>();
for (Edge edge : entry.getValue()) {
MavenProject targetProject = edge.capability().project();

// Determine edge color
String color;
if (targetProject.equals(sourceProject)) {
// Self-reference cycle - GRAY
color = "gray";
} else if (isInCycle(sourceProject, targetProject, cycles)) {
// Part of a transitive cycle - RED
color = "red";
} else {
// Normal dependency - BLACK
color = "black";
}

EdgeInfo edgeInfo = targetProjectEdges.computeIfAbsent(targetProject, k -> new EdgeInfo(color));
edgeInfo.addRequirement(edge.requirement().requirement());
}

// Write edge for each target project with requirement labels
for (var targetEntry : targetProjectEdges.entrySet()) {
MavenProject targetProject = targetEntry.getKey();
EdgeInfo edgeInfo = targetEntry.getValue();
String targetName = projectNames.get(targetProject);
if (targetName != null) {
// Build label from requirements
String label = edgeInfo.getLabel();

// Check if label contains HTML-like formatting
if (label.startsWith("<") && label.endsWith(">")) {
// Use HTML-like label without quotes
writer.println(" " + sourceName + " -> " + targetName + " [color=" + edgeInfo.color
+ ", label=" + label + "];");
} else {
// Use regular quoted label
writer.println(" " + sourceName + " -> " + targetName + " [color=" + edgeInfo.color
+ ", label=\"" + escapeLabel(label) + "\"];");
}
}
}
}

writer.println("}");
}
}

/**
* Helper class to collect edge information for a dependency relationship
*/
private static class EdgeInfo {
final String color;
final List<IRequirement> requirements = new ArrayList<>();

EdgeInfo(String color) {
this.color = color;
}

void addRequirement(IRequirement requirement) {
// Check if requirement is already in the list by comparing string representations
String reqString = requirement.toString();
boolean alreadyAdded = requirements.stream()
.anyMatch(r -> r.toString().equals(reqString));
if (!alreadyAdded) {
requirements.add(requirement);
}
}

String getLabel() {
if (requirements.isEmpty()) {
return "";
}

// Build formatted label for each requirement
List<String> formattedRequirements = new ArrayList<>();
for (IRequirement requirement : requirements) {
formattedRequirements.add(formatRequirement(requirement));
}

if (formattedRequirements.size() == 1) {
return formattedRequirements.get(0);
} else {
// Join multiple requirements with line breaks
return String.join("\\n", formattedRequirements);
}
}

/**
* Format a requirement with appropriate HTML-like styling based on its properties
*/
private static String formatRequirement(IRequirement requirement) {
String reqText = escapeHtml(requirement.toString());

boolean isOptional = requirement.getMin() == 0;
boolean isMandatoryCompile = isMandatoryCompileRequirement(requirement);
boolean isGreedy = requirement.isGreedy();

// Apply formatting if needed
if (isOptional || isMandatoryCompile || isGreedy) {
StringBuilder formatted = new StringBuilder("<");

// Apply italic for optional
if (isOptional) {
formatted.append("<I>");
}

// Apply bold for mandatory compile
if (isMandatoryCompile) {
formatted.append("<B>");
}

// Apply underline for greedy
if (isGreedy) {
formatted.append("<U>");
}

formatted.append(reqText);

// Close tags in reverse order
if (isGreedy) {
formatted.append("</U>");
}
if (isMandatoryCompile) {
formatted.append("</B>");
}
if (isOptional) {
formatted.append("</I>");
}

formatted.append(">");
return formatted.toString();
}

return reqText;
}

/**
* Check if a requirement is a mandatory compile requirement
* (osgi.bundle or java.package namespace)
*/
private static boolean isMandatoryCompileRequirement(IRequirement requirement) {
if (requirement instanceof IRequiredCapability) {
IRequiredCapability reqCap = (IRequiredCapability) requirement;
String namespace = reqCap.getNamespace();
return BundlesAction.CAPABILITY_NS_OSGI_BUNDLE.equals(namespace)
|| PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE.equals(namespace);
}
return false;
}

/**
* Escape HTML special characters for use in HTML-like labels
*/
private static String escapeHtml(String text) {
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;");
}
}

/**
* Check if an edge from source to target is part of a cycle
*/
private static boolean isInCycle(MavenProject source, MavenProject target, Set<Set<MavenProject>> cycles) {
for (Set<MavenProject> cycle : cycles) {
if (cycle.contains(source) && cycle.contains(target)) {
return true;
}
}
return false;
}

private static String escapeLabel(String label) {
return label.replace("\\", "\\\\").replace("\"", "\\\"");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
package org.eclipse.tycho.p2maven;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
Expand All @@ -29,12 +28,9 @@
import org.apache.maven.execution.MavenSession;
import org.apache.maven.project.MavenProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.equinox.internal.p2.metadata.IRequiredCapability;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.IProvidedCapability;
import org.eclipse.equinox.p2.metadata.IRequirement;
import org.eclipse.equinox.p2.metadata.expression.IMatchExpression;
import org.eclipse.tycho.p2maven.tmp.BundlesAction;

/**
* THis component computes dependencies between projects
Expand All @@ -47,9 +43,6 @@ public class MavenProjectDependencyProcessor {
@Inject
private InstallableUnitGenerator generator;

@Inject
private InstallableUnitSlicer slicer;

/**
* Computes the {@link ProjectDependencyClosure} of the given collection of
* projects.
Expand All @@ -69,38 +62,7 @@ public ProjectDependencyClosure computeProjectDependencyClosure(Collection<Maven
throws CoreException {
Objects.requireNonNull(session);
Map<MavenProject, Collection<IInstallableUnit>> projectIUMap = generator.getInstallableUnits(projects, session);
return new ProjectDependencyClosureGraph(projectIUMap, slicer);
}

private static boolean hasAnyHost(IInstallableUnit unit, Iterable<IInstallableUnit> collection) {
return getFragmentHostRequirement(unit).anyMatch(req -> {
for (IInstallableUnit iu : collection) {
if (req.isMatch(iu)) {
return true;
}
}
return false;
});
}

private static Stream<IProvidedCapability> getFragmentCapability(IInstallableUnit installableUnit) {

return installableUnit.getProvidedCapabilities().stream()
.filter(cap -> BundlesAction.CAPABILITY_NS_OSGI_FRAGMENT.equals(cap.getNamespace()));
}

private static Stream<IRequirement> getFragmentHostRequirement(IInstallableUnit installableUnit) {
return getFragmentCapability(installableUnit).map(provided -> {
String hostName = provided.getName();
for (IRequirement requirement : installableUnit.getRequirements()) {
if (requirement instanceof IRequiredCapability requiredCapability) {
if (hostName.equals(requiredCapability.getName())) {
return requirement;
}
}
}
return null;
}).filter(Objects::nonNull);
return new ProjectDependencyClosureGraph(projectIUMap);
}

private static boolean isMatch(IRequirement requirement, Collection<IInstallableUnit> contextIUs) {
Expand All @@ -114,7 +76,7 @@ private static boolean isMatch(IRequirement requirement, Collection<IInstallable
public static final class ProjectDependencies {

private final Map<IRequirement, Collection<IInstallableUnit>> requirementsMap;
private final Set<IInstallableUnit> projectUnits;
final Set<IInstallableUnit> projectUnits;

ProjectDependencies(Map<IRequirement, Collection<IInstallableUnit>> requirementsMap,
Set<IInstallableUnit> projectUnits) {
Expand Down Expand Up @@ -166,31 +128,13 @@ Stream<Entry<MavenProject, Collection<IInstallableUnit>>> dependencies(
* @return the collection of projects this maven project depend on in this
* closure
*/
default Collection<MavenProject> getDependencyProjects(MavenProject mavenProject,
Collection<IInstallableUnit> contextIUs) {
ProjectDependencies projectDependecies = getProjectDependecies(mavenProject);
List<MavenProject> list = projectDependecies.getDependencies(contextIUs).stream()
.flatMap(dependency -> getProject(dependency).stream()).distinct().toList();
if (isFragment(mavenProject)) {
// for projects that are fragments don't do any special processing...
return list;
}
// for regular projects we must check if they have any fragment requirements
// that must be attached here, example is SWT that defines a requirement to its
// fragments and if build inside the same reactor with a consumer (e.g. JFace)
// has to be applied
return list.stream().flatMap(project -> {
ProjectDependencies dependecies = getProjectDependecies(project);
return Stream.concat(Stream.of(project), dependecies.getDependencies(contextIUs).stream()
.filter(dep -> hasAnyHost(dep, dependecies.projectUnits))
.flatMap(dependency -> getProject(dependency).stream()));
}).toList();
}
Collection<MavenProject> getDependencyProjects(MavenProject mavenProject,
Collection<IInstallableUnit> contextIUs);

/**
* Check if the given unit is a fragment
*
* @param installableUnit the unit to check
* @param mavenProject the project to check
* @return <code>true</code> if this is a fragment, <code>false</code> otherwise
*/
boolean isFragment(MavenProject mavenProject);
Expand Down
Loading
Loading