Skip to content

Commit

Permalink
Move p2 dependency tree logic from tycho-p2-plugin to tycho-core
Browse files Browse the repository at this point in the history
This moves the algorithm which is responsible for calculating the IU
dependency tree used by the dependency-tree Mojo to tycho-core, so that
it can be used by other projects.

For a given project, this tree can be calculated using the
P2DependencyTreeGenerator, which can be injected into a Mojo as a simple
Plexus component.
  • Loading branch information
ptziegler authored and laeubi committed Jan 27, 2024
1 parent afcfd26 commit 502a622
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*******************************************************************************
* Copyright (c) 2024 Patrick Ziegler 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:
* Patrick Ziegler - initial API and implementation
*******************************************************************************/

package org.eclipse.tycho.p2.tools;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;

import org.apache.maven.plugin.LegacySupport;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.annotations.Component;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.IRequirement;
import org.eclipse.tycho.ArtifactDescriptor;
import org.eclipse.tycho.core.TychoProject;
import org.eclipse.tycho.core.TychoProjectManager;
import org.eclipse.tycho.core.osgitools.DefaultReactorProject;
import org.eclipse.tycho.p2maven.InstallableUnitGenerator;

/**
* Utility class for converting a flat dependency into a dependency tree. The tree is structured in
* such a way that an IU {@code a} is a child of another IU {@code b}, if and only if {@code a} is
* required by {@code b}. If {@code b} is required by multiple IUs, the first one is selected.<br>
* Used by e.g. the dependency-tree Mojo, in order to mimic the behavior of the native Maven
* dependency-tree Mojo.<br>
* This class is intended to be use as a Plexus component, so that all required fields are
* automatically initialized using DI.
*/
@Component(role = P2DependencyTreeGenerator.class)
public final class P2DependencyTreeGenerator {
private final InstallableUnitGenerator generator;
private final TychoProjectManager projectManager;
private final LegacySupport legacySupport;

@Inject
public P2DependencyTreeGenerator(InstallableUnitGenerator generator, TychoProjectManager projectManager,
LegacySupport legacySupport) {
this.generator = generator;
this.projectManager = projectManager;
this.legacySupport = legacySupport;
}

/**
* Calculates and returns the dependency tree of the given Maven project. The list that is
* returned by this method corresponds to the IUs which are directly required by the given
* project.
*
* @param project
* One of the Maven projects of the current reactor build. If this project is not a
* Tycho project (e.g. the parent pom), an empty list is returned.
* @param unmapped
* A set containing all IUs which could not be added to the dependency tree.Meaning
* that those units are required by the project but not by any of its IUs. Must be
* mutable.
* @return as described.
* @throws CoreException
* if anything goes wrong
*/
public List<DependencyTreeNode> buildDependencyTree(MavenProject project, Set<IInstallableUnit> unmapped)
throws CoreException {
//TODO maybe we can compute a org.apache.maven.shared.dependency.graph.DependencyNode and reuse org.apache.maven.plugins.dependency.tree.TreeMojo wich has a getSerializingDependencyNodeVisitor
Optional<TychoProject> tychoProject = projectManager.getTychoProject(project);
if (tychoProject.isEmpty()) {
return Collections.emptyList();
}

List<ArtifactDescriptor> artifacts = tychoProject.get() //
.getDependencyArtifacts(DefaultReactorProject.adapt(project)) //
.getArtifacts();
Set<IInstallableUnit> units = artifacts.stream() //
.flatMap(d -> d.getInstallableUnits().stream()) //
.collect(Collectors.toCollection(HashSet::new));
List<IInstallableUnit> initial = List
.copyOf(generator.getInstallableUnits(project, legacySupport.getSession(), false));
units.removeAll(initial);

return Collections.unmodifiableList(DependencyTreeNode.create(initial, units, unmapped));
}

/**
* This class represents a single IU within the dependency tree and holds. Two nodes in this
* tree are connected (as in parent and child), if and only if the child IU is required by the
* parent. Each IU is unique and must only appear once in the dependency tree.
*/
public static class DependencyTreeNode {
private static final Comparator<IInstallableUnit> COMPARATOR = Comparator.comparing(IInstallableUnit::getId,
String.CASE_INSENSITIVE_ORDER);
private final IInstallableUnit iu;
private final IRequirement satisfies;
private final List<DependencyTreeNode> children = new ArrayList<>();

private DependencyTreeNode(IInstallableUnit iu, IRequirement satisfies) {
this.iu = iu;
this.satisfies = satisfies;
}

public IInstallableUnit getInstallableUnit() {
return iu;
}

public IRequirement getRequirement() {
return satisfies;
}

public List<DependencyTreeNode> getChildren() {
return Collections.unmodifiableList(children);
}

/**
* Returns the IU (if present) that is contained by this node. Primarily used to make
* debugging easier.
*/
@Override
public String toString() {
return Objects.toString(iu);
}

/**
* Create the dependency tree based on the {@code initial} IUs. A tree node is created for
* each IU of {@code initial}. The children of a node correspond to all IUs that are
* (directly) required by the parent IU. Each IU in {@code units} only appears once, even if
* it required by multiple IUs.
*
* @param initial
* The "direct" IUs referenced by a given artifact.
* @param units
* All IUs that are required by a given artifact, excluding {@code initial}.
* @param unmapped
* A subset of {@code units}, which are not contained in the dependency tree.
* Meaning that those IUs are not necessarily required to satisfy the
* dependencies of an artifact.
* @return A list of dependency tree models. Each model in this list matches an IU of
* {@code initial}.
*/
private static List<DependencyTreeNode> create(List<IInstallableUnit> initial, Set<IInstallableUnit> units,
Set<IInstallableUnit> unmapped) {
List<DependencyTreeNode> rootNodes = new ArrayList<>();
for (int i = 0; i < initial.size(); ++i) {
DependencyTreeNode rootNode = new DependencyTreeNode(initial.get(i), null);
create(rootNode, units);
rootNodes.add(rootNode);
}
unmapped.addAll(units);
return rootNodes;
}

/**
* Internal helper method which recursively goes through IUs that are required by the IU
* held by {@code node}. For each IU that satisfies this requirement a new
* {@link DependencyTreeNode} is created and added as a child to {@link node}. If such an IU
* is found, it is removed from {@code units}, meaning that each IU can only show up once in
* the dependency tree. The children of each node are sorted lexicographically according to
* {@link #COMPARATOR}.
*
* @param node
* The (intermediate) head of the dependency tree.
* @param units
* A set of all IUs that are associated with the currently handled project.
*/
private static void create(DependencyTreeNode node, Set<IInstallableUnit> units) {
List<IInstallableUnit> collected = new ArrayList<>();
Map<IInstallableUnit, IRequirement> requirementsMap = new HashMap<>();
IInstallableUnit unit = node.getInstallableUnit();
//
Stream.concat(unit.getRequirements().stream(), unit.getMetaRequirements().stream()).forEach(requirement -> {
for (Iterator<IInstallableUnit> iterator = units.iterator(); iterator.hasNext();) {
IInstallableUnit other = iterator.next();
if (other.satisfies(requirement)) {
collected.add(other);
requirementsMap.put(other, requirement);
iterator.remove();
}
}
});
//
Collections.sort(collected, COMPARATOR);
for (IInstallableUnit iu : collected) {
IRequirement satisfies = requirementsMap.get(iu);
DependencyTreeNode childNode = new DependencyTreeNode(iu, satisfies);
node.children.add(childNode);
create(childNode, units);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022 Christoph Läubrich and others.
* Copyright (c) 2022, 2024 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
Expand All @@ -10,22 +10,15 @@
package org.eclipse.tycho.plugins.p2;

import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.LegacySupport;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
Expand All @@ -41,74 +34,65 @@
import org.eclipse.tycho.core.TychoProject;
import org.eclipse.tycho.core.TychoProjectManager;
import org.eclipse.tycho.core.osgitools.DefaultReactorProject;
import org.eclipse.tycho.p2maven.InstallableUnitGenerator;
import org.eclipse.tycho.p2.tools.P2DependencyTreeGenerator;
import org.eclipse.tycho.p2.tools.P2DependencyTreeGenerator.DependencyTreeNode;

/**
* Similar to dependency:tree outputs a tree of P2 dependencies.
*/
@Mojo(name = "dependency-tree", requiresProject = true, threadSafe = true, requiresDependencyCollection = ResolutionScope.TEST)
public class DependenciesTreeMojo extends AbstractMojo {

private static final Comparator<IInstallableUnit> COMPARATOR = Comparator.comparing(IInstallableUnit::getId,
String.CASE_INSENSITIVE_ORDER);
@Parameter(property = "project")
private MavenProject project;

@Component
private InstallableUnitGenerator generator;

@Component
private LegacySupport legacySupport;
private P2DependencyTreeGenerator generator;

@Component
private TychoProjectManager projectManager;

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
//TODO maybe we can compute a org.apache.maven.shared.dependency.graph.DependencyNode and reuse org.apache.maven.plugins.dependency.tree.TreeMojo wich has a getSerializingDependencyNodeVisitor

Optional<TychoProject> tychoProject = projectManager.getTychoProject(project);
if (tychoProject.isEmpty()) {
return;
}

Set<String> written = new HashSet<String>();
written.add(project.getId());
getLog().info(project.getId());

List<ArtifactDescriptor> artifacts = tychoProject.get()
.getDependencyArtifacts(DefaultReactorProject.adapt(project)).getArtifacts();
Map<IInstallableUnit, Set<ReactorProject>> projectMap = artifacts.stream()
.filter(a -> a.getMavenProject() != null).flatMap(a -> {
return a.getInstallableUnits().stream().map(iu -> new SimpleEntry<>(iu, a.getMavenProject()));
})
List<ArtifactDescriptor> artifacts = tychoProject.get() //
.getDependencyArtifacts(DefaultReactorProject.adapt(project)) //
.getArtifacts();
Map<IInstallableUnit, Set<ReactorProject>> projectMap = artifacts.stream() //
.filter(a -> a.getMavenProject() != null) //
.flatMap(a -> a.getInstallableUnits().stream().map(iu -> new SimpleEntry<>(iu, a.getMavenProject()))) //
.collect(Collectors.groupingBy(Entry::getKey, Collectors.mapping(Entry::getValue, Collectors.toSet())));
Set<IInstallableUnit> units = artifacts.stream().flatMap(d -> d.getInstallableUnits().stream())
.collect(Collectors.toCollection(HashSet::new));
List<IInstallableUnit> initial;

Set<IInstallableUnit> unmapped = new HashSet<>();
List<DependencyTreeNode> dependencyTree;
try {
initial = new ArrayList<IInstallableUnit>(
generator.getInstallableUnits(project, legacySupport.getSession(), false));
dependencyTree = generator.buildDependencyTree(project, unmapped);
} catch (CoreException e) {
throw new MojoFailureException(e);
}
units.removeAll(initial);
int size = initial.size();
for (int i = 0; i < size; i++) {
IInstallableUnit unit = initial.get(i);
printUnit(unit, null, units, projectMap, 0, i == size - 1);

for (DependencyTreeNode rootNode : dependencyTree) {
printUnit(rootNode, projectMap, 0);
}
if (!units.isEmpty()) {

if (!unmapped.isEmpty()) {
getLog().info("Units that cannot be matched to any requirement:");
for (IInstallableUnit unit : units) {
for (IInstallableUnit unit : unmapped) {
getLog().info(unit.toString());
}
}

}

private void printUnit(IInstallableUnit unit, IRequirement satisfies, Set<IInstallableUnit> units,
Map<IInstallableUnit, Set<ReactorProject>> projectMap, int indent, boolean last) {
private void printUnit(DependencyTreeNode model, Map<IInstallableUnit, Set<ReactorProject>> projectMap,
int indent) {
IInstallableUnit unit = model.getInstallableUnit();
IRequirement satisfies = model.getRequirement();
StringBuffer line = new StringBuffer();
for (int i = 0; i < indent; i++) {
line.append(" ");
Expand All @@ -129,24 +113,8 @@ private void printUnit(IInstallableUnit unit, IRequirement satisfies, Set<IInsta
.collect(Collectors.joining(", ", "[", "]")));
}
getLog().info(line.toString());
List<IInstallableUnit> collected = new ArrayList<IInstallableUnit>();
Map<IInstallableUnit, IRequirement> requirementsMap = new HashMap<IInstallableUnit, IRequirement>();
Stream.concat(unit.getRequirements().stream(), unit.getMetaRequirements().stream()).forEach(requirement -> {
for (Iterator<IInstallableUnit> iterator = units.iterator(); iterator.hasNext();) {
IInstallableUnit other = iterator.next();
if (other.satisfies(requirement)) {
collected.add(other);
requirementsMap.put(other, requirement);
iterator.remove();
}
}
});
Collections.sort(collected, COMPARATOR);
int size = collected.size();
for (int i = 0; i < size; i++) {
IInstallableUnit iu = collected.get(i);
printUnit(iu, requirementsMap.get(iu), units, projectMap, indent + 1, i == size - 1);
for (DependencyTreeNode child : model.getChildren()) {
printUnit(child, projectMap, indent + 1);
}
}

}

0 comments on commit 502a622

Please sign in to comment.