Skip to content

Commit

Permalink
Add a mojo that generates a target platform from the current reactor
Browse files Browse the repository at this point in the history
If one currently uses p2 repositories in Tycho this is quite convenient
as Tycho just fetches what ever is required by the project. As PDE has
no such feature yet, it is on the other hand not very usable in the IDE
itself or one needs to manage a second target platform for development.

This now adds a new tycho-dependency-tools-plugin:generate-target mojo
that collects all dependencies of the reactor, maps them to update sites
whenever possible and reduces the set then to the root units that are at
the top of the dependency chain. Finally these units are tried to be
matched to features of the site to finally select a set of units that
are suitable for target resolution with planner.
  • Loading branch information
laeubi committed Feb 15, 2024
1 parent c06787c commit 644c52e
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public IQueryable<IInstallableUnit> getCompositeMetadataRepository(Collection<Re
* @return the {@link IMetadataRepository} for the given {@link Repository}
* @throws ProvisionException if loading the repository failed
*/
public IMetadataRepository getMetadataRepositor(MavenRepositoryLocation repository)
public IMetadataRepository getMetadataRepository(MavenRepositoryLocation repository)
throws ProvisionException {
return getMetadataRepositor(repository.getURL(), repository.getId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public interface TychoProject {
*/
public DependencyArtifacts getDependencyArtifacts(ReactorProject project);

public DependencyArtifacts getDependencyArtifacts(MavenProject project);

/**
* Returns resolved project dependencies. For projects targeting multiple runtime environments,
* returned collection includes artifacts for all supported runtime environments.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.eclipse.tycho.ArtifactKey;
import org.eclipse.tycho.ClasspathEntry;
import org.eclipse.tycho.DefaultArtifactKey;
import org.eclipse.tycho.DependencyArtifacts;
import org.eclipse.tycho.ExecutionEnvironmentConfiguration;
import org.eclipse.tycho.ReactorProject;
import org.eclipse.tycho.ResolvedArtifactKey;
Expand Down Expand Up @@ -185,7 +186,7 @@ public TargetPlatformConfiguration getTargetPlatformConfiguration(ReactorProject
public Collection<TargetEnvironment> getTargetEnvironments(MavenProject project) {
TychoProject tychoProject = projectTypes.get(project.getPackaging());
if (tychoProject != null) {
//these will already be filtered at reading the target configuration
//these will already be filtered at reading the target configuration
return getTargetPlatformConfiguration(project).getEnvironments();
}
//if no tycho project, just assume the default running environment
Expand All @@ -199,6 +200,10 @@ public Optional<TychoProject> getTychoProject(MavenProject project) {
return Optional.ofNullable(projectTypes.get(project.getPackaging()));
}

public Optional<DependencyArtifacts> getDependencyArtifacts(MavenProject project) {
return getTychoProject(project).map(tp -> tp.getDependencyArtifacts(project));
}

public Optional<TychoProject> getTychoProject(ReactorProject project) {
if (project == null) {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ public abstract class AbstractTychoProject extends AbstractLogEnabled implements
@Requirement(hint = "p2")
protected DependencyResolver dependencyResolver;

@Override
public DependencyArtifacts getDependencyArtifacts(MavenProject project) {
return getDependencyArtifacts(DefaultReactorProject.adapt(project));
}

@Override
public DependencyArtifacts getDependencyArtifacts(ReactorProject reactorProject) {
return reactorProject.computeContextValue(TychoConstants.CTX_DEPENDENCY_ARTIFACTS, () -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public Map<String, IP2Artifact> getProjectBaseline(Collection<MavenRepositoryLoc

for (MavenRepositoryLocation location : baselineLocations) {
try {
baselineUnits.add(repositoryManager.getMetadataRepositor(location));
baselineUnits.add(repositoryManager.getMetadataRepository(location));
baselineArtifacts.add(repositoryManager.getArtifactRepository(location));
} catch (ProvisionException e) {
// baseline repository may not exist yet
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*******************************************************************************
* Copyright (c) 2023 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
*******************************************************************************/
package org.eclipse.tycho.extras.pde;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Stream;

import org.apache.maven.execution.MavenSession;
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.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.IRequirement;
import org.eclipse.equinox.p2.metadata.MetadataFactory;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
import org.eclipse.tycho.DependencyArtifacts;
import org.eclipse.tycho.MavenRepositoryLocation;
import org.eclipse.tycho.TychoConstants;
import org.eclipse.tycho.core.TychoProjectManager;
import org.eclipse.tycho.p2maven.repository.P2RepositoryManager;

/**
* This mojo generates a target platform from all the dependencies of a maven build for example to
* be used inside of PDE
*/
@Mojo(name = "generate-target", defaultPhase = LifecyclePhase.NONE, requiresProject = true, threadSafe = true, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME, aggregator = true)
public class GenerateTargetMojo extends AbstractMojo {
@Component
private TychoProjectManager projectManager;

@Component
private MavenSession mavenSession;

@Component
private MavenProject mavenProject;

@Component
private LegacySupport legacySupport;

@Component
private P2RepositoryManager repositoryManager;

@Parameter(property = "generateTargetFile", defaultValue = "${project.build.directory}/generate.target", required = true)
private File targetFile;

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
Log log = getLog();
log.info("Scan reactor for dependencies...");
List<MavenProject> projects = mavenSession.getProjects();
List<IInstallableUnit> reactorDependencies = projects.stream().parallel().unordered().flatMap(project -> {
MavenSession old = legacySupport.getSession();
try {
MavenSession clone = mavenSession.clone();
clone.setCurrentProject(project);
legacySupport.setSession(clone);
return projectManager.getDependencyArtifacts(project).map(DependencyArtifacts::getNonReactorUnits)
.stream().flatMap(Collection::stream).filter(iu -> {
if (iu.getId().endsWith(".source")) {
return false;
}
if (iu.getId().endsWith(".feature.jar")) {
return false;
}
return true;
});
} finally {
legacySupport.setSession(old);
}
}).distinct().toList();
List<String> repoList = projects.stream().flatMap(project -> {
Stream<String> pomRepos = project.getRepositories().stream().filter(repo -> "p2".equals(repo.getLayout()))
.map(repo -> repo.getUrl());
return pomRepos;
}).distinct().sorted().toList();
log.info("Found " + reactorDependencies.size() + " dependencies and " + repoList.size()
+ " possible repositories: ");
Map<IMetadataRepository, Set<IInstallableUnit>> repo2unitMap = new LinkedHashMap<>();
Set<IInstallableUnit> notFound = new HashSet<>(reactorDependencies);
for (String repository : repoList) {
log.info("\tScanning " + repository + "...");
try {
IMetadataRepository metadataRepository = repositoryManager
.getMetadataRepository(new MavenRepositoryLocation(null, URI.create(repository)));
Set<IInstallableUnit> units = new HashSet<>();
for (IInstallableUnit unit : reactorDependencies) {
if (metadataRepository.contains(unit)) {
units.add(unit);
notFound.remove(unit);
}
}
if (units.size() > 0) {
repo2unitMap.put(metadataRepository, units);
log.info("\tFound: " + units.size() + " dependencies in this repository!");
}
} catch (ProvisionException e) {
throw new MojoFailureException("can't load repository " + repository, e);
}
}
if (notFound.size() > 0) {
log.info(notFound.size() + " dependencies where not mapped to a repository:");
for (IInstallableUnit unit : notFound) {
log.info("\t" + unit);
}
}
log.info("Generate Target ...");
StringBuilder builder = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
+ "<?pde version=\"3.8\"?>\n<target name=\"" + mavenProject.getName() + "\">\n\t<locations>\n");
for (Entry<IMetadataRepository, Set<IInstallableUnit>> entry : repo2unitMap.entrySet()) {
builder.append("\t\t<location includeMode=\"planner\" type=\"InstallableUnit\">\n");
Set<IInstallableUnit> requiredUnits = entry.getValue();
//First step is to remove everything that is already required by something...
Set<IInstallableUnit> alreayTransitiveRequired = computeRequired(requiredUnits);
requiredUnits.removeAll(alreayTransitiveRequired);
IMetadataRepository repository = entry.getKey();
if (!hasOnlyFeatures(requiredUnits)) {
Set<IInstallableUnit> strict = new HashSet<>(requiredUnits);
//Next is we try to find all features that do require something we still want to have
IQueryResult<IInstallableUnit> features = repository.query(QueryUtil.createIUGroupQuery(), null);
Set<IInstallableUnit> providingFeatures = computeFeatureRequires(features, requiredUnits);
requiredUnits.addAll(providingFeatures);
//Now a final reduction step
Set<IInstallableUnit> transitiveRequiredFinal = computeRequired(requiredUnits);
requiredUnits.removeAll(transitiveRequiredFinal);
if (!strict.equals(requiredUnits)) {
builder.append("\t\t\t<!-- List of strictly required units -->\n");
for (IInstallableUnit unit : strict) {
builder.append("\t\t\t<!-- ");
addUnit(unit, builder);
builder.append(" -->\n");
}
builder.append(
"\n\t\t\t<!-- Required units using features, this might include more than is strictly required -->\n");
}
}
for (IInstallableUnit unit : requiredUnits) {
builder.append("\t\t\t");
addUnit(unit, builder);
builder.append("\n");
}
builder.append("\t\t\t<repository location=\"" + repository.getLocation() + "\"/>\n");
builder.append("\t\t</location>\n");
}
if (notFound.size() > 0) {
List<String> osgiMaven = new ArrayList<>();
List<String> wrappedMaven = new ArrayList<>();
for (IInstallableUnit mavenUnit : notFound) {
String osgi = getMavenDependency(mavenUnit, builder, TychoConstants.PROP_GROUP_ID,
TychoConstants.PROP_ARTIFACT_ID, TychoConstants.PROP_VERSION);
if (osgi == null) {
String wrapped = getMavenDependency(mavenUnit, builder, TychoConstants.PROP_WRAPPED_GROUP_ID,
TychoConstants.PROP_WRAPPED_ARTIFACT_ID, TychoConstants.PROP_WRAPPED_VERSION);
if (wrapped != null) {
wrappedMaven.add(wrapped);
} else {
log.warn(mavenUnit + " can not be represented in the target at all!");
}
} else {
osgiMaven.add(osgi);
}
}
createMavenLocation(osgiMaven, "error", builder);
createMavenLocation(wrappedMaven, "generate", builder);
}
builder.append("\t</locations>\n</target>");
try {
targetFile.getParentFile().mkdirs();
Files.writeString(targetFile.toPath(), builder, StandardCharsets.UTF_8);
log.info("Target is written to " + targetFile.getAbsolutePath());
} catch (IOException e) {
throw new MojoFailureException("writing target file failed", e);
}
}

private void createMavenLocation(List<String> artifacts, String mm, StringBuilder builder) {
builder.append("\t\t<location includeDependencyDepth=\"none\" missingManifest=\"");
builder.append(mm);
builder.append("\" type=\"Maven\">\n");
for (String artifact : artifacts) {
builder.append("\t\t\t");
builder.append(artifact);
builder.append("\n");
}
builder.append("\t\t</location>\n");
}

private String getMavenDependency(IInstallableUnit mavenUnit, StringBuilder builder, String groupProperty,
String artifactProperty, String versionProperty) {
String groupId = mavenUnit.getProperty(groupProperty);
String artifactId = mavenUnit.getProperty(artifactProperty);
String version = mavenUnit.getProperty(versionProperty);
if (groupId != null && artifactId != null && version != null) {
return String.format(
"<dependency>\n\t\t\t\t<groupId>%s</groupId>\n\t\t\t\t<artifactId>%s</artifactId>\n\t\t\t\t<version>%s</version>\n\t\t\t</dependency>",
groupId, artifactId, version);
}
return null;
}

private boolean hasOnlyFeatures(Set<IInstallableUnit> requiredUnits) {
for (IInstallableUnit unit : requiredUnits) {
if (!Boolean.TRUE.toString().equals(unit.getProperty(QueryUtil.PROP_TYPE_GROUP))) {
return false;
}
}
return true;
}

private void addUnit(IInstallableUnit unit, StringBuilder builder) {
builder.append("<unit id=\"");
builder.append(unit.getId());
builder.append("\" version=\"");
builder.append(unit.getVersion());
builder.append("\"/>");
}

private Set<IInstallableUnit> computeFeatureRequires(IQueryResult<IInstallableUnit> features,
Set<IInstallableUnit> units) {
HashSet<IInstallableUnit> result = new HashSet<>();
outer: for (IInstallableUnit feature : features) {
if (units.contains(feature)) {
continue;
}
if (Boolean.TRUE.toString()
.equals(feature.getProperty(MetadataFactory.InstallableUnitDescription.PROP_TYPE_PRODUCT))) {
continue;
}
for (IRequirement featureRequirement : feature.getRequirements()) {
for (IInstallableUnit unit : units) {
if (unit.satisfies(featureRequirement)) {
result.add(feature);
continue outer;
}
}
}
}
return result;
}

private Set<IInstallableUnit> computeRequired(Set<IInstallableUnit> units) {
Set<IInstallableUnit> required = new HashSet<>();
for (IInstallableUnit base : units) {
for (IRequirement requirement : base.getRequirements()) {
for (IInstallableUnit other : units) {
if (other == base) {
continue;
}
if (other.satisfies(requirement)) {
required.add(other);
}
}
}
}
return required;
}

}

0 comments on commit 644c52e

Please sign in to comment.