diff --git a/tycho-baseline-plugin/pom.xml b/tycho-baseline-plugin/pom.xml index 5acee8e8ba..f76e211228 100644 --- a/tycho-baseline-plugin/pom.xml +++ b/tycho-baseline-plugin/pom.xml @@ -53,6 +53,16 @@ asciitable 0.3.2 + + org.ow2.asm + asm + 9.7.1 + + + org.eclipse.emf + org.eclipse.emf.ecore + 2.38.0 + @@ -60,6 +70,10 @@ org.codehaus.plexus plexus-component-metadata + + org.eclipse.sisu + sisu-maven-plugin + org.apache.maven.plugins maven-plugin-plugin diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java new file mode 100644 index 0000000000..388e485224 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java @@ -0,0 +1,377 @@ +/******************************************************************************* + * 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.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +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.Consumer; +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.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.ArtifactDescriptor; +import org.eclipse.tycho.ArtifactType; +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.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.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: + *
    + *
  1. The current project artifact is analyzed for method signatures it + * calls
  2. + *
  3. Then it is checked what of these match to a given dependency
  4. + *
  5. All dependency versions matching the range are fetched and inspected + * using {@link ArtifactVersionProvider}s
  6. + *
  7. Then it checks if there are any missing signatures or inconsistencies and + * possibly failing the build
  8. + *
+ */ +@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 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 usages = analyzeUsage(file, jrtClassResolver); + if (usages.isEmpty()) { + return; + } + Collection units = artifacts.getInstallableUnits(); + ModuleRevisionBuilder builder = readOSGiInfo(file); + List requirements = builder.getRequirements(); + List dependencyProblems = new ArrayList<>(); + Map analyzeCache = new HashMap<>(); + Log log = getLog(); + Map lowestPackageVersion = new HashMap<>(); + Set packageWithError = new HashSet<>(); + Function> classResolver = createDependencyClassResolver(jrtClassResolver, + artifacts); + for (GenericInfo genericInfo : requirements) { + if (PackageNamespace.PACKAGE_NAMESPACE.equals(genericInfo.getNamespace())) { + Map 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 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(); + ArtifactMatcher.getPackageVersion(unit, packageName).filter(v -> v.isOSGiCompatible()) + .ifPresent(v -> lowestPackageVersion.put(packageName, new Version(v.toString()))); + VersionRange versionRange = VersionRange.valueOf(packageVersion); + List 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 packageMethods = new TreeSet<>(); + Map> 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 + Set checked = new HashSet<>(); + for (ArtifactVersion v : list) { + Version version = v.getVersion(); + if (version == null || !checked.add(version)) { + 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 = analyzeProvides(artifact.toFile(), classResolver, null); + analyzeCache.put(artifact, collection); + } + boolean ok = true; + Set set = collection.provides().collect(Collectors.toSet()); + for (MethodSignature mthd : packageMethods) { + if (!set.contains(mthd)) { + List 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) 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()), 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 results = new ArrayList<>(); + for (DependencyVersionProblem problem : dependencyProblems) { + Collection 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 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 + " : " + lowestPackageVersion.get(pkg); + log.info(suggestion); + results.add(suggestion); + } + if (results.isEmpty()) { + return; + } + Path path = project.getBasedir().toPath().resolve("versionProblems.txt"); + try { + Files.writeString(path, results.stream().collect(Collectors.joining(System.lineSeparator()))); + } catch (IOException e) { + } + } + + private Function> createDependencyClassResolver(JrtClasses jrtClassResolver, + DependencyArtifacts artifacts) throws MojoFailureException { + ClassCollection allClassMethods = new ClassCollection(); + Function> function = allClassMethods.chain(jrtClassResolver); + List list = artifacts.getArtifacts(ArtifactType.TYPE_ECLIPSE_PLUGIN); + for (ArtifactDescriptor descriptor : list) { + File file = descriptor.fetchArtifact().join(); + analyzeProvides(file, function, allClassMethods); + } + return function; + } + + private Map getVersionInfo(GenericInfo genericInfo, String versionAttribute) { + Map 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 analyzeUsage(File file, JrtClasses jre) throws MojoFailureException { + List usages = new ArrayList<>(); + try { + try (JarFile jar = new JarFile(file)) { + Enumeration 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 ClassCollection analyzeProvides(File file, Function> classResolver, + Consumer consumer) + throws MojoFailureException { + try { + ClassCollection local = new ClassCollection(); + Function> resolver = local.chain(classResolver); + try (JarFile jar = new JarFile(file)) { + Enumeration 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); + } + } + + 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 references, + List provided) { + + } +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassCollection.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassCollection.java new file mode 100644 index 0000000000..2acf030db4 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassCollection.java @@ -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>, Consumer { + + private Map classLookupMap = new HashMap<>(); + + @Override + public Optional apply(String className) { + return Optional.ofNullable(classLookupMap.get(className)); + } + + public Stream provides() { + + return classLookupMap.values().stream().distinct().flatMap(cm -> cm.provides()); + } + + public List get(String className) { + return apply(className).stream().flatMap(cm -> cm.provides()).toList(); + } + + public Function> chain(Function> 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); + }); + } + +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassDef.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassDef.java new file mode 100644 index 0000000000..1b9d9359de --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassDef.java @@ -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) { + +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethodSignature.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethodSignature.java new file mode 100644 index 0000000000..850965370b --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethodSignature.java @@ -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) { + +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethods.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethods.java new file mode 100644 index 0000000000..01d097dc28 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethods.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * 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 classDefs = new ArrayList<>(); + private List signatures = new ArrayList<>(); + private Function> supplier; + private Set collect; + + public ClassMethods(byte[] classbytes, Function> 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 definitions() { + return classDefs.stream(); + } + + /** + * @return a stream of all method signatures this class can provide + */ + Stream provides() { + // all methods declared by our class are provided + Stream 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 supermethods = classDefs.stream().flatMap(cd -> { + return Optional.ofNullable(cd.superName()).flatMap(cn -> findRef(cn)).stream().flatMap(cm -> cm.provides()) + .map(ms -> new MethodSignature(cd.name(), ms.methodName(), ms.signature())); + }); + // and possibly from interfaces + Stream interfaces = classDefs.stream().flatMap(cd -> { + return Optional.ofNullable(cd.interfaces()).stream().flatMap(Arrays::stream) + .flatMap(cn -> findRef(cn).stream()).flatMap(cm -> cm.provides()) + .map(ms -> new MethodSignature(cd.name(), ms.methodName(), ms.signature())); + }); + Stream inherited = Stream.concat(supermethods, interfaces); + return Stream.concat(declared, inherited).distinct(); + } + + private Optional findRef(String cn) { + Optional ref = supplier.apply(cn); + if (ref.isEmpty()) { + System.out.println("Referenced class " + cn + " was not found by the supplier!"); + } + return ref; + } + +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassUsage.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassUsage.java new file mode 100644 index 0000000000..bc42aba0e0 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassUsage.java @@ -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 usedMethodSignatures = new HashSet<>(); + private Map> 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 signatures() { + return usedMethodSignatures.stream(); + } + + public Collection classRef(MethodSignature mthd) { + return classRef.getOrDefault(mthd, List.of()); + } +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/DependencyAnalyzer.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/DependencyAnalyzer.java new file mode 100644 index 0000000000..0933881c19 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/DependencyAnalyzer.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * 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 org.objectweb.asm.Opcodes; + +public class DependencyAnalyzer { + + 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; + } +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/JrtClasses.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/JrtClasses.java new file mode 100644 index 0000000000..6626724893 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/JrtClasses.java @@ -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> { + + private Path rootPath; + private Map> cache = new ConcurrentHashMap<>(); + + public JrtClasses(String javaHome) { + try { + Map 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 apply(String className) { + if (rootPath == null) { + return Optional.empty(); + } + return cache.computeIfAbsent(className.replace('.', '/'), path -> lookupJreClass(className)); + } + + private Optional 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 module = Files.list(modulesPath)) { + Iterator 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(); + } + +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/MethodSignature.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/MethodSignature.java new file mode 100644 index 0000000000..5bbaa9973b --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/MethodSignature.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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 { + + 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()); + } +} \ No newline at end of file diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/EclipseIndexArtifactVersionProvider.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/EclipseIndexArtifactVersionProvider.java new file mode 100644 index 0000000000..2640062396 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/EclipseIndexArtifactVersionProvider.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * 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.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.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; + + @Inject + public EclipseIndexArtifactVersionProvider(TransportCacheConfig cacheConfig, + P2RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + p2Index = new P2IndexImpl(new File(cacheConfig.getCacheLocation(), "index")); + } + + @Override + public Stream getPackageVersions(IInstallableUnit unit, String packageName, + VersionRange versionRange, MavenProject mavenProject) { + Map> map = p2Index.lookupCapabilities(PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE, + packageName); + Map> 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()); + }).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 org.apache.maven.model.Repository repository; + private String packageName; + private Path tempFile; + private String unitId; + private List repositories; + private org.osgi.framework.Version osgiVersion; + private Optional unit; + + public EclipseIndexArtifactVersion(List repositories, String unitId, String packageName, + Version version) { + 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(); + for (Repository repository : repositories) { + org.apache.maven.model.Repository r = new org.apache.maven.model.Repository(); + r.setUrl(repository.getLocation().toString()); + try { + try (OutputStream stream = Files.newOutputStream(file)) { + repositoryManager.downloadArtifact(unit, repositoryManager.getArtifactRepository(r), + stream); + return tempFile = file; + } + } catch (Exception e) { + } + } + file.toFile().delete(); + } + } + return tempFile; + } + + private Optional getUnit() { + if (unit == null) { + for (Repository repository : repositories) { + org.apache.maven.model.Repository r = new org.apache.maven.model.Repository(); + r.setUrl(repository.getLocation().toString()); + try { + IMetadataRepository metadataRepository = repositoryManager.getMetadataRepository(r); + return unit = ArtifactMatcher.findPackage(packageName, + metadataRepository.query(QueryUtil.createIUQuery(unitId), null), version); + } catch (Exception e) { + } + } + } + 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); + } + + } + +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/MavenArtifactVersionProvider.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/MavenArtifactVersionProvider.java new file mode 100644 index 0000000000..285382e105 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/MavenArtifactVersionProvider.java @@ -0,0 +1,177 @@ +/******************************************************************************* + * 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.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 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 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 versions = range.getVersions().stream() + .sorted(Comparator.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 repositories; + private Path path; + private String packageName; + private org.osgi.framework.Version packageVersion; + + public MavenPackageArtifactVersion(Artifact artifact, Version version, String packageName, + List 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 capabilities = builder.getCapabilities(PackageNamespace.PACKAGE_NAMESPACE); + for (GenericInfo info : capabilities) { + Map 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; + } + +} diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java index 03cc9203fa..7e1db8dbbb 100644 --- a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java @@ -8,15 +8,14 @@ * Contributors: * Eike Stepper - initial API and implementation */ -package org.eclipse.oomph.p2.internal.core; - -import org.eclipse.emf.common.util.URI; - -import org.eclipse.equinox.p2.metadata.Version; +package org.eclipse.tycho.copyfrom.oomph; import java.util.Map; import java.util.Set; +import org.eclipse.emf.common.util.URI; +import org.eclipse.equinox.p2.metadata.Version; + /** * @author Eike Stepper */ @@ -26,8 +25,6 @@ public interface P2Index public static final int COMPOSED_REPOSITORY = 1; - public static final P2Index INSTANCE = P2IndexImpl.INSTANCE; - public Repository[] getRepositories(); public Map> getCapabilities(); diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java index b7df4d3433..a728ea9032 100644 --- a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java @@ -8,22 +8,10 @@ * Contributors: * Eike Stepper - initial API and implementation */ -package org.eclipse.oomph.p2.internal.core; - -import org.eclipse.oomph.util.CollectionUtil; -import org.eclipse.oomph.util.IOUtil; -import org.eclipse.oomph.util.StringUtil; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl; -import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl.EObjectInputStream; - -import org.eclipse.core.runtime.IPath; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Path; -import org.eclipse.equinox.p2.metadata.Version; +package org.eclipse.tycho.copyfrom.oomph; import java.io.BufferedReader; +import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -38,555 +26,528 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl; +import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl.EObjectInputStream; +import org.eclipse.equinox.p2.metadata.Version; + /** * @author Eike Stepper */ -public class P2IndexImpl implements P2Index -{ - public static final P2IndexImpl INSTANCE = new P2IndexImpl(); - - private static final String INDEX_BASE = "https://download.eclipse.org/oomph/index/"; //$NON-NLS-1$ - - private long timeStamp; - - private Map repositories; - - private Repository[] repositoriesArray; - - private Map> capabilitiesMap; - - private File repositoriesCacheFile; - - private File capabilitiesCacheFile; - - private int capabilitiesRefreshHours = -1; - - private int repositoriesRefreshHours = -1; - - private P2IndexImpl() - { - } - - private synchronized void initCapabilities() - { - if (capabilitiesMap == null || capabilitiesCacheFile.lastModified() + capabilitiesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) - { - capabilitiesMap = new LinkedHashMap<>(); - - ZipFile zipFile = null; - InputStream inputStream = null; - - try - { - initCapabilitiesCacheFile(); - - zipFile = new ZipFile(capabilitiesCacheFile); - ZipEntry zipEntry = zipFile.getEntry("capabilities"); //$NON-NLS-1$ - - inputStream = zipFile.getInputStream(zipEntry); - - Map options = new HashMap<>(); - options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); - options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); - options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); - - EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); - capabilitiesRefreshHours = stream.readInt(); - - int mapSize = stream.readCompressedInt(); - for (int i = 0; i < mapSize; ++i) - { - String key = stream.readSegmentedString(); - int valuesSize = stream.readCompressedInt(); - for (int j = 0; j < valuesSize; ++j) - { - String value = stream.readSegmentedString(); - CollectionUtil.add(capabilitiesMap, key, value); - } - } - } - catch (Exception ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - finally - { - IOUtil.closeSilent(inputStream); - if (zipFile != null) - { - try - { - zipFile.close(); - } - catch (IOException ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - } - } - } - } - - private synchronized void initRepositories(boolean force) - { - if (repositories == null || force || repositoriesCacheFile.lastModified() + repositoriesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) - { - repositories = new HashMap<>(); - - ZipFile zipFile = null; - InputStream inputStream = null; - - try - { - initRepositoriesCacheFile(); - - zipFile = new ZipFile(repositoriesCacheFile); - ZipEntry zipEntry = zipFile.getEntry("repositories"); //$NON-NLS-1$ - - inputStream = zipFile.getInputStream(zipEntry); - - Map options = new HashMap<>(); - options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); - options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); - options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); - - EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); - - timeStamp = stream.readLong(); - repositoriesRefreshHours = stream.readInt(); - int repositoryCount = stream.readInt(); - - Map> composedRepositories = new HashMap<>(); - for (int id = 1; id <= repositoryCount; id++) - { - RepositoryImpl repository = new RepositoryImpl(stream, id, composedRepositories); - repositories.put(id, repository); - } - - for (Map.Entry> entry : composedRepositories.entrySet()) - { - RepositoryImpl repository = entry.getKey(); - for (int compositeID : entry.getValue()) - { - RepositoryImpl composite = repositories.get(compositeID); - if (composite != null) - { - composite.addChild(repository); - repository.addComposite(composite); - } - } - } - - try - { - int problematicRepositories = stream.readInt(); - for (int i = 0; i < problematicRepositories; i++) - { - int id = stream.readInt(); - int unresolvedChildren = stream.readInt(); - - RepositoryImpl repository = repositories.get(id); - repository.unresolvedChildren = unresolvedChildren; - } - } - catch (Exception ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - - repositoriesArray = repositories.values().toArray(new Repository[repositories.size()]); - } - catch (Exception ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - finally - { - IOUtil.close(inputStream); - if (zipFile != null) - { - try - { - zipFile.close(); - } - catch (IOException ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - } - } - } - } - - private boolean initRepositoriesCacheFile() throws Exception - { - if (repositoriesCacheFile == null) - { - IPath stateLocation = P2CorePlugin.INSTANCE.isOSGiRunning() ? P2CorePlugin.INSTANCE.getStateLocation() : new Path("."); //$NON-NLS-1$ - repositoriesCacheFile = new File(stateLocation.toOSString(), "repositories"); //$NON-NLS-1$ - } - - downloadIfModifiedSince(new URL(INDEX_BASE + "repositories"), repositoriesCacheFile); //$NON-NLS-1$ - - return true; - } - - private boolean initCapabilitiesCacheFile() throws Exception - { - if (capabilitiesCacheFile == null) - { - IPath stateLocation = P2CorePlugin.INSTANCE.isOSGiRunning() ? P2CorePlugin.INSTANCE.getStateLocation() : new Path("."); //$NON-NLS-1$ - capabilitiesCacheFile = new File(stateLocation.toOSString(), "capabilities"); //$NON-NLS-1$ - } - - downloadIfModifiedSince(new URL(INDEX_BASE + "capabilities"), capabilitiesCacheFile); //$NON-NLS-1$ - - return true; - } - - @Override - public Repository[] getRepositories() - { - initRepositories(false); - return repositoriesArray; - } - - @Override - public Map> getCapabilities() - { - initCapabilities(); - return Collections.unmodifiableMap(capabilitiesMap); - } - - @Override - public Map> lookupCapabilities(String namespace, String name) - { - Map> capabilities = new HashMap<>(); - if (!StringUtil.isEmpty(namespace) && !StringUtil.isEmpty(name)) - { - namespace = URI.encodeSegment(namespace, false); - name = URI.encodeSegment(name, false); - - BufferedReader reader = null; - - try - { - InputStream inputStream = new URL(INDEX_BASE + namespace + "/" + name).openStream(); //$NON-NLS-1$ - reader = new BufferedReader(new InputStreamReader(inputStream)); - - String line = reader.readLine(); - if (line == null) - { - return capabilities; - } - - long timeStamp = Long.parseLong(line); - initRepositories(timeStamp != this.timeStamp); - - while ((line = reader.readLine()) != null) - { - String[] tokens = line.split(","); //$NON-NLS-1$ - int repositoryID = Integer.parseInt(tokens[0]); - Repository repository = repositories.get(repositoryID); - if (repository != null) - { - Set versions = new HashSet<>(); - for (int i = 1; i < tokens.length; i++) - { - versions.add(Version.parseVersion(tokens[i])); - } - - capabilities.put(repository, versions); - } - } - } - catch (FileNotFoundException ex) - { - // Ignore. - } - catch (Exception ex) - { - P2CorePlugin.INSTANCE.log(ex, IStatus.WARNING); - } - finally - { - IOUtil.close(reader); - } - } - - return capabilities; - } - - @Override - public Map> generateCapabilitiesFromComposedRepositories(Map> capabilitiesFromSimpleRepositories) - { - Map> capabilities = new HashMap<>(); - for (Map.Entry> entry : capabilitiesFromSimpleRepositories.entrySet()) - { - Repository repository = entry.getKey(); - Set versions = entry.getValue(); - recurseComposedRepositories(capabilities, repository, versions); - } - - return capabilities; - } - - private void recurseComposedRepositories(Map> capabilities, Repository repository, Set versions) - { - for (Repository composite : repository.getComposites()) - { - Set set = capabilities.get(composite); - if (set == null) - { - set = new HashSet<>(); - capabilities.put(composite, set); - } - - set.addAll(versions); - recurseComposedRepositories(capabilities, composite, versions); - } - } - - private static void downloadIfModifiedSince(URL url, File file) throws IOException - { - long lastModified = -1L; - if (file.isFile()) - { - lastModified = file.lastModified(); - } - - InputStream inputStream = null; - OutputStream outputStream = null; - - try - { - HttpURLConnection connection = (HttpURLConnection)url.openConnection(); - if (lastModified != -1) - { - connection.setIfModifiedSince(lastModified); - } - - connection.connect(); - inputStream = connection.getInputStream(); - if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) - { - return; - } - - outputStream = new FileOutputStream(file); - IOUtil.copy(inputStream, outputStream); - outputStream.close(); - file.setLastModified(connection.getLastModified()); - } - finally - { - IOUtil.close(outputStream); - IOUtil.close(inputStream); - } - } - - /** - * @author Eike Stepper - */ - public static final class RepositoryImpl implements Repository - { - public static final int UNINITIALIZED = -1; - - private static final Repository[] NO_REPOSITORIES = {}; - - private final URI location; - - private final int id; - - private final boolean composed; - - private final boolean compressed; - - private final long timestamp; - - private int capabilityCount; - - private int unresolvedChildren; - - private Repository[] children; - - private Repository[] composites; - - public RepositoryImpl(EObjectInputStream stream, int id, Map> composedRepositories) throws IOException - { - this.id = id; - location = stream.readURI(); - composed = stream.readBoolean(); - compressed = stream.readBoolean(); - timestamp = stream.readLong(); - - if (composed) - { - capabilityCount = UNINITIALIZED; - } - else - { - capabilityCount = stream.readInt(); - } - - List composites = null; - while (stream.readBoolean()) - { - if (composites == null) - { - composites = new ArrayList<>(); - composedRepositories.put(this, composites); - } - - int composite = stream.readInt(); - composites.add(composite); - } - } - - @Override - public URI getLocation() - { - return location; - } - - @Override - public int getID() - { - return id; - } - - @Override - public boolean isComposed() - { - return composed; - } - - @Override - public boolean isCompressed() - { - return compressed; - } - - @Override - public long getTimestamp() - { - return timestamp; - } - - @Override - public int getCapabilityCount() - { - if (composed && capabilityCount == UNINITIALIZED) - { - capabilityCount = 0; - for (Repository child : getChildren()) - { - capabilityCount += child.getCapabilityCount(); - } - } - - return capabilityCount; - } - - @Override - public int getUnresolvedChildren() - { - return unresolvedChildren; - } - - @Override - public Repository[] getChildren() - { - if (children == null) - { - return NO_REPOSITORIES; - } - - return children; - } - - @Override - public Repository[] getComposites() - { - if (composites == null) - { - return NO_REPOSITORIES; - } - - return composites; - } - - @Override - public int hashCode() - { - final int prime = 31; - int result = 1; - result = prime * result + id; - return result; - } - - @Override - public boolean equals(Object obj) - { - if (this == obj) - { - return true; - } - - if (obj == null || getClass() != obj.getClass()) - { - return false; - } - - RepositoryImpl other = (RepositoryImpl)obj; - if (id != other.id) - { - return false; - } - - return true; - } - - @Override - public int compareTo(Repository o) - { - return location.toString().compareTo(o.getLocation().toString()); - } - - @Override - public String toString() - { - return location.toString(); - } - - public void addChild(Repository child) - { - children = addRepository(children, child); - } - - public void addComposite(Repository composite) - { - composites = addRepository(composites, composite); - } - - private Repository[] addRepository(Repository[] repositories, Repository repository) - { - if (repositories == null) - { - return new Repository[] { repository }; - } - - int length = repositories.length; - Repository[] newRepositories = new Repository[length + 1]; - System.arraycopy(repositories, 0, newRepositories, 0, length); - newRepositories[length] = repository; - return newRepositories; - } - } +public class P2IndexImpl implements P2Index { + + private static final String INDEX_BASE = "https://download.eclipse.org/oomph/index/"; //$NON-NLS-1$ + + private long timeStamp; + + private Map repositories; + + private Repository[] repositoriesArray; + + private Map> capabilitiesMap; + + private File repositoriesCacheFile; + + private File capabilitiesCacheFile; + + private int capabilitiesRefreshHours = -1; + + private int repositoriesRefreshHours = -1; + + private File basedir; + + public P2IndexImpl(File basedir) { + this.basedir = basedir; + basedir.mkdirs(); + } + + private synchronized void initCapabilities() { + if (capabilitiesMap == null || capabilitiesCacheFile.lastModified() + + capabilitiesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) { + capabilitiesMap = new LinkedHashMap<>(); + + ZipFile zipFile = null; + InputStream inputStream = null; + + try { + initCapabilitiesCacheFile(); + + zipFile = new ZipFile(capabilitiesCacheFile); + ZipEntry zipEntry = zipFile.getEntry("capabilities"); //$NON-NLS-1$ + + inputStream = zipFile.getInputStream(zipEntry); + + Map options = new HashMap<>(); + options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); + options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); + options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); + + EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); + capabilitiesRefreshHours = stream.readInt(); + + int mapSize = stream.readCompressedInt(); + for (int i = 0; i < mapSize; ++i) { + String key = stream.readSegmentedString(); + int valuesSize = stream.readCompressedInt(); + for (int j = 0; j < valuesSize; ++j) { + String value = stream.readSegmentedString(); + add(capabilitiesMap, key, value); + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + closeSilent(inputStream); + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException ex) { + } + } + } + } + } + + private synchronized void initRepositories(boolean force) { + if (repositories == null || force || repositoriesCacheFile.lastModified() + + repositoriesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) { + repositories = new HashMap<>(); + + ZipFile zipFile = null; + InputStream inputStream = null; + + try { + initRepositoriesCacheFile(); + + zipFile = new ZipFile(repositoriesCacheFile); + ZipEntry zipEntry = zipFile.getEntry("repositories"); //$NON-NLS-1$ + + inputStream = zipFile.getInputStream(zipEntry); + + Map options = new HashMap<>(); + options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); + options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); + options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); + + EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); + + timeStamp = stream.readLong(); + repositoriesRefreshHours = stream.readInt(); + int repositoryCount = stream.readInt(); + + Map> composedRepositories = new HashMap<>(); + for (int id = 1; id <= repositoryCount; id++) { + RepositoryImpl repository = new RepositoryImpl(stream, id, composedRepositories); + repositories.put(id, repository); + } + + for (Map.Entry> entry : composedRepositories.entrySet()) { + RepositoryImpl repository = entry.getKey(); + for (int compositeID : entry.getValue()) { + RepositoryImpl composite = repositories.get(compositeID); + if (composite != null) { + composite.addChild(repository); + repository.addComposite(composite); + } + } + } + + try { + int problematicRepositories = stream.readInt(); + for (int i = 0; i < problematicRepositories; i++) { + int id = stream.readInt(); + int unresolvedChildren = stream.readInt(); + + RepositoryImpl repository = repositories.get(id); + repository.unresolvedChildren = unresolvedChildren; + } + } catch (Exception ex) { + } + + repositoriesArray = repositories.values().toArray(new Repository[repositories.size()]); + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + close(inputStream); + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException ex) { + } + } + } + } + } + + private boolean initRepositoriesCacheFile() throws Exception { + if (repositoriesCacheFile == null) { + repositoriesCacheFile = new File(basedir, "repositories"); //$NON-NLS-1$ + } + + downloadIfModifiedSince(new URL(INDEX_BASE + "repositories"), repositoriesCacheFile); //$NON-NLS-1$ + + return true; + } + + private boolean initCapabilitiesCacheFile() throws Exception { + if (capabilitiesCacheFile == null) { + capabilitiesCacheFile = new File(basedir, "capabilities"); //$NON-NLS-1$ + } + + downloadIfModifiedSince(new URL(INDEX_BASE + "capabilities"), capabilitiesCacheFile); //$NON-NLS-1$ + + return true; + } + + @Override + public Repository[] getRepositories() { + initRepositories(false); + return repositoriesArray; + } + + @Override + public Map> getCapabilities() { + initCapabilities(); + return Collections.unmodifiableMap(capabilitiesMap); + } + + @Override + public Map> lookupCapabilities(String namespace, String name) { + initCapabilities(); + Map> capabilities = new HashMap<>(); + if (!isEmpty(namespace) && !isEmpty(name)) { + namespace = URI.encodeSegment(namespace, false); + name = URI.encodeSegment(name, false); + + BufferedReader reader = null; + + try { + InputStream inputStream = new URL(INDEX_BASE + namespace + "/" + name).openStream(); //$NON-NLS-1$ + reader = new BufferedReader(new InputStreamReader(inputStream)); + + String line = reader.readLine(); + if (line == null) { + return capabilities; + } + + long timeStamp = Long.parseLong(line); + initRepositories(timeStamp != this.timeStamp); + + while ((line = reader.readLine()) != null) { + String[] tokens = line.split(","); //$NON-NLS-1$ + int repositoryID = Integer.parseInt(tokens[0]); + Repository repository = repositories.get(repositoryID); + if (repository != null) { + Set versions = new HashSet<>(); + for (int i = 1; i < tokens.length; i++) { + versions.add(Version.parseVersion(tokens[i])); + } + + capabilities.put(repository, versions); + } + } + } catch (FileNotFoundException ex) { + // Ignore. + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + close(reader); + } + } + + return capabilities; + } + + @Override + public Map> generateCapabilitiesFromComposedRepositories( + Map> capabilitiesFromSimpleRepositories) { + Map> capabilities = new HashMap<>(); + for (Map.Entry> entry : capabilitiesFromSimpleRepositories.entrySet()) { + Repository repository = entry.getKey(); + Set versions = entry.getValue(); + recurseComposedRepositories(capabilities, repository, versions); + } + + return capabilities; + } + + private void recurseComposedRepositories(Map> capabilities, Repository repository, + Set versions) { + for (Repository composite : repository.getComposites()) { + Set set = capabilities.get(composite); + if (set == null) { + set = new HashSet<>(); + capabilities.put(composite, set); + } + + set.addAll(versions); + recurseComposedRepositories(capabilities, composite, versions); + } + } + + private static void downloadIfModifiedSince(URL url, File file) throws IOException { + long lastModified = -1L; + if (file.isFile()) { + lastModified = file.lastModified(); + } + + InputStream inputStream = null; + OutputStream outputStream = null; + + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + if (lastModified != -1) { + connection.setIfModifiedSince(lastModified); + } + + connection.connect(); + inputStream = connection.getInputStream(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + return; + } + + outputStream = new FileOutputStream(file); + copy(inputStream, outputStream); + outputStream.close(); + file.setLastModified(connection.getLastModified()); + } finally { + close(outputStream); + close(inputStream); + } + } + + /** + * @author Eike Stepper + */ + public static final class RepositoryImpl implements Repository { + public static final int UNINITIALIZED = -1; + + private static final Repository[] NO_REPOSITORIES = {}; + + private final URI location; + + private final int id; + + private final boolean composed; + + private final boolean compressed; + + private final long timestamp; + + private int capabilityCount; + + private int unresolvedChildren; + + private Repository[] children; + + private Repository[] composites; + + public RepositoryImpl(EObjectInputStream stream, int id, + Map> composedRepositories) throws IOException { + this.id = id; + location = stream.readURI(); + composed = stream.readBoolean(); + compressed = stream.readBoolean(); + timestamp = stream.readLong(); + + if (composed) { + capabilityCount = UNINITIALIZED; + } else { + capabilityCount = stream.readInt(); + } + + List composites = null; + while (stream.readBoolean()) { + if (composites == null) { + composites = new ArrayList<>(); + composedRepositories.put(this, composites); + } + + int composite = stream.readInt(); + composites.add(composite); + } + } + + @Override + public URI getLocation() { + return location; + } + + @Override + public int getID() { + return id; + } + + @Override + public boolean isComposed() { + return composed; + } + + @Override + public boolean isCompressed() { + return compressed; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public int getCapabilityCount() { + if (composed && capabilityCount == UNINITIALIZED) { + capabilityCount = 0; + for (Repository child : getChildren()) { + capabilityCount += child.getCapabilityCount(); + } + } + + return capabilityCount; + } + + @Override + public int getUnresolvedChildren() { + return unresolvedChildren; + } + + @Override + public Repository[] getChildren() { + if (children == null) { + return NO_REPOSITORIES; + } + + return children; + } + + @Override + public Repository[] getComposites() { + if (composites == null) { + return NO_REPOSITORIES; + } + + return composites; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + RepositoryImpl other = (RepositoryImpl) obj; + if (id != other.id) { + return false; + } + + return true; + } + + @Override + public int compareTo(Repository o) { + return location.toString().compareTo(o.getLocation().toString()); + } + + @Override + public String toString() { + return location.toString(); + } + + public void addChild(Repository child) { + children = addRepository(children, child); + } + + public void addComposite(Repository composite) { + composites = addRepository(composites, composite); + } + + private Repository[] addRepository(Repository[] repositories, Repository repository) { + if (repositories == null) { + return new Repository[] { repository }; + } + + int length = repositories.length; + Repository[] newRepositories = new Repository[length + 1]; + System.arraycopy(repositories, 0, newRepositories, 0, length); + newRepositories[length] = repository; + return newRepositories; + } + } + + public static boolean add(Map> map, K key, V value) { + Set set = getSet(map, key); + return set.add(value); + } + + public static Set getSet(Map> map, K key) { + Set set = map.get(key); + if (set == null) { + set = new LinkedHashSet<>(); + map.put(key, set); + } + + return set; + } + + public static void close(Closeable closeable) throws RuntimeException { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + public static Exception closeSilent(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + + return null; + } catch (Exception ex) { + return ex; + } + } + + public static long copy(InputStream input, OutputStream output) { + byte buffer[] = new byte[8192]; + try { + long length = 0; + int n; + + while ((n = input.read(buffer)) != -1) { + output.write(buffer, 0, n); + length += n; + } + + return length; + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + } \ No newline at end of file diff --git a/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java b/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java index f2a463844b..9c0f789faa 100644 --- a/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java +++ b/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java @@ -14,11 +14,13 @@ package org.eclipse.tycho.core.resolver.target; import java.util.AbstractMap.SimpleEntry; +import java.util.Collection; import java.util.Comparator; import java.util.LinkedHashSet; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.eclipse.equinox.p2.metadata.IInstallableUnit; import org.eclipse.equinox.p2.metadata.IProvidedCapability; @@ -27,6 +29,7 @@ import org.eclipse.equinox.p2.publisher.eclipse.Feature; import org.eclipse.equinox.p2.publisher.eclipse.FeatureEntry; import org.eclipse.equinox.p2.publisher.eclipse.FeaturesAction; +import org.eclipse.equinox.p2.query.CollectionResult; import org.eclipse.equinox.p2.query.IQuery; import org.eclipse.equinox.p2.query.IQueryResult; import org.eclipse.equinox.p2.query.QueryUtil; @@ -53,16 +56,31 @@ public static IInstallableUnit resolveReference(String type, String id, VersionR return ius.iterator().next(); } if (PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE.equals(type)) { - return ius.stream().flatMap(iu -> getPackageVersion(iu, id).map(v -> new SimpleEntry<>(iu, v)).stream()) - .max((o1, o2) -> { - return o1.getValue().compareTo(o2.getValue()); - }).map(Entry::getKey).orElse(null); + return findPackage(id, ius).orElse(null); } else { return ius.iterator().next(); } } - private static Optional getPackageVersion(IInstallableUnit unit, String packageName) { + public static Optional findPackage(String packageName, Collection ius) { + return findPackage(packageName, new CollectionResult<>(ius), null); + + } + + public static Optional findPackage(String packageName, IQueryResult query, + Version version) { + Stream> stream = query.stream() + .flatMap(iu -> getPackageVersion(iu, packageName).map(v -> new SimpleEntry<>(iu, v)).stream()); + if (version == null) { + return stream.max((o1, o2) -> { + return o1.getValue().compareTo(o2.getValue()); + }).map(Entry::getKey); + } else { + return stream.filter(e -> version.equals(e.getValue())).map(Entry::getKey).findFirst(); + } + } + + public static Optional getPackageVersion(IInstallableUnit unit, String packageName) { return unit.getProvidedCapabilities().stream() .filter(capability -> PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE.equals(capability.getNamespace())) diff --git a/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java new file mode 100644 index 0000000000..894b9459c0 --- /dev/null +++ b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * 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.artifacts; + +import java.nio.file.Path; + +import org.osgi.framework.Version; + +public interface ArtifactVersion { + + Path getArtifact(); + + Version getVersion(); + + String getProvider(); +} diff --git a/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java new file mode 100644 index 0000000000..cf41de1764 --- /dev/null +++ b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * 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.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 getPackageVersions(IInstallableUnit unit, String packageName, VersionRange versionRange, + MavenProject mavenProject); + +}