Skip to content

Commit

Permalink
Create a tycho-baseline:check-dependencies mojo to validate versions
Browse files Browse the repository at this point in the history
If version ranges on packages or bundles are used it is currently quite
hard to ensure these are actually work for the provided ranges
especially if they evolve over a long time.

This now adds a new tycho-baseline:check-dependencies mojo that can help
in this task by inspecting the byte-code of the project and the
dependencies if there is any inconsistency in any of the dependencies
matching the version range.
  • Loading branch information
laeubi committed Jan 12, 2025
1 parent db65ab4 commit 1edc533
Show file tree
Hide file tree
Showing 9 changed files with 1,087 additions and 568 deletions.
14 changes: 14 additions & 0 deletions tycho-baseline-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,27 @@
<artifactId>asciitable</artifactId>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.7.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore</artifactId>
<version>2.38.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-component-metadata</artifactId>
</plugin>
<plugin>
<groupId>org.eclipse.sisu</groupId>
<artifactId>sisu-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*******************************************************************************
* Copyright (c) 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
* 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.baseline;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
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.project.MavenProject;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.osgi.container.ModuleRevisionBuilder;
import org.eclipse.osgi.container.ModuleRevisionBuilder.GenericInfo;
import org.eclipse.osgi.container.builders.OSGiManifestBuilderFactory;
import org.eclipse.osgi.internal.framework.FilterImpl;
import org.eclipse.tycho.DependencyArtifacts;
import org.eclipse.tycho.artifacts.ArtifactVersion;
import org.eclipse.tycho.artifacts.ArtifactVersionProvider;
import org.eclipse.tycho.core.TychoProjectManager;
import org.eclipse.tycho.core.osgitools.BundleReader;
import org.eclipse.tycho.core.osgitools.OsgiManifest;
import org.eclipse.tycho.core.resolver.target.ArtifactMatcher;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.osgi.framework.BundleException;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.VersionRange;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.resource.Namespace;

/**
* This mojos performs deep inspections of dependencies to find out if a version
* range is actually valid. For this the following steps are performed:
* <ol>
* <li>The current project artifact is analyzed for method signatures it
* calls</li>
* <li>Then it is checked what of these match to a given dependency</li>
* <li>All dependency versions matching the range are fetched and inspected
* using {@link ArtifactVersionProvider}s</li>
* <li>Then it checks if there are any missing signatures or inconsistencies and
* possibly failing the build</li>
* </ol>
*/
@Mojo(defaultPhase = LifecyclePhase.VERIFY, name = "check-dependencies", threadSafe = true, requiresProject = true)
public class DependencyCheckMojo extends AbstractMojo {

@Parameter(property = "project", readonly = true)
private MavenProject project;

@Component
private TychoProjectManager projectManager;

@Component
private List<ArtifactVersionProvider> versionProvider;

@Component
private BundleReader bundleReader;

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
// TODO check for packaging types...? But maven now also provides profiles on
// packaging types!
DependencyArtifacts artifacts = projectManager.getDependencyArtifacts(project).orElse(null);
File file = project.getArtifact().getFile();
if (file == null || !file.isFile()) {
throw new MojoFailureException("Project artifact is not a valid file");
}
ClassUsage usages = analyzeUsage(file);
Collection<IInstallableUnit> units = artifacts.getInstallableUnits();
ModuleRevisionBuilder builder = readOSGiInfo(file);
List<GenericInfo> requirements = builder.getRequirements();
for (GenericInfo genericInfo : requirements) {
if (PackageNamespace.PACKAGE_NAMESPACE.equals(genericInfo.getNamespace())) {
Map<String, String> pkgInfo = getVersionInfo(genericInfo,
PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE);
String packageVersion = pkgInfo.getOrDefault(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, "0.0.0");
String packageName = pkgInfo.get(PackageNamespace.PACKAGE_NAMESPACE);
Optional<IInstallableUnit> packageProvidingUnit = ArtifactMatcher.findPackage(packageName, units);
if (packageProvidingUnit.isEmpty()) {
continue;
}
Set<MethodSignature> packageMethods = usages.signatures().stream()
.filter(ms -> packageName.equals(ms.packageName()))
.collect(Collectors.toCollection(LinkedHashSet::new));
if (packageMethods.isEmpty()) {
// it could be that actually no methods referenced (e.g. interface is only
// referencing a type)
// TODO we need to check that the types used are present in all versions as
// otherwise we will get CNF exception!
continue;
}
IInstallableUnit unit = packageProvidingUnit.get();
VersionRange versionRange = VersionRange.valueOf(packageVersion);
System.out.println("== " + packageName + " " + packageVersion + " is provided by " + unit
+ " with version range " + versionRange + ", used method signatures from package are:");
for (MethodSignature signature : packageMethods) {
System.out.println("\t" + signature.id());
}
List<ArtifactVersion> list = versionProvider.stream()
.flatMap(avp -> avp.getPackageVersions(unit, packageName, versionRange, project)).toList();
System.out.println("Matching versions:");
for (ArtifactVersion v : list) {
System.out.println("\t" + v);
}
// TODO we should emit a warning if the lower bound is not part of the
// discovered versions (or even fail?)
}
}
}

