-
Notifications
You must be signed in to change notification settings - Fork 194
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create a tycho-baseline:check-dependencies mojo to validate versions
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
Showing
16 changed files
with
1,580 additions
and
568 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
294 changes: 294 additions & 0 deletions
294
tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,294 @@ | ||
/******************************************************************************* | ||
* Copyright (c) 2025 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.nio.file.Path; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.Enumeration; | ||
import java.util.HashMap; | ||
import java.util.LinkedHashMap; | ||
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.function.Function; | ||
import java.util.jar.JarEntry; | ||
import java.util.jar.JarFile; | ||
import java.util.stream.Collectors; | ||
|
||
import org.apache.maven.execution.MavenSession; | ||
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.baseline.analyze.ClassMethods; | ||
import org.eclipse.tycho.baseline.analyze.ClassUsage; | ||
import org.eclipse.tycho.baseline.analyze.JrtClasses; | ||
import org.eclipse.tycho.baseline.analyze.MethodSignature; | ||
import org.eclipse.tycho.core.TychoProjectManager; | ||
import org.eclipse.tycho.core.maven.OSGiJavaToolchain; | ||
import org.eclipse.tycho.core.maven.ToolchainProvider; | ||
import org.eclipse.tycho.core.osgitools.BundleReader; | ||
import org.eclipse.tycho.core.osgitools.OsgiManifest; | ||
import org.eclipse.tycho.core.resolver.target.ArtifactMatcher; | ||
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 { | ||
|
||
private static final String CLASS_SUFFIX = ".class"; | ||
|
||
@Parameter(property = "project", readonly = true) | ||
private MavenProject project; | ||
|
||
@Parameter(property = "session", readonly = true) | ||
private MavenSession session; | ||
|
||
@Component | ||
private TychoProjectManager projectManager; | ||
|
||
@Component | ||
private List<ArtifactVersionProvider> versionProvider; | ||
|
||
@Component | ||
private BundleReader bundleReader; | ||
|
||
@Component | ||
ToolchainProvider toolchainProvider; | ||
|
||
@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"); | ||
} | ||
JrtClasses jrtClassResolver = getJRTClassResolver(); | ||
List<ClassUsage> usages = analyzeUsage(file, jrtClassResolver); | ||
if (usages.isEmpty()) { | ||
return; | ||
} | ||
Collection<IInstallableUnit> units = artifacts.getInstallableUnits(); | ||
ModuleRevisionBuilder builder = readOSGiInfo(file); | ||
List<GenericInfo> requirements = builder.getRequirements(); | ||
List<DependencyVersionProblem> dependencyProblems = new ArrayList<>(); | ||
Map<Path, ClassProvides> analyzeCache = new HashMap<>(); | ||
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; | ||
} | ||
IInstallableUnit unit = packageProvidingUnit.get(); | ||
VersionRange versionRange = VersionRange.valueOf(packageVersion); | ||
List<ArtifactVersion> list = versionProvider.stream() | ||
.flatMap(avp -> avp.getPackageVersions(unit, packageName, versionRange, project)).toList(); | ||
System.out.println("== " + packageName + " " + packageVersion + " is provided by " + unit | ||
+ " with version range " + versionRange + ", matching versions: " + list.stream() | ||
.map(av -> av.getVersion()).map(String::valueOf).collect(Collectors.joining(", "))); | ||
Map<MethodSignature, DependencyVersionProblem> problemMap = new LinkedHashMap<>(); | ||
for (ClassUsage usage : usages) { | ||
Set<MethodSignature> packageMethods = usage.signatures() | ||
.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! | ||
// TODO a class can also reference fields! | ||
continue; | ||
} | ||
// for (MethodSignature signature : packageMethods) { | ||
// System.out.println("\t" + signature.id()); | ||
// } | ||
// now we need to inspect all jars | ||
for (ArtifactVersion v : list) { | ||
Path artifact = v.getArtifact(); | ||
if (artifact == null) { | ||
// Retrieval of artifacts might be lazy and we can't get this one --> error? | ||
continue; | ||
} | ||
ClassProvides provides = analyzeCache.get(artifact); | ||
if (provides == null) { | ||
provides = analyzeProvides(artifact.toFile(), jrtClassResolver); | ||
analyzeCache.put(artifact, provides); | ||
} | ||
Map<String, List<MethodSignature>> group = null; | ||
for (MethodSignature mthd : packageMethods) { | ||
if (!provides.signatures().contains(mthd)) { | ||
if (group == null) { | ||
group = provides.signatures().stream() | ||
.collect(Collectors.groupingBy(ms -> ms.className())); | ||
} | ||
System.out.println("Not found: " + mthd); | ||
List<MethodSignature> provided = group.get(mthd.className()); | ||
if (provided != null) { | ||
for (MethodSignature s : provided) { | ||
System.out.println("Provided: " + s); | ||
} | ||
} | ||
problemMap.computeIfAbsent(mthd, s -> new DependencyVersionProblem(String.format( | ||
"Import-Package '%s %s (compiled against %s %s) includes %s (provided by %s) but this version is missing the method %s", | ||
packageName, packageVersion, unit.getId(), unit.getVersion(), v.getVersion(), | ||
v.getProvider(), mthd.id()), new TreeSet<>())).references() | ||
.addAll(usage.classRef(mthd)); | ||
} | ||
} | ||
} | ||
// TODO we should emit a warning if the lower bound is not part of the | ||
// discovered versions (or even fail?) | ||
} | ||
problemMap.values().forEach(dependencyProblems::add); | ||
|
||
} | ||
} | ||
for (DependencyVersionProblem problem : dependencyProblems) { | ||
getLog().error(String.format("%s, referenced by:%s%s", problem.message(), System.lineSeparator(), | ||
problem.references().stream().collect(Collectors.joining(System.lineSeparator())))); | ||
} | ||
} | ||
|
||
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 List<ClassUsage> analyzeUsage(File file, JrtClasses jre) | ||
throws MojoFailureException { | ||
List<ClassUsage> usages = new ArrayList<>(); | ||
try { | ||
Set<MethodSignature> usedMethodSignatures = new TreeSet<>(); | ||
Map<MethodSignature, Collection<String>> classRef = new HashMap<>(); | ||
try (JarFile jar = new JarFile(file)) { | ||
Enumeration<JarEntry> entries = jar.entries(); | ||
while (entries.hasMoreElements()) { | ||
JarEntry jarEntry = entries.nextElement(); | ||
String name = jarEntry.getName(); | ||
if (name.endsWith(CLASS_SUFFIX)) { | ||
InputStream stream = jar.getInputStream(jarEntry); | ||
usages.add(new ClassUsage(stream.readAllBytes(), jre)); | ||
} | ||
} | ||
} | ||
return usages; | ||
} catch (IOException e) { | ||
throw new MojoFailureException(e); | ||
} | ||
} | ||
|
||
private ClassProvides analyzeProvides(File file, Function<String, Optional<ClassMethods>> classResolver) | ||
throws MojoFailureException { | ||
try { | ||
List<ClassMethods> classes = new ArrayList<>(); | ||
try (JarFile jar = new JarFile(file)) { | ||
Enumeration<JarEntry> entries = jar.entries(); | ||
while (entries.hasMoreElements()) { | ||
JarEntry jarEntry = entries.nextElement(); | ||
String name = jarEntry.getName(); | ||
if (name.endsWith(CLASS_SUFFIX)) { | ||
InputStream stream = jar.getInputStream(jarEntry); | ||
classes.add(new ClassMethods(stream.readAllBytes(), classResolver)); | ||
} | ||
} | ||
} | ||
return new ClassProvides(classes.stream().flatMap(cm -> cm.provides()).collect(Collectors.toSet())); | ||
} catch (IOException e) { | ||
// TODO | ||
System.err.println(e); | ||
return new ClassProvides(List.of()); | ||
// throw new MojoFailureException(e); | ||
} | ||
} | ||
|
||
|
||
private JrtClasses getJRTClassResolver() { | ||
String profileName = projectManager.getExecutionEnvironments(project, session).findFirst() | ||
.map(ee -> ee.getProfileName()).orElse(null); | ||
if (profileName != null) { | ||
OSGiJavaToolchain osgiToolchain = toolchainProvider.getToolchain(profileName).orElse(null); | ||
if (osgiToolchain != null) { | ||
return new JrtClasses(osgiToolchain.getJavaHome()); | ||
} | ||
} | ||
//use running jvm | ||
return new JrtClasses(null); | ||
} | ||
|
||
private static record ClassProvides(Collection<MethodSignature> signatures) { | ||
|
||
} | ||
|
||
private static record DependencyVersionProblem(String message, Collection<String> references) { | ||
|
||
} | ||
} |
Oops, something went wrong.