diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index fb7007aef6..d450e392b5 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -32,6 +32,38 @@ All of the following variants to specify a version are now possible:
 </target>
 ```
 
+## new `check-dependencies` mojo
+
+When using version ranges there is a certain risk that one actually uses some methods from never release and it goes unnoticed.
+
+There is now a new `tycho-baseline:dependencies mojo` that analyze the compiled class files for used method references and compares them to
+the individual artifacts that match the version range. To find these versions it uses the maven metadata stored in P2 as well as
+the eclipse-repository index to find possible candidates.
+
+If any problems are found, these are written by default to `target/versionProblems.txt` but one can also enable to update the version ranges
+according to the discovered problems, a configuration for this might look like this:
+
+```xml
+   <plugin>
+    <groupId>org.eclipse.tycho</groupId>
+    <artifactId>tycho-baseline-plugin</artifactId>
+    <version>${tycho.version}</version>
+    <executions>
+      <execution>
+        <id>checkit</id>
+        <goals>
+          <goal>check-dependencies</goal>
+        </goals>
+        <configuration>
+        	<applySuggestions>true</applySuggestions>
+        </configuration>
+      </execution>
+    </executions>
+  </plugin
+```
+
+Because this can be a time consuming task to fetch all matching versions it is best placed inside a profile that is enabled on demand.
+
 ## new `update-manifest` mojo
 
 It is recommended to use as the lower bound the dependency the code was
diff --git a/tycho-baseline-plugin/pom.xml b/tycho-baseline-plugin/pom.xml
index 5acee8e8ba..f76e211228 100644
--- a/tycho-baseline-plugin/pom.xml
+++ b/tycho-baseline-plugin/pom.xml
@@ -53,6 +53,16 @@
 			<artifactId>asciitable</artifactId>
 			<version>0.3.2</version>
 		</dependency>
+		<dependency>
+			<groupId>org.ow2.asm</groupId>
+			<artifactId>asm</artifactId>
+			<version>9.7.1</version>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.emf</groupId>
+			<artifactId>org.eclipse.emf.ecore</artifactId>
+			<version>2.38.0</version>
+		</dependency>
 	</dependencies>
 	<build>
 		<plugins>
@@ -60,6 +70,10 @@
 				<groupId>org.codehaus.plexus</groupId>
 				<artifactId>plexus-component-metadata</artifactId>
 			</plugin>
+			<plugin>
+				<groupId>org.eclipse.sisu</groupId>
+				<artifactId>sisu-maven-plugin</artifactId>
+			</plugin>
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-plugin-plugin</artifactId>
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:
+ * <ol>
+ * <li>The current project artifact is analyzed for method signatures it
+ * calls</li>
+ * <li>Then it is checked what of these match to a given dependency</li>
+ * <li>All dependency versions matching the range are fetched and inspected
+ * using {@link ArtifactVersionProvider}s</li>
+ * <li>Then it checks if there are any missing signatures or inconsistencies and
+ * possibly failing the build</li>
+ * </ol>
+ */
+@Mojo(defaultPhase = LifecyclePhase.VERIFY, name = "check-dependencies", threadSafe = true, requiresProject = true)
+public class DependencyCheckMojo extends AbstractMojo {
+
+	@Parameter(property = "project", readonly = true)
+	private MavenProject project;
+
+	@Parameter(property = "session", readonly = true)
+	private MavenSession session;
+
+	@Parameter(defaultValue = "${project.build.directory}/versionProblems.txt", property = "tycho.dependency.check.report")
+	private File reportFileName;
+
+	@Parameter(defaultValue = "${project.basedir}/META-INF/MANIFEST.MF", property = "tycho.dependency.check.manifest")
+	private File manifestFile;
+
+	@Parameter(defaultValue = "false", property = "tycho.dependency.check.apply")
+	private boolean applySuggestions;
+
+	@Component
+	private TychoProjectManager projectManager;
+
+	@Component
+	private List<ArtifactVersionProvider> versionProvider;
+
+	@Component
+	private BundleReader bundleReader;
+
+	@Component
+	ToolchainProvider toolchainProvider;
+
+	@Override
+	public void execute() throws MojoExecutionException, MojoFailureException {
+		if (!"jar".equals(project.getPackaging())
+				&& !PackagingType.TYPE_ECLIPSE_PLUGIN.equals(project.getPackaging())) {
+			return;
+		}
+		DependencyArtifacts artifacts = projectManager.getDependencyArtifacts(project).orElse(null);
+		File file = project.getArtifact().getFile();
+		if (file == null || !file.isFile()) {
+			throw new MojoFailureException("Project artifact is not a valid file");
+		}
+		JrtClasses jrtClassResolver = getJRTClassResolver();
+		List<ClassUsage> usages = DependencyAnalyzer.analyzeUsage(file, jrtClassResolver);
+		if (usages.isEmpty()) {
+			return;
+		}
+		Collection<IInstallableUnit> units = artifacts.getInstallableUnits();
+		ModuleRevisionBuilder builder = readOSGiInfo(file);
+		List<GenericInfo> requirements = builder.getRequirements();
+		List<DependencyVersionProblem> dependencyProblems = new ArrayList<>();
+		Map<Path, ClassCollection> analyzeCache = new HashMap<>();
+		Log log = getLog();
+		Map<String, Version> lowestPackageVersion = new HashMap<>();
+		Map<String, Set<Version>> allPackageVersion = new HashMap<>();
+		Set<String> packageWithError = new HashSet<>();
+		Function<String, Optional<ClassMethods>> classResolver = DependencyAnalyzer
+				.createDependencyClassResolver(jrtClassResolver, artifacts);
+		for (GenericInfo genericInfo : requirements) {
+			if (PackageNamespace.PACKAGE_NAMESPACE.equals(genericInfo.getNamespace())) {
+				Map<String, String> pkgInfo = getVersionInfo(genericInfo,
+						PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE);
+				String packageVersion = pkgInfo.getOrDefault(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, "0.0.0");
+				String packageName = pkgInfo.get(PackageNamespace.PACKAGE_NAMESPACE);
+				Optional<IInstallableUnit> packageProvidingUnit = ArtifactMatcher.findPackage(packageName, units);
+				if (packageProvidingUnit.isEmpty()) {
+					continue;
+				}
+				if (packageName.contains(".internal.")) {
+					// TODO configurable, but also internal packages should be properly versioned!
+					continue;
+				}
+				IInstallableUnit unit = packageProvidingUnit.get();
+				Optional<org.eclipse.equinox.p2.metadata.Version> matchedPackageVersion = ArtifactMatcher
+						.getPackageVersion(unit, packageName);
+				matchedPackageVersion.filter(v -> v.isOSGiCompatible())
+						.ifPresent(v -> {
+							Version current = new Version(v.toString());
+							allPackageVersion.computeIfAbsent(packageName, nil -> new TreeSet<>()).add(current);
+							lowestPackageVersion.put(packageName, current);
+						});
+				VersionRange versionRange = VersionRange.valueOf(packageVersion);
+				List<ArtifactVersion> list = versionProvider.stream()
+						.flatMap(avp -> avp.getPackageVersions(unit, packageName, versionRange, project)).toList();
+				if (log.isDebugEnabled()) {
+					log.debug("== " + packageName + " " + packageVersion + " is provided by " + unit
+							+ " with version range " + versionRange + ", matching versions: " + list.stream()
+									.map(av -> av.getVersion()).map(String::valueOf).collect(Collectors.joining(", ")));
+				}
+				Set<MethodSignature> packageMethods = new TreeSet<>();
+				Map<MethodSignature, Collection<String>> references = new HashMap<>();
+				for (ClassUsage usage : usages) {
+					usage.signatures().filter(ms -> packageName.equals(ms.packageName())).forEach(sig -> {
+						packageMethods.add(sig);
+						references.computeIfAbsent(sig, nil -> new TreeSet<>()).addAll(usage.classRef(sig));
+					});
+				}
+				if (packageMethods.isEmpty()) {
+					// it could be that actually no methods referenced (e.g. interface is only
+					// referencing a type)
+					// TODO we need to check that the types used are present in all versions as
+					// otherwise we will get CNF exception!
+					// TODO a class can also reference fields!
+					continue;
+				}
+				if (log.isDebugEnabled()) {
+					for (MethodSignature signature : packageMethods) {
+						log.debug("Referenced: " + signature.id());
+					}
+				}
+				// now we need to inspect all jars
+				for (ArtifactVersion v : list) {
+					Version version = v.getVersion();
+					if (version == null) {
+						continue;
+					}
+					if (!allPackageVersion.computeIfAbsent(packageName, nil -> new TreeSet<>()).add(version)) {
+						// already checked!
+						continue;
+					}
+					Path artifact = v.getArtifact();
+					log.debug(v + "=" + artifact);
+					if (artifact == null) {
+						// Retrieval of artifacts might be lazy and we can't get this one --> error?
+						continue;
+					}
+					ClassCollection collection = analyzeCache.get(artifact);
+					if (collection == null) {
+						collection = DependencyAnalyzer.analyzeProvides(artifact.toFile(), classResolver, null);
+						analyzeCache.put(artifact, collection);
+					}
+					boolean ok = true;
+					Set<MethodSignature> set = collection.provides().collect(Collectors.toSet());
+					for (MethodSignature mthd : packageMethods) {
+						if (!set.contains(mthd)) {
+							List<MethodSignature> provided = collection.get(mthd.className());
+							if (provided != null) {
+								provided = provided.stream().filter(ms -> packageName.equals(ms.packageName()))
+										.toList();
+							}
+							if (log.isDebugEnabled()) {
+								log.debug("Not found: " + mthd);
+								if (provided != null) {
+									for (MethodSignature s : provided) {
+										log.debug("Provided:  " + s);
+									}
+								}
+							}
+							dependencyProblems.add(new DependencyVersionProblem(String.format(
+									"Import-Package '%s %s (compiled against %s %s / %s) includes %s (provided by %s) but this version is missing the method %s",
+									packageName, packageVersion, unit.getId(), unit.getVersion(),
+									matchedPackageVersion.orElse(org.eclipse.equinox.p2.metadata.Version.emptyVersion)
+											.toString(),
+									v.getVersion(), v.getProvider(), mthd.id()), references.get(mthd), provided));
+							ok = false;
+							packageWithError.add(packageName);
+						}
+					}
+					if (ok) {
+						lowestPackageVersion.merge(packageName, version, (v1, v2) -> {
+							if (v1.compareTo(v2) > 0) {
+								return v2;
+							}
+							return v1;
+						});
+					}
+				}
+				// TODO we should emit a warning if the lower bound is not part of the
+				// discovered versions (or even fail?)
+
+			}
+		}
+		if (dependencyProblems.isEmpty()) {
+			return;
+		}
+		List<String> results = new ArrayList<>();
+		for (DependencyVersionProblem problem : dependencyProblems) {
+			Collection<String> references = problem.references();
+			String message;
+			if (references == null || references.isEmpty()) {
+				message = problem.message();
+			} else {
+				message = String.format("%s, referenced by:%s%s", problem.message(), System.lineSeparator(),
+						problem.references().stream().collect(Collectors.joining(System.lineSeparator())));
+			}
+			log.error(message);
+			results.add(message);
+			List<MethodSignature> provided = problem.provided();
+			if (provided != null && !provided.isEmpty()) {
+				results.add("Provided Methods in this version are:");
+				for (MethodSignature sig : provided) {
+					results.add("\t" + sig);
+				}
+			}
+		}
+		for (String pkg : packageWithError) {
+			String suggestion = "Suggested lower version for package " + pkg + " is " + lowestPackageVersion.get(pkg);
+			Set<Version> all = allPackageVersion.get(pkg);
+			if (all != null && !all.isEmpty()) {
+				suggestion += " out of " + all;
+			}
+			log.info(suggestion);
+			results.add(suggestion);
+		}
+		if (results.isEmpty()) {
+			return;
+		}
+		try {
+			Files.writeString(reportFileName.toPath(),
+					results.stream().collect(Collectors.joining(System.lineSeparator())));
+			if (applySuggestions) {
+				applyLowerBounds(packageWithError, lowestPackageVersion);
+			}
+		} catch (IOException e) {
+			throw new MojoFailureException(e);
+		}
+	}
+
+	private void applyLowerBounds(Set<String> packageWithError, Map<String, Version> lowestPackageVersion)
+			throws IOException {
+		MutableBundleManifest manifest = MutableBundleManifest.read(manifestFile);
+		Map<String, String> exportedPackagesVersion = manifest.getExportedPackagesVersion();
+		Map<String, String> updates = new HashMap<>();
+		for (String packageName : packageWithError) {
+			Version lowestVersion = lowestPackageVersion.getOrDefault(packageName, Version.emptyVersion);
+			String current = exportedPackagesVersion.get(packageName);
+			if (current == null) {
+				updates.put(packageName, String.format("[%s,%d)", lowestVersion, (lowestVersion.getMajor() + 1)));
+			} else {
+				VersionRange range = VersionRange.valueOf(current);
+				Version right = range.getRight();
+				updates.put(packageName, String.format("[%s,%s%c", lowestVersion, right, range.getRightType()));
+			}
+		}
+		manifest.updateImportedPackageVersions(updates);
+		MutableBundleManifest.write(manifest, manifestFile);
+	}
+
+	private Map<String, String> getVersionInfo(GenericInfo genericInfo, String versionAttribute) {
+		Map<String, String> directives = new HashMap<>(genericInfo.getDirectives());
+		String filter = directives.remove(Namespace.REQUIREMENT_FILTER_DIRECTIVE);
+		FilterImpl filterImpl;
+		try {
+			filterImpl = FilterImpl.newInstance(filter);
+		} catch (InvalidSyntaxException e) {
+			throw new IllegalArgumentException("Invalid filter directive", e); //$NON-NLS-1$
+		}
+		return filterImpl.getStandardOSGiAttributes(versionAttribute);
+	}
+
+	private ModuleRevisionBuilder readOSGiInfo(File file) throws MojoFailureException {
+		OsgiManifest manifest = bundleReader.loadManifest(file);
+		ModuleRevisionBuilder builder;
+		try {
+			builder = OSGiManifestBuilderFactory.createBuilder(manifest.getHeaders());
+		} catch (BundleException e) {
+			throw new MojoFailureException(e);
+		}
+		return builder;
+	}
+
+	private JrtClasses getJRTClassResolver() {
+		String profileName = projectManager.getExecutionEnvironments(project, session).findFirst()
+				.map(ee -> ee.getProfileName()).orElse(null);
+		if (profileName != null) {
+			OSGiJavaToolchain osgiToolchain = toolchainProvider.getToolchain(profileName).orElse(null);
+			if (osgiToolchain != null) {
+				return new JrtClasses(osgiToolchain.getJavaHome());
+			}
+		}
+		// use running jvm
+		return new JrtClasses(null);
+	}
+
+	private static record DependencyVersionProblem(String message, Collection<String> references,
+			List<MethodSignature> provided) {
+
+	}
+}
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<String, Optional<ClassMethods>>, Consumer<ClassMethods> {
+
+	private Map<String, ClassMethods> classLookupMap = new HashMap<>();
+
+	@Override
+	public Optional<ClassMethods> apply(String className) {
+		return Optional.ofNullable(classLookupMap.get(className));
+	}
+
+	public Stream<MethodSignature> provides() {
+
+		return classLookupMap.values().stream().distinct().flatMap(cm -> cm.provides());
+	}
+
+	public List<MethodSignature> get(String className) {
+		return apply(className).stream().flatMap(cm -> cm.provides()).toList();
+	}
+
+	public Function<String, Optional<ClassMethods>> chain(Function<String, Optional<ClassMethods>> chained) {
+		return cls -> {
+			return apply(cls).or(() -> chained.apply(cls));
+		};
+	}
+
+	@Override
+	public void accept(ClassMethods methods) {
+		methods.definitions().forEach(def -> {
+			classLookupMap.put(def.name(), methods);
+		});
+	}
+
+}
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<ClassDef> classDefs = new ArrayList<>();
+	private List<ClassMethodSignature> signatures = new ArrayList<>();
+	private Function<String, Optional<ClassMethods>> supplier;
+	private Set<MethodSignature> collect;
+
+	public ClassMethods(byte[] classbytes, Function<String, Optional<ClassMethods>> supplier) {
+		this.supplier = supplier;
+		ClassReader reader = new ClassReader(classbytes);
+		reader.accept(new ClassVisitor(DependencyAnalyzer.ASM_API) {
+
+			private ClassDef classDef;
+
+			@Override
+			public void visit(int version, int access, String name, String signature, String superName,
+					String[] interfaces) {
+				if ((access & Opcodes.ACC_PRIVATE) != 0) {
+					// private methods can not be called
+					return;
+				}
+				classDef = new ClassDef(access, name, signature, superName, interfaces);
+				classDefs.add(classDef);
+			}
+
+			@Override
+			public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
+					String[] exceptions) {
+				signatures.add(new ClassMethodSignature(classDef, access, name, descriptor, signature));
+				return null;
+			}
+		}, ClassReader.SKIP_FRAMES);
+	}
+
+	Stream<ClassDef> definitions() {
+		return classDefs.stream();
+	}
+
+	/**
+	 * @return a stream of all method signatures this class can provide
+	 */
+	Stream<MethodSignature> provides() {
+		// all methods declared by our class are provided
+		Stream<MethodSignature> declared = signatures.stream().map(cms -> {
+			return new MethodSignature(cms.clazz().name(), cms.name(), cms.descriptor());
+		});
+		// and from the super class, transformed with our class as the classname
+		Stream<MethodSignature> supermethods = classDefs.stream().flatMap(cd -> {
+			return Optional.ofNullable(cd.superName()).flatMap(cn -> findRef(cn)).stream().flatMap(cm -> cm.provides())
+					// constructors are not inheritable
+					.filter(ms -> !ms.isContructor()).map(ms -> inherit(cd, ms));
+		});
+		// and possibly from interfaces
+		Stream<MethodSignature> interfaces = classDefs.stream().flatMap(cd -> {
+			return Optional.ofNullable(cd.interfaces()).stream().flatMap(Arrays::stream)
+					.flatMap(cn -> findRef(cn).stream()).flatMap(cm -> cm.provides())
+					// constructors are not inheritable
+					.filter(ms -> !ms.isContructor()).map(ms -> inherit(cd, ms));
+		});
+		Stream<MethodSignature> inherited = Stream.concat(supermethods, interfaces);
+		return Stream.concat(declared, inherited).distinct();
+	}
+
+	private MethodSignature inherit(ClassDef cd, MethodSignature ms) {
+		return new MethodSignature(cd.name(), ms.methodName(), ms.signature());
+	}
+
+	private Optional<ClassMethods> findRef(String cn) {
+		return supplier.apply(cn);
+	}
+
+}
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<MethodSignature> usedMethodSignatures = new HashSet<>();
+	private Map<MethodSignature, Collection<String>> classRef = new HashMap<>();
+
+	public ClassUsage(byte[] classbytes, JrtClasses jrt) {
+		ClassReader reader = new ClassReader(classbytes);
+		reader.accept(new ClassVisitor(Opcodes.ASM9) {
+
+			private String className;
+
+			@Override
+			public void visit(int version, int access, String name, String signature, String superName,
+					String[] interfaces) {
+				this.className = name.replace('/', '.');
+			}
+
+			@Override
+			public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
+					String[] exceptions) {
+				return new MethodVisitor(Opcodes.ASM9) {
+					@Override
+					public void visitMethodInsn(int opcode, String owner, String name, String descriptor,
+							boolean isInterface) {
+						if (jrt.apply(owner).isPresent()) {
+							// ignore references to java provided classes
+							return;
+						}
+						MethodSignature sig = new MethodSignature(owner, name, descriptor);
+						classRef.computeIfAbsent(sig, nil -> new TreeSet<>()).add(className);
+						usedMethodSignatures.add(sig);
+					}
+				};
+			}
+		}, ClassReader.SKIP_FRAMES);
+	}
+
+	public Stream<MethodSignature> signatures() {
+		return usedMethodSignatures.stream();
+	}
+
+	public Collection<String> classRef(MethodSignature mthd) {
+		return classRef.getOrDefault(mthd, List.of());
+	}
+}
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<String, Optional<ClassMethods>> createDependencyClassResolver(JrtClasses jrtClassResolver,
+			DependencyArtifacts artifacts) throws MojoFailureException {
+		ClassCollection allClassMethods = new ClassCollection();
+		Function<String, Optional<ClassMethods>> function = allClassMethods.chain(jrtClassResolver);
+		List<ArtifactDescriptor> list = artifacts.getArtifacts(ArtifactType.TYPE_ECLIPSE_PLUGIN);
+		for (ArtifactDescriptor descriptor : list) {
+			File file = descriptor.fetchArtifact().join();
+			analyzeProvides(file, function, allClassMethods);
+		}
+		return function;
+	}
+
+	public static List<ClassUsage> analyzeUsage(File file, JrtClasses jre) throws MojoFailureException {
+		List<ClassUsage> usages = new ArrayList<>();
+		try {
+			try (JarFile jar = new JarFile(file)) {
+				Enumeration<JarEntry> entries = jar.entries();
+				while (entries.hasMoreElements()) {
+					JarEntry jarEntry = entries.nextElement();
+					String name = jarEntry.getName();
+					if (name.endsWith(CLASS_SUFFIX)) {
+						InputStream stream = jar.getInputStream(jarEntry);
+						usages.add(new ClassUsage(stream.readAllBytes(), jre));
+					}
+				}
+			}
+			return usages;
+		} catch (IOException e) {
+			throw new MojoFailureException(e);
+		}
+	}
+
+	public static ClassCollection analyzeProvides(File file, Function<String, Optional<ClassMethods>> classResolver,
+			Consumer<ClassMethods> consumer) throws MojoFailureException {
+		try {
+			ClassCollection local = new ClassCollection();
+			Function<String, Optional<ClassMethods>> resolver = local.chain(classResolver);
+			try (JarFile jar = new JarFile(file)) {
+				Enumeration<JarEntry> entries = jar.entries();
+				while (entries.hasMoreElements()) {
+					JarEntry jarEntry = entries.nextElement();
+					String name = jarEntry.getName();
+					if (name.endsWith(CLASS_SUFFIX)) {
+						InputStream stream = jar.getInputStream(jarEntry);
+						ClassMethods methods = new ClassMethods(stream.readAllBytes(), resolver);
+						if (consumer != null) {
+							consumer.accept(methods);
+						}
+						local.accept(methods);
+					}
+				}
+			}
+			return local;
+		} catch (IOException e) {
+			throw new MojoFailureException(e);
+		}
+	}
+
+}
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<String, Optional<ClassMethods>> {
+
+	private Path rootPath;
+	private Map<String, Optional<ClassMethods>> cache = new ConcurrentHashMap<>();
+
+	public JrtClasses(String javaHome) {
+		try {
+			Map<String, String> map;
+			if (javaHome != null) {
+				map = Map.of("java.home", javaHome);
+			} else {
+				map = Map.of();
+			}
+			FileSystem fs = FileSystems.newFileSystem(URI.create("jrt:/"), map);
+			rootPath = fs.getPath("/packages");
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	@Override
+	public Optional<ClassMethods> apply(String className) {
+		if (rootPath == null) {
+			return Optional.empty();
+		}
+		return cache.computeIfAbsent(className.replace('.', '/'), path -> lookupJreClass(className));
+	}
+
+	private Optional<ClassMethods> lookupJreClass(String classPath) {
+//	Paths in the "jrt:/" NIO filesystem are of this form:
+//
+//		  /modules/$MODULE/$PATH
+//		  /packages/$PACKAGE/$MODULE
+//
+//		where $PACKAGE is a package name (e.g., "java.lang"). A path of the
+//		second form names a symbolic link which, in turn, points to the
+//		directory under /modules that contains a module that defines that
+//		package. Example:
+//
+//		  /packages/java.lang/java.base -> /modules/java.base
+//
+//		To find java/sql/Array.class without knowing its module you look up
+//		/packages/java.sql, which is a directory, and enumerate its entries.
+//		In this case there will be just one entry, a symbolic link named
+//		"java.sql", which will point to /modules/java.sql, which will contain
+//		java/sql/Array.class.
+//
+		String packageName = DependencyAnalyzer.getPackageName(classPath);
+		Path modulesPath = rootPath.resolve(packageName);
+		if (Files.isDirectory(modulesPath)) {
+			try (Stream<Path> module = Files.list(modulesPath)) {
+				Iterator<Path> iterator = module.iterator();
+				while (iterator.hasNext()) {
+					Path modulePath = iterator.next();
+					Path classFile = modulePath.resolve(classPath + ".class");
+					if (Files.isRegularFile(classFile)) {
+						return Optional.of(new ClassMethods(Files.readAllBytes(classFile), JrtClasses.this));
+					}
+				}
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+		return Optional.empty();
+	}
+
+}
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<MethodSignature> {
+
+	public String packageName() {
+		String cn = className();
+		return DependencyAnalyzer.getPackageName(cn);
+	}
+
+	public String id() {
+		return className() + "#" + methodName() + signature();
+	}
+
+	@Override
+	public int compareTo(MethodSignature o) {
+		return id().compareTo(o.id());
+	}
+
+	public boolean isContructor() {
+		return "<clinit>".equals(methodName) || "<init>".equals(methodName);
+	}
+}
\ 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..5cbc378b2a
--- /dev/null
+++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/EclipseIndexArtifactVersionProvider.java
@@ -0,0 +1,192 @@
+/*******************************************************************************
+ * Copyright (c) 2024 Christoph Läubrich and others.
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christoph Läubrich - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.tycho.baseline.provider;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.logging.Logger;
+import org.eclipse.equinox.p2.metadata.IInstallableUnit;
+import org.eclipse.equinox.p2.metadata.Version;
+import org.eclipse.equinox.p2.query.QueryUtil;
+import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
+import org.eclipse.equinox.spi.p2.publisher.PublisherHelper;
+import org.eclipse.tycho.artifacts.ArtifactVersion;
+import org.eclipse.tycho.artifacts.ArtifactVersionProvider;
+import org.eclipse.tycho.copyfrom.oomph.P2Index;
+import org.eclipse.tycho.copyfrom.oomph.P2Index.Repository;
+import org.eclipse.tycho.copyfrom.oomph.P2IndexImpl;
+import org.eclipse.tycho.core.resolver.target.ArtifactMatcher;
+import org.eclipse.tycho.p2maven.repository.P2RepositoryManager;
+import org.eclipse.tycho.p2maven.transport.TransportCacheConfig;
+import org.osgi.framework.VersionRange;
+
+/**
+ * {@link ArtifactVersionProvider} using eclipse index
+ */
+@Named
+public class EclipseIndexArtifactVersionProvider implements ArtifactVersionProvider {
+
+	private P2Index p2Index;
+	private P2RepositoryManager repositoryManager;
+	private Logger logger;
+
+	@Inject
+	public EclipseIndexArtifactVersionProvider(TransportCacheConfig cacheConfig, P2RepositoryManager repositoryManager,
+			Logger logger) {
+		this.repositoryManager = repositoryManager;
+		this.logger = logger;
+		p2Index = new P2IndexImpl(new File(cacheConfig.getCacheLocation(), "index"));
+	}
+
+	@Override
+	public Stream<ArtifactVersion> getPackageVersions(IInstallableUnit unit, String packageName,
+			VersionRange versionRange, MavenProject mavenProject) {
+		Map<Repository, Set<Version>> map = p2Index.lookupCapabilities(PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE,
+				packageName);
+		Map<Version, List<Repository>> found = new HashMap<>();
+		map.entrySet().forEach(entry -> {
+			entry.getValue().stream().filter(v -> v.isOSGiCompatible()).forEach(v -> {
+				found.computeIfAbsent(v, x -> new ArrayList<>()).add(entry.getKey());
+			});
+		});
+		String id = unit.getId();
+		return found.entrySet().stream().map(entry -> {
+			return new EclipseIndexArtifactVersion(entry.getValue(), id, packageName, entry.getKey(), logger);
+		}).filter(eia -> versionRange.includes(eia.getVersion()))
+				.sorted(Comparator.comparing(EclipseIndexArtifactVersion::getVersion).reversed())
+				.map(ArtifactVersion.class::cast);
+	}
+
+	private class EclipseIndexArtifactVersion implements ArtifactVersion {
+
+		private Version version;
+		private String packageName;
+		private Path tempFile;
+		private String unitId;
+		private List<Repository> repositories;
+		private org.osgi.framework.Version osgiVersion;
+		private Optional<IInstallableUnit> unit;
+		private Repository unitRepo;
+
+		public EclipseIndexArtifactVersion(List<Repository> repositories, String unitId, String packageName,
+				Version version, Logger logger) {
+			osgiVersion = org.osgi.framework.Version.parseVersion(version.getOriginal());
+			this.repositories = repositories;
+
+			this.unitId = unitId;
+			this.packageName = packageName;
+			this.version = version;
+		}
+
+		@Override
+		public Path getArtifact() {
+			if (tempFile == null) {
+				IInstallableUnit unit = getUnit().orElse(null);
+				if (unit != null) {
+					Path file;
+					try {
+						file = Files.createTempFile(unit.getId(), ".jar");
+					} catch (IOException e) {
+						return null;
+					}
+					file.toFile().deleteOnExit();
+					List<Repository> list = new ArrayList<>(repositories);
+					if (unitRepo != null) {
+						list.remove(unitRepo);
+						list.add(0, unitRepo);
+					}
+					for (Repository repository : list) {
+						try {
+							org.apache.maven.model.Repository r = new org.apache.maven.model.Repository();
+							r.setUrl(repository.getLocation().toString());
+							try (OutputStream stream = Files.newOutputStream(file)) {
+								repositoryManager.downloadArtifact(unit, repositoryManager.getArtifactRepository(r),
+										stream);
+								return tempFile = file;
+							}
+						} catch (Exception e) {
+							logger.error("Fetch artifact for unit " + unit.getId() + " from " + repository.getLocation()
+									+ " failed: " + e);
+						}
+					}
+					file.toFile().delete();
+				}
+			}
+			return tempFile;
+		}
+
+		private Optional<IInstallableUnit> getUnit() {
+			if (unit == null) {
+				for (Repository repository : repositories) {
+					try {
+						org.apache.maven.model.Repository r = new org.apache.maven.model.Repository();
+						r.setUrl(repository.getLocation().toString());
+						IMetadataRepository metadataRepository = repositoryManager.getMetadataRepository(r);
+						Optional<IInstallableUnit> optional = ArtifactMatcher.findPackage(packageName,
+								metadataRepository.query(QueryUtil.createIUQuery(unitId), null), version);
+						if (optional.isPresent()) {
+							this.unitRepo = repository;
+							return unit = optional;
+						} else {
+							// if we have a package exported from many bundles it might be the case that the
+							// actual unit we look for is not found, so we need to try on
+							logger.debug(
+									"Package " + packageName + " not found in metadata of " + repository.getLocation());
+						}
+					} catch (Exception e) {
+						logger.error("Fetch metadata from " + repository.getLocation() + ":: " + e);
+					}
+				}
+				unit = Optional.empty();
+			}
+			return Objects.requireNonNullElse(unit, Optional.empty());
+		}
+
+		@Override
+		public org.osgi.framework.Version getVersion() {
+			return osgiVersion;
+		}
+
+		@Override
+		public String toString() {
+			if (unit != null && unit.isPresent()) {
+				return getVersion() + " (" + unit.get() + ")";
+			}
+			return getVersion().toString();
+		}
+
+		@Override
+		public String getProvider() {
+			return getUnit().map(unit -> unit.getId() + " " + unit.getVersion()).orElse(null);
+		}
+
+	}
+
+}
diff --git a/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/MavenArtifactVersionProvider.java b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/MavenArtifactVersionProvider.java
new file mode 100644
index 0000000000..285382e105
--- /dev/null
+++ b/tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/MavenArtifactVersionProvider.java
@@ -0,0 +1,177 @@
+/*******************************************************************************
+ * Copyright (c) 2024 Christoph Läubrich and others.
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christoph Läubrich - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.tycho.baseline.provider;
+
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.maven.RepositoryUtils;
+import org.apache.maven.SessionScoped;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.project.MavenProject;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.ArtifactRequest;
+import org.eclipse.aether.resolution.ArtifactResolutionException;
+import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.version.Version;
+import org.eclipse.equinox.p2.metadata.IInstallableUnit;
+import org.eclipse.osgi.container.ModuleRevisionBuilder;
+import org.eclipse.osgi.container.ModuleRevisionBuilder.GenericInfo;
+import org.eclipse.osgi.container.builders.OSGiManifestBuilderFactory;
+import org.eclipse.tycho.TychoConstants;
+import org.eclipse.tycho.artifacts.ArtifactVersion;
+import org.eclipse.tycho.artifacts.ArtifactVersionProvider;
+import org.eclipse.tycho.core.osgitools.BundleReader;
+import org.eclipse.tycho.core.osgitools.OsgiManifest;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.VersionRange;
+import org.osgi.framework.namespace.PackageNamespace;
+
+/**
+ * A {@link ArtifactVersionProvider} that checks maven repository for possible
+ * candidates
+ */
+@Named
+@SessionScoped
+public class MavenArtifactVersionProvider implements ArtifactVersionProvider {
+
+	private MavenSession session;
+	private RepositorySystem repoSystem;
+	private BundleReader bundleReader;
+
+	@Inject
+	public MavenArtifactVersionProvider(MavenSession session, RepositorySystem repoSystem, BundleReader bundleReader) {
+		this.session = session;
+		this.repoSystem = repoSystem;
+		this.bundleReader = bundleReader;
+	}
+
+	@Override
+	public Stream<ArtifactVersion> getPackageVersions(IInstallableUnit unit, String packageName,
+			VersionRange versionRange, MavenProject mavenProject) {
+		String groupId = unit.getProperty(TychoConstants.PROP_GROUP_ID);
+		String artifactId = unit.getProperty(TychoConstants.PROP_ARTIFACT_ID);
+		String classifier = unit.getProperty(TychoConstants.PROP_CLASSIFIER);
+		if (groupId != null && artifactId != null && !"sources".equals(classifier)) {
+			List<RemoteRepository> repositories = RepositoryUtils.toRepos(mavenProject.getRemoteArtifactRepositories());
+			DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId, classifier, "jar", "[0,)");
+			// as we have no mean for a package version in maven we can only fetch all
+			// versions an check if the match
+			VersionRangeRequest rangeRequest = new VersionRangeRequest(artifact, repositories, "");
+			try {
+				VersionRangeResult range = repoSystem.resolveVersionRange(session.getRepositorySession(), rangeRequest);
+				// now we sort from highest > lowest version
+				List<Version> versions = range.getVersions().stream()
+						.sorted(Comparator.<Version>naturalOrder().reversed()).toList();
+				return versions.stream()
+						.map(v -> new MavenPackageArtifactVersion(artifact, v, packageName, repositories))
+						.filter(mav -> mav.getVersion() != null)
+						// and drop all until we find a matching version
+						.dropWhile(mav -> !versionRange.includes(mav.getVersion()))
+						// and stop when we find the first non matching version
+						.takeWhile(mav -> versionRange.includes(mav.getVersion()))
+						// cast to make compiler happy
+						.map(ArtifactVersion.class::cast);
+			} catch (VersionRangeResolutionException e) {
+				// can't provide any useful data then...
+			}
+		}
+		return Stream.empty();
+	}
+
+	private class MavenPackageArtifactVersion implements ArtifactVersion {
+
+		private Artifact artifact;
+		private List<RemoteRepository> repositories;
+		private Path path;
+		private String packageName;
+		private org.osgi.framework.Version packageVersion;
+
+		public MavenPackageArtifactVersion(Artifact artifact, Version version, String packageName,
+				List<RemoteRepository> repositories) {
+			this.artifact = new DefaultArtifact(artifact.getGroupId(), artifact.getArtifactId(),
+					artifact.getExtension(), version.toString());
+			this.packageName = packageName;
+			this.repositories = repositories;
+		}
+
+		@Override
+		public Path getArtifact() {
+			try {
+				ArtifactRequest request = new ArtifactRequest(artifact, repositories, "");
+				ArtifactResult result = repoSystem.resolveArtifact(session.getRepositorySession(), request);
+				path = result.getArtifact().getFile().toPath();
+			} catch (ArtifactResolutionException e) {
+			}
+			return path;
+		}
+
+		@Override
+		public org.osgi.framework.Version getVersion() {
+			if (packageVersion == null) {
+				ModuleRevisionBuilder builder = readOSGiInfo(getArtifact());
+				if (builder != null) {
+					List<GenericInfo> capabilities = builder.getCapabilities(PackageNamespace.PACKAGE_NAMESPACE);
+					for (GenericInfo info : capabilities) {
+						Map<String, Object> attributes = info.getAttributes();
+						if (packageName.equals(attributes.get(PackageNamespace.PACKAGE_NAMESPACE))) {
+							packageVersion = (org.osgi.framework.Version) attributes.getOrDefault(
+									PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE,
+									org.osgi.framework.Version.emptyVersion);
+						}
+					}
+				}
+			}
+			return packageVersion;
+		}
+
+		@Override
+		public String toString() {
+			return getVersion() + " (maven artifact " + artifact + ")";
+		}
+
+		@Override
+		public String getProvider() {
+			ModuleRevisionBuilder info = readOSGiInfo(getArtifact());
+			if (info != null) {
+				return info.getSymbolicName() + " " + info.getVersion();
+			}
+			return null;
+		}
+
+	}
+
+	private ModuleRevisionBuilder readOSGiInfo(Path path) {
+		if (path != null) {
+			OsgiManifest manifest = bundleReader.loadManifest(path.toFile());
+			try {
+				return OSGiManifestBuilderFactory.createBuilder(manifest.getHeaders());
+			} catch (BundleException e) {
+			}
+		}
+		return null;
+	}
+
+}
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<String, Set<String>> 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<Integer, RepositoryImpl> repositories;
-
-  private Repository[] repositoriesArray;
-
-  private Map<String, Set<String>> 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<Object, Object> 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<Object, Object> 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<RepositoryImpl, List<Integer>> composedRepositories = new HashMap<>();
-        for (int id = 1; id <= repositoryCount; id++)
-        {
-          RepositoryImpl repository = new RepositoryImpl(stream, id, composedRepositories);
-          repositories.put(id, repository);
-        }
-
-        for (Map.Entry<RepositoryImpl, List<Integer>> 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<String, Set<String>> getCapabilities()
-  {
-    initCapabilities();
-    return Collections.unmodifiableMap(capabilitiesMap);
-  }
-
-  @Override
-  public Map<Repository, Set<Version>> lookupCapabilities(String namespace, String name)
-  {
-    Map<Repository, Set<Version>> 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<Version> 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<Repository, Set<Version>> generateCapabilitiesFromComposedRepositories(Map<Repository, Set<Version>> capabilitiesFromSimpleRepositories)
-  {
-    Map<Repository, Set<Version>> capabilities = new HashMap<>();
-    for (Map.Entry<Repository, Set<Version>> entry : capabilitiesFromSimpleRepositories.entrySet())
-    {
-      Repository repository = entry.getKey();
-      Set<Version> versions = entry.getValue();
-      recurseComposedRepositories(capabilities, repository, versions);
-    }
-
-    return capabilities;
-  }
-
-  private void recurseComposedRepositories(Map<Repository, Set<Version>> capabilities, Repository repository, Set<Version> versions)
-  {
-    for (Repository composite : repository.getComposites())
-    {
-      Set<Version> 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<RepositoryImpl, List<Integer>> 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<Integer> 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<Integer, RepositoryImpl> repositories;
+
+	private Repository[] repositoriesArray;
+
+	private Map<String, Set<String>> 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<Object, Object> 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<Object, Object> 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<RepositoryImpl, List<Integer>> composedRepositories = new HashMap<>();
+				for (int id = 1; id <= repositoryCount; id++) {
+					RepositoryImpl repository = new RepositoryImpl(stream, id, composedRepositories);
+					repositories.put(id, repository);
+				}
+
+				for (Map.Entry<RepositoryImpl, List<Integer>> 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<String, Set<String>> getCapabilities() {
+		initCapabilities();
+		return Collections.unmodifiableMap(capabilitiesMap);
+	}
+
+	@Override
+	public Map<Repository, Set<Version>> lookupCapabilities(String namespace, String name) {
+		initCapabilities();
+		Map<Repository, Set<Version>> 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<Version> 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<Repository, Set<Version>> generateCapabilitiesFromComposedRepositories(
+			Map<Repository, Set<Version>> capabilitiesFromSimpleRepositories) {
+		Map<Repository, Set<Version>> capabilities = new HashMap<>();
+		for (Map.Entry<Repository, Set<Version>> entry : capabilitiesFromSimpleRepositories.entrySet()) {
+			Repository repository = entry.getKey();
+			Set<Version> versions = entry.getValue();
+			recurseComposedRepositories(capabilities, repository, versions);
+		}
+
+		return capabilities;
+	}
+
+	private void recurseComposedRepositories(Map<Repository, Set<Version>> capabilities, Repository repository,
+			Set<Version> versions) {
+		for (Repository composite : repository.getComposites()) {
+			Set<Version> 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<RepositoryImpl, List<Integer>> 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<Integer> 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 <K, V> boolean add(Map<K, Set<V>> map, K key, V value) {
+		Set<V> set = getSet(map, key);
+		return set.add(value);
+	}
+
+	public static <K, V> Set<V> getSet(Map<K, Set<V>> map, K key) {
+		Set<V> set = map.get(key);
+		if (set == null) {
+			set = new LinkedHashSet<>();
+			map.put(key, set);
+		}
+
+		return set;
+	}
+
+	public static void close(Closeable closeable) throws RuntimeException {
+		try {
+			if (closeable != null) {
+				closeable.close();
+			}
+		} catch (IOException ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
+	public static Exception closeSilent(Closeable closeable) {
+		try {
+			if (closeable != null) {
+				closeable.close();
+			}
+
+			return null;
+		} catch (Exception ex) {
+			return ex;
+		}
+	}
+
+	public static long copy(InputStream input, OutputStream output) {
+		byte buffer[] = new byte[8192];
+		try {
+			long length = 0;
+			int n;
+
+			while ((n = input.read(buffer)) != -1) {
+				output.write(buffer, 0, n);
+				length += n;
+			}
+
+			return length;
+		} catch (IOException ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
+	public static boolean isEmpty(String str) {
+		return str == null || str.length() == 0;
+	}
+
 }
\ No newline at end of file
diff --git a/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java b/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java
index f2a463844b..9c0f789faa 100644
--- a/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java
+++ b/tycho-core/src/main/java/org/eclipse/tycho/core/resolver/target/ArtifactMatcher.java
@@ -14,11 +14,13 @@
 package org.eclipse.tycho.core.resolver.target;
 
 import java.util.AbstractMap.SimpleEntry;
+import java.util.Collection;
 import java.util.Comparator;
 import java.util.LinkedHashSet;
 import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Stream;
 
 import org.eclipse.equinox.p2.metadata.IInstallableUnit;
 import org.eclipse.equinox.p2.metadata.IProvidedCapability;
@@ -27,6 +29,7 @@
 import org.eclipse.equinox.p2.publisher.eclipse.Feature;
 import org.eclipse.equinox.p2.publisher.eclipse.FeatureEntry;
 import org.eclipse.equinox.p2.publisher.eclipse.FeaturesAction;
+import org.eclipse.equinox.p2.query.CollectionResult;
 import org.eclipse.equinox.p2.query.IQuery;
 import org.eclipse.equinox.p2.query.IQueryResult;
 import org.eclipse.equinox.p2.query.QueryUtil;
@@ -53,16 +56,31 @@ public static IInstallableUnit resolveReference(String type, String id, VersionR
             return ius.iterator().next();
         }
         if (PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE.equals(type)) {
-            return ius.stream().flatMap(iu -> getPackageVersion(iu, id).map(v -> new SimpleEntry<>(iu, v)).stream())
-                    .max((o1, o2) -> {
-                        return o1.getValue().compareTo(o2.getValue());
-                    }).map(Entry::getKey).orElse(null);
+            return findPackage(id, ius).orElse(null);
         } else {
             return ius.iterator().next();
         }
     }
 
-    private static Optional<Version> getPackageVersion(IInstallableUnit unit, String packageName) {
+    public static Optional<IInstallableUnit> findPackage(String packageName, Collection<IInstallableUnit> ius) {
+        return findPackage(packageName, new CollectionResult<>(ius), null);
+
+    }
+
+    public static Optional<IInstallableUnit> findPackage(String packageName, IQueryResult<IInstallableUnit> query,
+            Version version) {
+        Stream<SimpleEntry<IInstallableUnit, Version>> 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<Version> getPackageVersion(IInstallableUnit unit, String packageName) {
 
         return unit.getProvidedCapabilities().stream()
                 .filter(capability -> PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE.equals(capability.getNamespace()))
diff --git a/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java
new file mode 100644
index 0000000000..894b9459c0
--- /dev/null
+++ b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersion.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Copyright (c) 2024 Christoph Läubrich and others.
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christoph Läubrich - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.tycho.artifacts;
+
+import java.nio.file.Path;
+
+import org.osgi.framework.Version;
+
+public interface ArtifactVersion {
+
+    Path getArtifact();
+
+    Version getVersion();
+
+    String getProvider();
+}
diff --git a/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java
new file mode 100644
index 0000000000..cf41de1764
--- /dev/null
+++ b/tycho-spi/src/main/java/org/eclipse/tycho/artifacts/ArtifactVersionProvider.java
@@ -0,0 +1,26 @@
+/*******************************************************************************
+ * Copyright (c) 2024 Christoph Läubrich and others.
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Christoph Läubrich - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.tycho.artifacts;
+
+import java.util.stream.Stream;
+
+import org.apache.maven.project.MavenProject;
+import org.eclipse.equinox.p2.metadata.IInstallableUnit;
+import org.osgi.framework.VersionRange;
+
+public interface ArtifactVersionProvider {
+
+    Stream<ArtifactVersion> getPackageVersions(IInstallableUnit unit, String packageName, VersionRange versionRange,
+            MavenProject mavenProject);
+
+}