From adfdcd8b36572e7b8c455492086c037379b20306 Mon Sep 17 00:00:00 2001 From: Patrick Ziegler Date: Fri, 26 Jan 2024 17:09:05 +0100 Subject: [PATCH] Move p2 dependency tree logic from tycho-p2-plugin to tycho-core 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. (cherry picked from commit 502a622cdddaa35f15873a8801e1ea6e1f7c2617) --- .../p2/tools/P2DependencyTreeGenerator.java | 209 ++++++++++++++++++ .../plugins/p2/DependenciesTreeMojo.java | 84 +++---- 2 files changed, 235 insertions(+), 58 deletions(-) create mode 100644 tycho-core/src/main/java/org/eclipse/tycho/p2/tools/P2DependencyTreeGenerator.java diff --git a/tycho-core/src/main/java/org/eclipse/tycho/p2/tools/P2DependencyTreeGenerator.java b/tycho-core/src/main/java/org/eclipse/tycho/p2/tools/P2DependencyTreeGenerator.java new file mode 100644 index 0000000000..814aadf7c5 --- /dev/null +++ b/tycho-core/src/main/java/org/eclipse/tycho/p2/tools/P2DependencyTreeGenerator.java @@ -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.
+ * Used by e.g. the dependency-tree Mojo, in order to mimic the behavior of the native Maven + * dependency-tree Mojo.
+ * 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 buildDependencyTree(MavenProject project, Set 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 = projectManager.getTychoProject(project); + if (tychoProject.isEmpty()) { + return Collections.emptyList(); + } + + List artifacts = tychoProject.get() // + .getDependencyArtifacts(DefaultReactorProject.adapt(project)) // + .getArtifacts(); + Set units = artifacts.stream() // + .flatMap(d -> d.getInstallableUnits().stream()) // + .collect(Collectors.toCollection(HashSet::new)); + List 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 COMPARATOR = Comparator.comparing(IInstallableUnit::getId, + String.CASE_INSENSITIVE_ORDER); + private final IInstallableUnit iu; + private final IRequirement satisfies; + private final List 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 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 create(List initial, Set units, + Set unmapped) { + List 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 units) { + List collected = new ArrayList<>(); + Map requirementsMap = new HashMap<>(); + IInstallableUnit unit = node.getInstallableUnit(); + // + Stream.concat(unit.getRequirements().stream(), unit.getMetaRequirements().stream()).forEach(requirement -> { + for (Iterator 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); + } + } + } +} diff --git a/tycho-p2-plugin/src/main/java/org/eclipse/tycho/plugins/p2/DependenciesTreeMojo.java b/tycho-p2-plugin/src/main/java/org/eclipse/tycho/plugins/p2/DependenciesTreeMojo.java index d526a4c69a..6109353856 100644 --- a/tycho-p2-plugin/src/main/java/org/eclipse/tycho/plugins/p2/DependenciesTreeMojo.java +++ b/tycho-p2-plugin/src/main/java/org/eclipse/tycho/plugins/p2/DependenciesTreeMojo.java @@ -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 @@ -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; @@ -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 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 = projectManager.getTychoProject(project); if (tychoProject.isEmpty()) { return; } - Set written = new HashSet(); - written.add(project.getId()); getLog().info(project.getId()); - List artifacts = tychoProject.get() - .getDependencyArtifacts(DefaultReactorProject.adapt(project)).getArtifacts(); - Map> projectMap = artifacts.stream() - .filter(a -> a.getMavenProject() != null).flatMap(a -> { - return a.getInstallableUnits().stream().map(iu -> new SimpleEntry<>(iu, a.getMavenProject())); - }) + List artifacts = tychoProject.get() // + .getDependencyArtifacts(DefaultReactorProject.adapt(project)) // + .getArtifacts(); + Map> 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 units = artifacts.stream().flatMap(d -> d.getInstallableUnits().stream()) - .collect(Collectors.toCollection(HashSet::new)); - List initial; + + Set unmapped = new HashSet<>(); + List dependencyTree; try { - initial = new ArrayList( - 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 units, - Map> projectMap, int indent, boolean last) { + private void printUnit(DependencyTreeNode model, Map> projectMap, + int indent) { + IInstallableUnit unit = model.getInstallableUnit(); + IRequirement satisfies = model.getRequirement(); StringBuffer line = new StringBuffer(); for (int i = 0; i < indent; i++) { line.append(" "); @@ -129,24 +113,8 @@ private void printUnit(IInstallableUnit unit, IRequirement satisfies, Set collected = new ArrayList(); - Map requirementsMap = new HashMap(); - Stream.concat(unit.getRequirements().stream(), unit.getMetaRequirements().stream()).forEach(requirement -> { - for (Iterator 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); } } - }