-
Notifications
You must be signed in to change notification settings - Fork 197
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
[bp} Create 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.
Showing
18 changed files
with
1,844 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
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
360 changes: 360 additions & 0 deletions
360
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,360 @@ | ||
/******************************************************************************* | ||
* 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.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
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.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.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.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.PackagingType; | ||
import org.eclipse.tycho.artifacts.ArtifactVersion; | ||
import org.eclipse.tycho.artifacts.ArtifactVersionProvider; | ||
import org.eclipse.tycho.baseline.analyze.ClassCollection; | ||
import org.eclipse.tycho.baseline.analyze.ClassMethods; | ||
import org.eclipse.tycho.baseline.analyze.ClassUsage; | ||
import org.eclipse.tycho.baseline.analyze.DependencyAnalyzer; | ||
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.eclipse.tycho.model.manifest.MutableBundleManifest; | ||
import org.osgi.framework.BundleException; | ||
import org.osgi.framework.InvalidSyntaxException; | ||
import org.osgi.framework.Version; | ||
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; | ||
|
||
@Parameter(property = "session", readonly = true) | ||
private MavenSession session; | ||
|
||
@Parameter(defaultValue = "${project.build.directory}/versionProblems.txt", property = "tycho.dependency.check.report") | ||
private File reportFileName; | ||
|
||
@Parameter(defaultValue = "${project.basedir}/META-INF/MANIFEST.MF", property = "tycho.dependency.check.manifest") | ||
private File manifestFile; | ||
|
||
@Parameter(defaultValue = "false", property = "tycho.dependency.check.apply") | ||
private boolean applySuggestions; | ||
|
||
@Component | ||
private TychoProjectManager projectManager; | ||
|
||
@Component | ||
private List<ArtifactVersionProvider> versionProvider; | ||
|
||
@Component | ||
private BundleReader bundleReader; | ||
|
||
@Component | ||
ToolchainProvider toolchainProvider; | ||
|
||
@Override | ||
public void execute() throws MojoExecutionException, MojoFailureException { | ||
if (!"jar".equals(project.getPackaging()) | ||
&& !PackagingType.TYPE_ECLIPSE_PLUGIN.equals(project.getPackaging())) { | ||
return; | ||
} | ||
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 = DependencyAnalyzer.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, ClassCollection> analyzeCache = new HashMap<>(); | ||
Log log = getLog(); | ||
Map<String, Version> lowestPackageVersion = new HashMap<>(); | ||
Map<String, Set<Version>> allPackageVersion = new HashMap<>(); | ||
Set<String> packageWithError = new HashSet<>(); | ||
Function<String, Optional<ClassMethods>> classResolver = DependencyAnalyzer | ||
.createDependencyClassResolver(jrtClassResolver, artifacts); | ||
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; | ||
} | ||
if (packageName.contains(".internal.")) { | ||
// TODO configurable, but also internal packages should be properly versioned! | ||
continue; | ||
} | ||
IInstallableUnit unit = packageProvidingUnit.get(); | ||
Optional<org.eclipse.equinox.p2.metadata.Version> matchedPackageVersion = ArtifactMatcher | ||
.getPackageVersion(unit, packageName); | ||
matchedPackageVersion.filter(v -> v.isOSGiCompatible()) | ||
.ifPresent(v -> { | ||
Version current = new Version(v.toString()); | ||
allPackageVersion.computeIfAbsent(packageName, nil -> new TreeSet<>()).add(current); | ||
lowestPackageVersion.put(packageName, current); | ||
}); | ||
VersionRange versionRange = VersionRange.valueOf(packageVersion); | ||
List<ArtifactVersion> list = versionProvider.stream() | ||
.flatMap(avp -> avp.getPackageVersions(unit, packageName, versionRange, project)).toList(); | ||
if (log.isDebugEnabled()) { | ||
log.debug("== " + packageName + " " + packageVersion + " is provided by " + unit | ||
+ " with version range " + versionRange + ", matching versions: " + list.stream() | ||
.map(av -> av.getVersion()).map(String::valueOf).collect(Collectors.joining(", "))); | ||
} | ||
Set<MethodSignature> packageMethods = new TreeSet<>(); | ||
Map<MethodSignature, Collection<String>> references = new HashMap<>(); | ||
for (ClassUsage usage : usages) { | ||
usage.signatures().filter(ms -> packageName.equals(ms.packageName())).forEach(sig -> { | ||
packageMethods.add(sig); | ||
references.computeIfAbsent(sig, nil -> new TreeSet<>()).addAll(usage.classRef(sig)); | ||
}); | ||
} | ||
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; | ||
} | ||
if (log.isDebugEnabled()) { | ||
for (MethodSignature signature : packageMethods) { | ||
log.debug("Referenced: " + signature.id()); | ||
} | ||
} | ||
// now we need to inspect all jars | ||
for (ArtifactVersion v : list) { | ||
Version version = v.getVersion(); | ||
if (version == null) { | ||
continue; | ||
} | ||
if (!allPackageVersion.computeIfAbsent(packageName, nil -> new TreeSet<>()).add(version)) { | ||
// already checked! | ||
continue; | ||
} | ||
Path artifact = v.getArtifact(); | ||
log.debug(v + "=" + artifact); | ||
if (artifact == null) { | ||
// Retrieval of artifacts might be lazy and we can't get this one --> error? | ||
continue; | ||
} | ||
ClassCollection collection = analyzeCache.get(artifact); | ||
if (collection == null) { | ||
collection = DependencyAnalyzer.analyzeProvides(artifact.toFile(), classResolver, null); | ||
analyzeCache.put(artifact, collection); | ||
} | ||
boolean ok = true; | ||
Set<MethodSignature> set = collection.provides().collect(Collectors.toSet()); | ||
for (MethodSignature mthd : packageMethods) { | ||
if (!set.contains(mthd)) { | ||
List<MethodSignature> provided = collection.get(mthd.className()); | ||
if (provided != null) { | ||
provided = provided.stream().filter(ms -> packageName.equals(ms.packageName())) | ||
.toList(); | ||
} | ||
if (log.isDebugEnabled()) { | ||
log.debug("Not found: " + mthd); | ||
if (provided != null) { | ||
for (MethodSignature s : provided) { | ||
log.debug("Provided: " + s); | ||
} | ||
} | ||
} | ||
dependencyProblems.add(new DependencyVersionProblem(String.format( | ||
"Import-Package '%s %s (compiled against %s %s / %s) includes %s (provided by %s) but this version is missing the method %s", | ||
packageName, packageVersion, unit.getId(), unit.getVersion(), | ||
matchedPackageVersion.orElse(org.eclipse.equinox.p2.metadata.Version.emptyVersion) | ||
.toString(), | ||
v.getVersion(), v.getProvider(), mthd.id()), references.get(mthd), provided)); | ||
ok = false; | ||
packageWithError.add(packageName); | ||
} | ||
} | ||
if (ok) { | ||
lowestPackageVersion.merge(packageName, version, (v1, v2) -> { | ||
if (v1.compareTo(v2) > 0) { | ||
return v2; | ||
} | ||
return v1; | ||
}); | ||
} | ||
} | ||
// TODO we should emit a warning if the lower bound is not part of the | ||
// discovered versions (or even fail?) | ||
|
||
} | ||
} | ||
if (dependencyProblems.isEmpty()) { | ||
return; | ||
} | ||
List<String> results = new ArrayList<>(); | ||
for (DependencyVersionProblem problem : dependencyProblems) { | ||
Collection<String> references = problem.references(); | ||
String message; | ||
if (references == null || references.isEmpty()) { | ||
message = problem.message(); | ||
} else { | ||
message = String.format("%s, referenced by:%s%s", problem.message(), System.lineSeparator(), | ||
problem.references().stream().collect(Collectors.joining(System.lineSeparator()))); | ||
} | ||
log.error(message); | ||
results.add(message); | ||
List<MethodSignature> provided = problem.provided(); | ||
if (provided != null && !provided.isEmpty()) { | ||
results.add("Provided Methods in this version are:"); | ||
for (MethodSignature sig : provided) { | ||
results.add("\t" + sig); | ||
} | ||
} | ||
} | ||
for (String pkg : packageWithError) { | ||
String suggestion = "Suggested lower version for package " + pkg + " is " + lowestPackageVersion.get(pkg); | ||
Set<Version> all = allPackageVersion.get(pkg); | ||
if (all != null && !all.isEmpty()) { | ||
suggestion += " out of " + all; | ||
} | ||
log.info(suggestion); | ||
results.add(suggestion); | ||
} | ||
if (results.isEmpty()) { | ||
return; | ||
} | ||
try { | ||
Files.writeString(reportFileName.toPath(), | ||
results.stream().collect(Collectors.joining(System.lineSeparator()))); | ||
if (applySuggestions) { | ||
applyLowerBounds(packageWithError, lowestPackageVersion); | ||
} | ||
} catch (IOException e) { | ||
throw new MojoFailureException(e); | ||
} | ||
} | ||
|
||
private void applyLowerBounds(Set<String> packageWithError, Map<String, Version> lowestPackageVersion) | ||
throws IOException { | ||
MutableBundleManifest manifest = MutableBundleManifest.read(manifestFile); | ||
Map<String, String> exportedPackagesVersion = manifest.getExportedPackagesVersion(); | ||
Map<String, String> updates = new HashMap<>(); | ||
for (String packageName : packageWithError) { | ||
Version lowestVersion = lowestPackageVersion.getOrDefault(packageName, Version.emptyVersion); | ||
String current = exportedPackagesVersion.get(packageName); | ||
if (current == null) { | ||
updates.put(packageName, String.format("[%s,%d)", lowestVersion, (lowestVersion.getMajor() + 1))); | ||
} else { | ||
VersionRange range = VersionRange.valueOf(current); | ||
Version right = range.getRight(); | ||
updates.put(packageName, String.format("[%s,%s%c", lowestVersion, right, range.getRightType())); | ||
} | ||
} | ||
manifest.updateImportedPackageVersions(updates); | ||
MutableBundleManifest.write(manifest, manifestFile); | ||
} | ||
|
||
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 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 DependencyVersionProblem(String message, Collection<String> references, | ||
List<MethodSignature> provided) { | ||
|
||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassCollection.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,54 @@ | ||
/******************************************************************************* | ||
* 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.analyze; | ||
|
||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.function.Consumer; | ||
import java.util.function.Function; | ||
import java.util.stream.Stream; | ||
|
||
public class ClassCollection implements Function<String, Optional<ClassMethods>>, Consumer<ClassMethods> { | ||
|
||
private Map<String, ClassMethods> classLookupMap = new HashMap<>(); | ||
|
||
@Override | ||
public Optional<ClassMethods> apply(String className) { | ||
return Optional.ofNullable(classLookupMap.get(className)); | ||
} | ||
|
||
public Stream<MethodSignature> provides() { | ||
|
||
return classLookupMap.values().stream().distinct().flatMap(cm -> cm.provides()); | ||
} | ||
|
||
public List<MethodSignature> get(String className) { | ||
return apply(className).stream().flatMap(cm -> cm.provides()).toList(); | ||
} | ||
|
||
public Function<String, Optional<ClassMethods>> chain(Function<String, Optional<ClassMethods>> chained) { | ||
return cls -> { | ||
return apply(cls).or(() -> chained.apply(cls)); | ||
}; | ||
} | ||
|
||
@Override | ||
public void accept(ClassMethods methods) { | ||
methods.definitions().forEach(def -> { | ||
classLookupMap.put(def.name(), methods); | ||
}); | ||
} | ||
|
||
} |
17 changes: 17 additions & 0 deletions
17
tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassDef.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,17 @@ | ||
/******************************************************************************* | ||
* 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.analyze; | ||
|
||
record ClassDef(int access, String name, String signature, String superName, String[] interfaces) { | ||
|
||
} |
17 changes: 17 additions & 0 deletions
17
...aseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethodSignature.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,17 @@ | ||
/******************************************************************************* | ||
* 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.analyze; | ||
|
||
record ClassMethodSignature(ClassDef clazz, int access, String name, String descriptor, String signature) { | ||
|
||
} |
102 changes: 102 additions & 0 deletions
102
tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethods.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,102 @@ | ||
/******************************************************************************* | ||
* 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.analyze; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.function.Function; | ||
import java.util.stream.Stream; | ||
|
||
import org.objectweb.asm.ClassReader; | ||
import org.objectweb.asm.ClassVisitor; | ||
import org.objectweb.asm.MethodVisitor; | ||
import org.objectweb.asm.Opcodes; | ||
|
||
/** | ||
* Analyze a class about methods it possibly provides to callers | ||
*/ | ||
public class ClassMethods { | ||
|
||
private List<ClassDef> classDefs = new ArrayList<>(); | ||
private List<ClassMethodSignature> signatures = new ArrayList<>(); | ||
private Function<String, Optional<ClassMethods>> supplier; | ||
private Set<MethodSignature> collect; | ||
|
||
public ClassMethods(byte[] classbytes, Function<String, Optional<ClassMethods>> supplier) { | ||
this.supplier = supplier; | ||
ClassReader reader = new ClassReader(classbytes); | ||
reader.accept(new ClassVisitor(DependencyAnalyzer.ASM_API) { | ||
|
||
private ClassDef classDef; | ||
|
||
@Override | ||
public void visit(int version, int access, String name, String signature, String superName, | ||
String[] interfaces) { | ||
if ((access & Opcodes.ACC_PRIVATE) != 0) { | ||
// private methods can not be called | ||
return; | ||
} | ||
classDef = new ClassDef(access, name, signature, superName, interfaces); | ||
classDefs.add(classDef); | ||
} | ||
|
||
@Override | ||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, | ||
String[] exceptions) { | ||
signatures.add(new ClassMethodSignature(classDef, access, name, descriptor, signature)); | ||
return null; | ||
} | ||
}, ClassReader.SKIP_FRAMES); | ||
} | ||
|
||
Stream<ClassDef> definitions() { | ||
return classDefs.stream(); | ||
} | ||
|
||
/** | ||
* @return a stream of all method signatures this class can provide | ||
*/ | ||
Stream<MethodSignature> provides() { | ||
// all methods declared by our class are provided | ||
Stream<MethodSignature> declared = signatures.stream().map(cms -> { | ||
return new MethodSignature(cms.clazz().name(), cms.name(), cms.descriptor()); | ||
}); | ||
// and from the super class, transformed with our class as the classname | ||
Stream<MethodSignature> supermethods = classDefs.stream().flatMap(cd -> { | ||
return Optional.ofNullable(cd.superName()).flatMap(cn -> findRef(cn)).stream().flatMap(cm -> cm.provides()) | ||
// constructors are not inheritable | ||
.filter(ms -> !ms.isContructor()).map(ms -> inherit(cd, ms)); | ||
}); | ||
// and possibly from interfaces | ||
Stream<MethodSignature> interfaces = classDefs.stream().flatMap(cd -> { | ||
return Optional.ofNullable(cd.interfaces()).stream().flatMap(Arrays::stream) | ||
.flatMap(cn -> findRef(cn).stream()).flatMap(cm -> cm.provides()) | ||
// constructors are not inheritable | ||
.filter(ms -> !ms.isContructor()).map(ms -> inherit(cd, ms)); | ||
}); | ||
Stream<MethodSignature> inherited = Stream.concat(supermethods, interfaces); | ||
return Stream.concat(declared, inherited).distinct(); | ||
} | ||
|
||
private MethodSignature inherit(ClassDef cd, MethodSignature ms) { | ||
return new MethodSignature(cd.name(), ms.methodName(), ms.signature()); | ||
} | ||
|
||
private Optional<ClassMethods> findRef(String cn) { | ||
return supplier.apply(cn); | ||
} | ||
|
||
} |
76 changes: 76 additions & 0 deletions
76
tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassUsage.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,76 @@ | ||
/******************************************************************************* | ||
* 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.analyze; | ||
|
||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.TreeSet; | ||
import java.util.stream.Stream; | ||
|
||
import org.objectweb.asm.ClassReader; | ||
import org.objectweb.asm.ClassVisitor; | ||
import org.objectweb.asm.MethodVisitor; | ||
import org.objectweb.asm.Opcodes; | ||
|
||
/** | ||
* Analyze code that is used by the class itself | ||
*/ | ||
public class ClassUsage { | ||
|
||
private Set<MethodSignature> usedMethodSignatures = new HashSet<>(); | ||
private Map<MethodSignature, Collection<String>> classRef = new HashMap<>(); | ||
|
||
public ClassUsage(byte[] classbytes, JrtClasses jrt) { | ||
ClassReader reader = new ClassReader(classbytes); | ||
reader.accept(new ClassVisitor(Opcodes.ASM9) { | ||
|
||
private String className; | ||
|
||
@Override | ||
public void visit(int version, int access, String name, String signature, String superName, | ||
String[] interfaces) { | ||
this.className = name.replace('/', '.'); | ||
} | ||
|
||
@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 (jrt.apply(owner).isPresent()) { | ||
// ignore references to java provided classes | ||
return; | ||
} | ||
MethodSignature sig = new MethodSignature(owner, name, descriptor); | ||
classRef.computeIfAbsent(sig, nil -> new TreeSet<>()).add(className); | ||
usedMethodSignatures.add(sig); | ||
} | ||
}; | ||
} | ||
}, ClassReader.SKIP_FRAMES); | ||
} | ||
|
||
public Stream<MethodSignature> signatures() { | ||
return usedMethodSignatures.stream(); | ||
} | ||
|
||
public Collection<String> classRef(MethodSignature mthd) { | ||
return classRef.getOrDefault(mthd, List.of()); | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
...-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/DependencyAnalyzer.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,105 @@ | ||
/******************************************************************************* | ||
* 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.analyze; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.util.ArrayList; | ||
import java.util.Enumeration; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.function.Consumer; | ||
import java.util.function.Function; | ||
import java.util.jar.JarEntry; | ||
import java.util.jar.JarFile; | ||
|
||
import org.apache.maven.plugin.MojoFailureException; | ||
import org.eclipse.tycho.ArtifactDescriptor; | ||
import org.eclipse.tycho.ArtifactType; | ||
import org.eclipse.tycho.DependencyArtifacts; | ||
import org.objectweb.asm.Opcodes; | ||
|
||
public class DependencyAnalyzer { | ||
|
||
static final String CLASS_SUFFIX = ".class"; | ||
static final int ASM_API = Opcodes.ASM9; | ||
|
||
public static String getPackageName(String className) { | ||
className = className.replace('/', '.'); | ||
int idx = className.lastIndexOf('.'); | ||
if (idx > 0) { | ||
return className.substring(0, idx); | ||
} | ||
return className; | ||
} | ||
|
||
public static Function<String, Optional<ClassMethods>> createDependencyClassResolver(JrtClasses jrtClassResolver, | ||
DependencyArtifacts artifacts) throws MojoFailureException { | ||
ClassCollection allClassMethods = new ClassCollection(); | ||
Function<String, Optional<ClassMethods>> function = allClassMethods.chain(jrtClassResolver); | ||
List<ArtifactDescriptor> list = artifacts.getArtifacts(ArtifactType.TYPE_ECLIPSE_PLUGIN); | ||
for (ArtifactDescriptor descriptor : list) { | ||
File file = descriptor.fetchArtifact().join(); | ||
analyzeProvides(file, function, allClassMethods); | ||
} | ||
return function; | ||
} | ||
|
||
public static List<ClassUsage> analyzeUsage(File file, JrtClasses jre) throws MojoFailureException { | ||
List<ClassUsage> usages = new ArrayList<>(); | ||
try { | ||
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); | ||
} | ||
} | ||
|
||
public static ClassCollection analyzeProvides(File file, Function<String, Optional<ClassMethods>> classResolver, | ||
Consumer<ClassMethods> consumer) throws MojoFailureException { | ||
try { | ||
ClassCollection local = new ClassCollection(); | ||
Function<String, Optional<ClassMethods>> resolver = local.chain(classResolver); | ||
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); | ||
ClassMethods methods = new ClassMethods(stream.readAllBytes(), resolver); | ||
if (consumer != null) { | ||
consumer.accept(methods); | ||
} | ||
local.accept(methods); | ||
} | ||
} | ||
} | ||
return local; | ||
} catch (IOException e) { | ||
throw new MojoFailureException(e); | ||
} | ||
} | ||
|
||
} |
98 changes: 98 additions & 0 deletions
98
tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/JrtClasses.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,98 @@ | ||
/******************************************************************************* | ||
* 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.analyze; | ||
|
||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.nio.file.FileSystem; | ||
import java.nio.file.FileSystems; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.Iterator; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.function.Function; | ||
import java.util.stream.Stream; | ||
|
||
|
||
/** | ||
* Lookup of all classes provided by the java runtime | ||
*/ | ||
public class JrtClasses implements Function<String, Optional<ClassMethods>> { | ||
|
||
private Path rootPath; | ||
private Map<String, Optional<ClassMethods>> cache = new ConcurrentHashMap<>(); | ||
|
||
public JrtClasses(String javaHome) { | ||
try { | ||
Map<String, String> map; | ||
if (javaHome != null) { | ||
map = Map.of("java.home", javaHome); | ||
} else { | ||
map = Map.of(); | ||
} | ||
FileSystem fs = FileSystems.newFileSystem(URI.create("jrt:/"), map); | ||
rootPath = fs.getPath("/packages"); | ||
} catch (Exception e) { | ||
e.printStackTrace(); | ||
} | ||
} | ||
|
||
@Override | ||
public Optional<ClassMethods> apply(String className) { | ||
if (rootPath == null) { | ||
return Optional.empty(); | ||
} | ||
return cache.computeIfAbsent(className.replace('.', '/'), path -> lookupJreClass(className)); | ||
} | ||
|
||
private Optional<ClassMethods> lookupJreClass(String classPath) { | ||
// Paths in the "jrt:/" NIO filesystem are of this form: | ||
// | ||
// /modules/$MODULE/$PATH | ||
// /packages/$PACKAGE/$MODULE | ||
// | ||
// where $PACKAGE is a package name (e.g., "java.lang"). A path of the | ||
// second form names a symbolic link which, in turn, points to the | ||
// directory under /modules that contains a module that defines that | ||
// package. Example: | ||
// | ||
// /packages/java.lang/java.base -> /modules/java.base | ||
// | ||
// To find java/sql/Array.class without knowing its module you look up | ||
// /packages/java.sql, which is a directory, and enumerate its entries. | ||
// In this case there will be just one entry, a symbolic link named | ||
// "java.sql", which will point to /modules/java.sql, which will contain | ||
// java/sql/Array.class. | ||
// | ||
String packageName = DependencyAnalyzer.getPackageName(classPath); | ||
Path modulesPath = rootPath.resolve(packageName); | ||
if (Files.isDirectory(modulesPath)) { | ||
try (Stream<Path> module = Files.list(modulesPath)) { | ||
Iterator<Path> iterator = module.iterator(); | ||
while (iterator.hasNext()) { | ||
Path modulePath = iterator.next(); | ||
Path classFile = modulePath.resolve(classPath + ".class"); | ||
if (Files.isRegularFile(classFile)) { | ||
return Optional.of(new ClassMethods(Files.readAllBytes(classFile), JrtClasses.this)); | ||
} | ||
} | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
} | ||
} | ||
return Optional.empty(); | ||
} | ||
|
||
} |
35 changes: 35 additions & 0 deletions
35
tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/MethodSignature.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,35 @@ | ||
/******************************************************************************* | ||
* 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.analyze; | ||
|
||
public record MethodSignature(String className, String methodName, String signature) | ||
implements Comparable<MethodSignature> { | ||
|
||
public String packageName() { | ||
String cn = className(); | ||
return DependencyAnalyzer.getPackageName(cn); | ||
} | ||
|
||
public String id() { | ||
return className() + "#" + methodName() + signature(); | ||
} | ||
|
||
@Override | ||
public int compareTo(MethodSignature o) { | ||
return id().compareTo(o.id()); | ||
} | ||
|
||
public boolean isContructor() { | ||
return "<clinit>".equals(methodName) || "<init>".equals(methodName); | ||
} | ||
} |
192 changes: 192 additions & 0 deletions
192
...rc/main/java/org/eclipse/tycho/baseline/provider/EclipseIndexArtifactVersionProvider.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,192 @@ | ||
/******************************************************************************* | ||
* 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.provider; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.io.OutputStream; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.ArrayList; | ||
import java.util.Comparator; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
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.codehaus.plexus.logging.Logger; | ||
import org.eclipse.equinox.p2.metadata.IInstallableUnit; | ||
import org.eclipse.equinox.p2.metadata.Version; | ||
import org.eclipse.equinox.p2.query.QueryUtil; | ||
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository; | ||
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.core.resolver.target.ArtifactMatcher; | ||
import org.eclipse.tycho.p2maven.repository.P2RepositoryManager; | ||
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; | ||
private P2RepositoryManager repositoryManager; | ||
private Logger logger; | ||
|
||
@Inject | ||
public EclipseIndexArtifactVersionProvider(TransportCacheConfig cacheConfig, P2RepositoryManager repositoryManager, | ||
Logger logger) { | ||
this.repositoryManager = repositoryManager; | ||
this.logger = logger; | ||
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); | ||
Map<Version, List<Repository>> found = new HashMap<>(); | ||
map.entrySet().forEach(entry -> { | ||
entry.getValue().stream().filter(v -> v.isOSGiCompatible()).forEach(v -> { | ||
found.computeIfAbsent(v, x -> new ArrayList<>()).add(entry.getKey()); | ||
}); | ||
}); | ||
String id = unit.getId(); | ||
return found.entrySet().stream().map(entry -> { | ||
return new EclipseIndexArtifactVersion(entry.getValue(), id, packageName, entry.getKey(), logger); | ||
}).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 String packageName; | ||
private Path tempFile; | ||
private String unitId; | ||
private List<Repository> repositories; | ||
private org.osgi.framework.Version osgiVersion; | ||
private Optional<IInstallableUnit> unit; | ||
private Repository unitRepo; | ||
|
||
public EclipseIndexArtifactVersion(List<Repository> repositories, String unitId, String packageName, | ||
Version version, Logger logger) { | ||
osgiVersion = org.osgi.framework.Version.parseVersion(version.getOriginal()); | ||
this.repositories = repositories; | ||
|
||
this.unitId = unitId; | ||
this.packageName = packageName; | ||
this.version = version; | ||
} | ||
|
||
@Override | ||
public Path getArtifact() { | ||
if (tempFile == null) { | ||
IInstallableUnit unit = getUnit().orElse(null); | ||
if (unit != null) { | ||
Path file; | ||
try { | ||
file = Files.createTempFile(unit.getId(), ".jar"); | ||
} catch (IOException e) { | ||
return null; | ||
} | ||
file.toFile().deleteOnExit(); | ||
List<Repository> list = new ArrayList<>(repositories); | ||
if (unitRepo != null) { | ||
list.remove(unitRepo); | ||
list.add(0, unitRepo); | ||
} | ||
for (Repository repository : list) { | ||
try { | ||
org.apache.maven.model.Repository r = new org.apache.maven.model.Repository(); | ||
r.setUrl(repository.getLocation().toString()); | ||
try (OutputStream stream = Files.newOutputStream(file)) { | ||
repositoryManager.downloadArtifact(unit, repositoryManager.getArtifactRepository(r), | ||
stream); | ||
return tempFile = file; | ||
} | ||
} catch (Exception e) { | ||
logger.error("Fetch artifact for unit " + unit.getId() + " from " + repository.getLocation() | ||
+ " failed: " + e); | ||
} | ||
} | ||
file.toFile().delete(); | ||
} | ||
} | ||
return tempFile; | ||
} | ||
|
||
private Optional<IInstallableUnit> getUnit() { | ||
if (unit == null) { | ||
for (Repository repository : repositories) { | ||
try { | ||
org.apache.maven.model.Repository r = new org.apache.maven.model.Repository(); | ||
r.setUrl(repository.getLocation().toString()); | ||
IMetadataRepository metadataRepository = repositoryManager.getMetadataRepository(r); | ||
Optional<IInstallableUnit> optional = ArtifactMatcher.findPackage(packageName, | ||
metadataRepository.query(QueryUtil.createIUQuery(unitId), null), version); | ||
if (optional.isPresent()) { | ||
this.unitRepo = repository; | ||
return unit = optional; | ||
} else { | ||
// if we have a package exported from many bundles it might be the case that the | ||
// actual unit we look for is not found, so we need to try on | ||
logger.debug( | ||
"Package " + packageName + " not found in metadata of " + repository.getLocation()); | ||
} | ||
} catch (Exception e) { | ||
logger.error("Fetch metadata from " + repository.getLocation() + ":: " + e); | ||
} | ||
} | ||
unit = Optional.empty(); | ||
} | ||
return Objects.requireNonNullElse(unit, Optional.empty()); | ||
} | ||
|
||
@Override | ||
public org.osgi.framework.Version getVersion() { | ||
return osgiVersion; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
if (unit != null && unit.isPresent()) { | ||
return getVersion() + " (" + unit.get() + ")"; | ||
} | ||
return getVersion().toString(); | ||
} | ||
|
||
@Override | ||
public String getProvider() { | ||
return getUnit().map(unit -> unit.getId() + " " + unit.getVersion()).orElse(null); | ||
} | ||
|
||
} | ||
|
||
} |
177 changes: 177 additions & 0 deletions
177
...lugin/src/main/java/org/eclipse/tycho/baseline/provider/MavenArtifactVersionProvider.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,177 @@ | ||
/******************************************************************************* | ||
* 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.provider; | ||
|
||
import java.nio.file.Path; | ||
import java.util.Comparator; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Stream; | ||
|
||
import javax.inject.Inject; | ||
import javax.inject.Named; | ||
|
||
import org.apache.maven.RepositoryUtils; | ||
import org.apache.maven.SessionScoped; | ||
import org.apache.maven.execution.MavenSession; | ||
import org.apache.maven.project.MavenProject; | ||
import org.eclipse.aether.RepositorySystem; | ||
import org.eclipse.aether.artifact.Artifact; | ||
import org.eclipse.aether.artifact.DefaultArtifact; | ||
import org.eclipse.aether.repository.RemoteRepository; | ||
import org.eclipse.aether.resolution.ArtifactRequest; | ||
import org.eclipse.aether.resolution.ArtifactResolutionException; | ||
import org.eclipse.aether.resolution.ArtifactResult; | ||
import org.eclipse.aether.resolution.VersionRangeRequest; | ||
import org.eclipse.aether.resolution.VersionRangeResolutionException; | ||
import org.eclipse.aether.resolution.VersionRangeResult; | ||
import org.eclipse.aether.version.Version; | ||
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.tycho.TychoConstants; | ||
import org.eclipse.tycho.artifacts.ArtifactVersion; | ||
import org.eclipse.tycho.artifacts.ArtifactVersionProvider; | ||
import org.eclipse.tycho.core.osgitools.BundleReader; | ||
import org.eclipse.tycho.core.osgitools.OsgiManifest; | ||
import org.osgi.framework.BundleException; | ||
import org.osgi.framework.VersionRange; | ||
import org.osgi.framework.namespace.PackageNamespace; | ||
|
||
/** | ||
* A {@link ArtifactVersionProvider} that checks maven repository for possible | ||
* candidates | ||
*/ | ||
@Named | ||
@SessionScoped | ||
public class MavenArtifactVersionProvider implements ArtifactVersionProvider { | ||
|
||
private MavenSession session; | ||
private RepositorySystem repoSystem; | ||
private BundleReader bundleReader; | ||
|
||
@Inject | ||
public MavenArtifactVersionProvider(MavenSession session, RepositorySystem repoSystem, BundleReader bundleReader) { | ||
this.session = session; | ||
this.repoSystem = repoSystem; | ||
this.bundleReader = bundleReader; | ||
} | ||
|
||
@Override | ||
public Stream<ArtifactVersion> getPackageVersions(IInstallableUnit unit, String packageName, | ||
VersionRange versionRange, MavenProject mavenProject) { | ||
String groupId = unit.getProperty(TychoConstants.PROP_GROUP_ID); | ||
String artifactId = unit.getProperty(TychoConstants.PROP_ARTIFACT_ID); | ||
String classifier = unit.getProperty(TychoConstants.PROP_CLASSIFIER); | ||
if (groupId != null && artifactId != null && !"sources".equals(classifier)) { | ||
List<RemoteRepository> repositories = RepositoryUtils.toRepos(mavenProject.getRemoteArtifactRepositories()); | ||
DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId, classifier, "jar", "[0,)"); | ||
// as we have no mean for a package version in maven we can only fetch all | ||
// versions an check if the match | ||
VersionRangeRequest rangeRequest = new VersionRangeRequest(artifact, repositories, ""); | ||
try { | ||
VersionRangeResult range = repoSystem.resolveVersionRange(session.getRepositorySession(), rangeRequest); | ||
// now we sort from highest > lowest version | ||
List<Version> versions = range.getVersions().stream() | ||
.sorted(Comparator.<Version>naturalOrder().reversed()).toList(); | ||
return versions.stream() | ||
.map(v -> new MavenPackageArtifactVersion(artifact, v, packageName, repositories)) | ||
.filter(mav -> mav.getVersion() != null) | ||
// and drop all until we find a matching version | ||
.dropWhile(mav -> !versionRange.includes(mav.getVersion())) | ||
// and stop when we find the first non matching version | ||
.takeWhile(mav -> versionRange.includes(mav.getVersion())) | ||
// cast to make compiler happy | ||
.map(ArtifactVersion.class::cast); | ||
} catch (VersionRangeResolutionException e) { | ||
// can't provide any useful data then... | ||
} | ||
} | ||
return Stream.empty(); | ||
} | ||
|
||
private class MavenPackageArtifactVersion implements ArtifactVersion { | ||
|
||
private Artifact artifact; | ||
private List<RemoteRepository> repositories; | ||
private Path path; | ||
private String packageName; | ||
private org.osgi.framework.Version packageVersion; | ||
|
||
public MavenPackageArtifactVersion(Artifact artifact, Version version, String packageName, | ||
List<RemoteRepository> repositories) { | ||
this.artifact = new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(), | ||
artifact.getExtension(), version.toString()); | ||
this.packageName = packageName; | ||
this.repositories = repositories; | ||
} | ||
|
||
@Override | ||
public Path getArtifact() { | ||
try { | ||
ArtifactRequest request = new ArtifactRequest(artifact, repositories, ""); | ||
ArtifactResult result = repoSystem.resolveArtifact(session.getRepositorySession(), request); | ||
path = result.getArtifact().getFile().toPath(); | ||
} catch (ArtifactResolutionException e) { | ||
} | ||
return path; | ||
} | ||
|
||
@Override | ||
public org.osgi.framework.Version getVersion() { | ||
if (packageVersion == null) { | ||
ModuleRevisionBuilder builder = readOSGiInfo(getArtifact()); | ||
if (builder != null) { | ||
List<GenericInfo> capabilities = builder.getCapabilities(PackageNamespace.PACKAGE_NAMESPACE); | ||
for (GenericInfo info : capabilities) { | ||
Map<String, Object> attributes = info.getAttributes(); | ||
if (packageName.equals(attributes.get(PackageNamespace.PACKAGE_NAMESPACE))) { | ||
packageVersion = (org.osgi.framework.Version) attributes.getOrDefault( | ||
PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, | ||
org.osgi.framework.Version.emptyVersion); | ||
} | ||
} | ||
} | ||
} | ||
return packageVersion; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return getVersion() + " (maven artifact " + artifact + ")"; | ||
} | ||
|
||
@Override | ||
public String getProvider() { | ||
ModuleRevisionBuilder info = readOSGiInfo(getArtifact()); | ||
if (info != null) { | ||
return info.getSymbolicName() + " " + info.getVersion(); | ||
} | ||
return null; | ||
} | ||
|
||
} | ||
|
||
private ModuleRevisionBuilder readOSGiInfo(Path path) { | ||
if (path != null) { | ||
OsgiManifest manifest = bundleReader.loadManifest(path.toFile()); | ||
try { | ||
return OSGiManifestBuilderFactory.createBuilder(manifest.getHeaders()); | ||
} catch (BundleException e) { | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
} |
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
1,073 changes: 517 additions & 556 deletions
1,073
tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java
Large diffs are not rendered by default.
Oops, something went wrong.
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
26 changes: 26 additions & 0 deletions
26
tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.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,26 @@ | ||
/******************************************************************************* | ||
* 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.artifacts; | ||
|
||
import java.nio.file.Path; | ||
|
||
import org.osgi.framework.Version; | ||
|
||
public interface ArtifactVersion { | ||
|
||
Path getArtifact(); | ||
|
||
Version getVersion(); | ||
|
||
String getProvider(); | ||
} |
26 changes: 26 additions & 0 deletions
26
tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.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,26 @@ | ||
/******************************************************************************* | ||
* 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.artifacts; | ||
|
||
import java.util.stream.Stream; | ||
|
||
import org.apache.maven.project.MavenProject; | ||
import org.eclipse.equinox.p2.metadata.IInstallableUnit; | ||
import org.osgi.framework.VersionRange; | ||
|
||
public interface ArtifactVersionProvider { | ||
|
||
Stream<ArtifactVersion> getPackageVersions(IInstallableUnit unit, String packageName, VersionRange versionRange, | ||
MavenProject mavenProject); | ||
|
||
} |