From db64bb923df73e2a29c584c8aef91b05878d4d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Sun, 12 Jan 2025 17:35:51 +0100 Subject: [PATCH 1/2] Add classes from org.eclipse.oomph.p2.core to read eclipse index files --- .../eclipse/tycho/copyfrom/oomph/P2Index.java | 62 ++ .../tycho/copyfrom/oomph/P2IndexImpl.java | 592 ++++++++++++++++++ 2 files changed, 654 insertions(+) create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java 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 new file mode 100644 index 0000000000..03cc9203fa --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2Index.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014, 2016 Eike Stepper (Loehne, Germany) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * 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; + +import java.util.Map; +import java.util.Set; + +/** + * @author Eike Stepper + */ +public interface P2Index +{ + public static final int SIMPLE_REPOSITORY = 0; + + public static final int COMPOSED_REPOSITORY = 1; + + public static final P2Index INSTANCE = P2IndexImpl.INSTANCE; + + public Repository[] getRepositories(); + + public Map> getCapabilities(); + + public Map> lookupCapabilities(String namespace, String name); + + public Map> generateCapabilitiesFromComposedRepositories(Map> capabilitiesFromSimpleRepositories); + + /** + * @author Eike Stepper + */ + public interface Repository extends Comparable + { + public URI getLocation(); + + public int getID(); + + public boolean isComposed(); + + public boolean isCompressed(); + + public long getTimestamp(); + + public int getCapabilityCount(); + + public int getUnresolvedChildren(); + + public Repository[] getChildren(); + + public Repository[] getComposites(); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..b7df4d3433 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/copyfrom/oomph/P2IndexImpl.java @@ -0,0 +1,592 @@ +/* + * Copyright (c) 2014, 2016-2018 Eike Stepper (Loehne, Germany) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * 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; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * @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; + } + } +} \ No newline at end of file From ae5c1e02de3050126cc0150de1ad312f3f05c889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Sun, 12 Jan 2025 17:48:44 +0100 Subject: [PATCH 2/2] [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. --- RELEASE_NOTES.md | 1 + pom.xml | 15 + tycho-baseline-plugin/pom.xml | 14 + .../tycho/baseline/DependencyCheckMojo.java | 360 ++++++ .../baseline/analyze/ClassCollection.java | 54 + .../tycho/baseline/analyze/ClassDef.java | 17 + .../analyze/ClassMethodSignature.java | 17 + .../tycho/baseline/analyze/ClassMethods.java | 102 ++ .../tycho/baseline/analyze/ClassUsage.java | 76 ++ .../baseline/analyze/DependencyAnalyzer.java | 105 ++ .../tycho/baseline/analyze/JrtClasses.java | 98 ++ .../baseline/analyze/MethodSignature.java | 35 + .../EclipseIndexArtifactVersionProvider.java | 192 +++ .../MavenArtifactVersionProvider.java | 177 +++ .../eclipse/tycho/copyfrom/oomph/P2Index.java | 11 +- .../tycho/copyfrom/oomph/P2IndexImpl.java | 1073 ++++++++--------- .../core/resolver/target/ArtifactMatcher.java | 28 +- .../tycho/artifacts/ArtifactVersion.java | 26 + .../artifacts/ArtifactVersionProvider.java | 26 + 19 files changed, 1859 insertions(+), 568 deletions(-) create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassCollection.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassDef.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethodSignature.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethods.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassUsage.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/DependencyAnalyzer.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/JrtClasses.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/MethodSignature.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/EclipseIndexArtifactVersionProvider.java create mode 100644 tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/MavenArtifactVersionProvider.java create mode 100644 tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java create mode 100644 tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8cebf868c0..dcd59b0645 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -42,6 +42,7 @@ This is now fixed, but might result in build previously working now fail due to backports: - Support for implicit dependencies in target definitions +- Add tycho-baseline:check-dependencies mojo ## 4.0.10 diff --git a/pom.xml b/pom.xml index 79cf294cf8..ef3300804f 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,7 @@ 3.1.1 3.3.2 ${surefire-version} + 0.9.0.M3 @@ -511,6 +512,20 @@ maven-invoker-plugin 3.9.0 + + org.eclipse.sisu + sisu-maven-plugin + ${sisu-version} + + + index-project + + main-index + test-index + + + + diff --git a/tycho-baseline-plugin/pom.xml b/tycho-baseline-plugin/pom.xml index 7be7900027..f7c7ae81a7 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..404d7a5895 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java @@ -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: + *
    + *
  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 { + + @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 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 = DependencyAnalyzer.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<>(); + Map> allPackageVersion = new HashMap<>(); + Set packageWithError = new HashSet<>(); + Function> classResolver = DependencyAnalyzer + .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(); + Optional 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 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 + 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 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 / %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 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 + " is " + lowestPackageVersion.get(pkg); + Set 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 packageWithError, Map lowestPackageVersion) + throws IOException { + MutableBundleManifest manifest = MutableBundleManifest.read(manifestFile); + Map exportedPackagesVersion = manifest.getExportedPackagesVersion(); + Map 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 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 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..7efc7a9833 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/ClassMethods.java @@ -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 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()) + // constructors are not inheritable + .filter(ms -> !ms.isContructor()).map(ms -> inherit(cd, ms)); + }); + // 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()) + // constructors are not inheritable + .filter(ms -> !ms.isContructor()).map(ms -> inherit(cd, ms)); + }); + Stream 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 findRef(String cn) { + return supplier.apply(cn); + } + +} 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..1152b53858 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/DependencyAnalyzer.java @@ -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> 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; + } + + public static 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); + } + } + + public static 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); + } + } + +} 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..b26241f799 --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/MethodSignature.java @@ -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 { + + 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 "".equals(methodName) || "".equals(methodName); + } +} \ 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..95e6917c4e --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/EclipseIndexArtifactVersionProvider.java @@ -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 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(), 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 repositories; + private org.osgi.framework.Version osgiVersion; + private Optional unit; + private Repository unitRepo; + + public EclipseIndexArtifactVersion(List 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 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 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 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); + } + + } + +} 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..619a52ff0e --- /dev/null +++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/MavenArtifactVersionProvider.java @@ -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 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 b138e48891..418949c624 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..55a640125d --- /dev/null +++ b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java @@ -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(); +} 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..eaa0c492d6 --- /dev/null +++ b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java @@ -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 getPackageVersions(IInstallableUnit unit, String packageName, VersionRange versionRange, + MavenProject mavenProject); + +}