Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tycho-4.0.x] Move p2 dependency tree logic from tycho-p2-plugin to tycho-core #3421

Merged
merged 1 commit into from
Jan 27, 2024
Merged
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
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);
}
}

}