private Map<String, String> getVersionInfo(GenericInfo genericInfo, String versionAttribute) {
Map<String, String> directives = new HashMap<>(genericInfo.getDirectives());
String filter = directives.remove(Namespace.REQUIREMENT_FILTER_DIRECTIVE);
FilterImpl filterImpl;
try {
filterImpl = FilterImpl.newInstance(filter);
} catch (InvalidSyntaxException e) {
throw new IllegalArgumentException("Invalid filter directive", e); //$NON-NLS-1$
}
return filterImpl.getStandardOSGiAttributes(versionAttribute);
}

private ModuleRevisionBuilder readOSGiInfo(File file) throws MojoFailureException {
OsgiManifest manifest = bundleReader.loadManifest(file);
ModuleRevisionBuilder builder;
try {
builder = OSGiManifestBuilderFactory.createBuilder(manifest.getHeaders());
} catch (BundleException e) {
throw new MojoFailureException(e);
}
return builder;
}

private ClassUsage analyzeUsage(File file) throws MojoFailureException {
try {
Set<MethodSignature> usedMethodSignatures = new TreeSet<>();
try (JarFile jar = new JarFile(file)) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
if (jarEntry.getName().endsWith(".class")) {
InputStream stream = jar.getInputStream(jarEntry);
ClassReader reader = new ClassReader(stream.readAllBytes());
reader.accept(new ClassVisitor(Opcodes.ASM9) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
return new MethodVisitor(Opcodes.ASM9) {
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String descriptor, boolean isInterface) {
if (owner.startsWith("java/")) {
// ignore references to java core classes
return;
}
usedMethodSignatures
.add(new MethodSignature(owner.replace('/', '.'), name, descriptor));
}
};
}
}, ClassReader.SKIP_FRAMES);
}
}
}
return new ClassUsage(usedMethodSignatures);
} catch (IOException e) {
throw new MojoFailureException(e);
}
}

private static record ClassUsage(Collection<MethodSignature> signatures) {

}

private static record MethodSignature(String className, String methodName, String signature)
implements Comparable<MethodSignature> {
public String packageName() {
String cn = className();
int idx = cn.lastIndexOf('.');
if (idx > 0) {
String substring = cn.substring(0, idx);
return substring;
}
return cn;
}

public String id() {
return className() + "#" + methodName() + signature();
}

@Override
public int compareTo(MethodSignature o) {
return id().compareTo(o.id());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*******************************************************************************
* Copyright (c) 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
* 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.baseline;

import java.io.File;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import javax.inject.Inject;
import javax.inject.Named;

import org.apache.maven.project.MavenProject;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.Version;
import org.eclipse.equinox.spi.p2.publisher.PublisherHelper;
import org.eclipse.tycho.artifacts.ArtifactVersion;
import org.eclipse.tycho.artifacts.ArtifactVersionProvider;
import org.eclipse.tycho.copyfrom.oomph.P2Index;
import org.eclipse.tycho.copyfrom.oomph.P2Index.Repository;
import org.eclipse.tycho.copyfrom.oomph.P2IndexImpl;
import org.eclipse.tycho.p2maven.transport.TransportCacheConfig;
import org.osgi.framework.VersionRange;

/**
* {@link ArtifactVersionProvider} using eclipse index
*/
@Named
public class EclipseIndexArtifactVersionProvider implements ArtifactVersionProvider {

private P2Index p2Index;

@Inject
public EclipseIndexArtifactVersionProvider(TransportCacheConfig cacheConfig) {
p2Index = new P2IndexImpl(new File(cacheConfig.getCacheLocation(), "index"));
}

@Override
public Stream<ArtifactVersion> getPackageVersions(IInstallableUnit unit, String packageName,
VersionRange versionRange, MavenProject mavenProject) {
Map<Repository, Set<Version>> map = p2Index.lookupCapabilities(PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE,
packageName);
Set<Version> found = new HashSet<>();
return map.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.filter(v -> v.isOSGiCompatible()).filter(v -> found.add(v))
.map(version -> new EclipseIndexArtifactVersion(entry.getKey(), packageName, version)))
.filter(eia -> versionRange.includes(eia.getVersion()))
.sorted(Comparator.comparing(EclipseIndexArtifactVersion::getVersion).reversed())
.map(ArtifactVersion.class::cast);
}

private class EclipseIndexArtifactVersion implements ArtifactVersion {

private Version version;
private Repository repository;
private String packageName;

public EclipseIndexArtifactVersion(Repository repository, String packageName, Version version) {
this.repository = repository;
this.packageName = packageName;
this.version = version;
}

@Override
public Path getArtifact() {
// TODO search in repo and fetch artifact!
return null;
}

@Override
public org.osgi.framework.Version getVersion() {
return org.osgi.framework.Version.parseVersion(version.getOriginal());
}

@Override
public String toString() {
return getVersion() + " (from repository " + repository + ")";
}

}

}
Loading

0 comments on commit 1edc533

Please sign in to comment.