diff --git a/features/org.eclipse.pde-feature/feature.xml b/features/org.eclipse.pde-feature/feature.xml
index e075368b7d..790c3917a7 100644
--- a/features/org.eclipse.pde-feature/feature.xml
+++ b/features/org.eclipse.pde-feature/feature.xml
@@ -28,6 +28,7 @@
+
@@ -35,6 +36,11 @@
+
+
+
+
+
T getAdapter(Object adaptableObject, Class adapterType) {
@@ -46,6 +46,51 @@ public class BndPluginAdapter implements IAdapterFactory {
}
```
+### The `Bndrun` adapter
+
+For some cases one would need to get hold of a `Bndrun` (e.g. for launching tests), to support this similar is needed as to create a template that then can further be customized.
+For this use-case it is required to provide an adapter that can transform an (Eclipse) `IProject` into a (bndlib) `Bndrun` that is initialized with the runrequires contain the identity
+of the project and the workspace be able to resolve such bundle.
+
+```
+@Component
+@AdapterTypes(adaptableClass = IProject.class, adapterNames = Bndrun.class)
+public class BndRunAdapter implements IAdapterFactory {
+
+ @Override
+ public T getAdapter(Object adaptableObject, Class adapterType) {
+ if (adaptableObject instanceof IProject eclipseProject) {
+ if (adapterType == Project.class) {
+ //... here we need to determine if the project is managed by our tooling, e.g. it is a PDE, Bndtool, Maven, ... backed project
+ if (/*... is relevant ... */) {
+ Workspace workspace = .... find the workspace according to your tooling implementation
+ Path base = workspace.getBase().toPath();
+ Path file = Files.createTempFile(base, project.getName(), ".bndrun");
+ file.toFile().deleteOnExit();
+ Files.createDirectories(file.getParent());
+ Properties properties = new Properties();
+ String bsn = ... derive the symbolic name ...
+ String version = ... derive the version name ...
+ properties.setProperty(Constants.RUNREQUIRES, String.format("bnd.identity; id=%s;version=%s", bsn, version));
+ //maybe more customizations....
+ Bndrun bndrun = Bndrun.createBndrun(workspace, file.toFile());
+ //make sure the file is deleted when the bndrun is closed...
+ bndrun.addClose(new AutoCloseable() {
+
+ @Override
+ public void close() throws Exception {
+ Files.delete(file);
+ }
+ });
+ }
+ }
+ }
+ return null;
+ }
+
+}
+```
+
## Available components
Beside some integration stuff (e.g. enable to [discover bndlib plugins](https://github.com/eclipse-pde/eclipse.pde/blob/master/ui/org.eclipse.pde.bnd.ui/src/org/eclipse/pde/bnd/ui/internal/Auxiliary.java) inside an OSGi runtime)
diff --git a/ui/org.eclipse.pde.core/META-INF/MANIFEST.MF b/ui/org.eclipse.pde.core/META-INF/MANIFEST.MF
index fa63280643..f96b378f97 100644
--- a/ui/org.eclipse.pde.core/META-INF/MANIFEST.MF
+++ b/ui/org.eclipse.pde.core/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %name
Bundle-SymbolicName: org.eclipse.pde.core; singleton:=true
-Bundle-Version: 3.20.100.qualifier
+Bundle-Version: 3.21.0.qualifier
Bundle-Activator: org.eclipse.pde.internal.core.PDECore
Bundle-Vendor: %provider-name
Bundle-Localization: plugin
@@ -48,6 +48,7 @@ Export-Package:
org.eclipse.pde.ui,
org.eclipse.pde.api.tools.tests,
org.eclipse.pde.api.tools",
+ org.eclipse.pde.internal.core.osgitest;x-internal:=true,
org.eclipse.pde.internal.core.plugin;
x-friends:="org.eclipse.pde.ui,
org.eclipse.pde.ds.ui,
@@ -97,6 +98,7 @@ Import-Package: aQute.bnd.build;version="[4.4.0,5.0.0)",
aQute.bnd.service.progress;version="[1.3.0,2.0.0)",
aQute.bnd.version;version="[2.2.0,3.0.0)",
aQute.service.reporter;version="[1.2.0,2.0.0)",
+ biz.aQute.resolve;version="[9.1.0,10.0.0)",
org.bndtools.versioncontrol.ignores.manager.api;version="[1.0.0,2.0.0)",
org.eclipse.equinox.internal.p2.publisher.eclipse,
org.eclipse.equinox.p2.publisher,
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/core/build/IBuildEntry.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/core/build/IBuildEntry.java
index 0d7da2ef76..3da5d852ae 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/core/build/IBuildEntry.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/core/build/IBuildEntry.java
@@ -27,39 +27,39 @@ public interface IBuildEntry extends IWritable {
/**
* A property name for changes to the 'name' field.
*/
- public static final String P_NAME = "name"; //$NON-NLS-1$
+ String P_NAME = "name"; //$NON-NLS-1$
/**
* The prefix for any key denoting the source folders that
* should be compiled into a JAR. The suffix will
* be the name of the JAR.
*/
- public static final String JAR_PREFIX = "source."; //$NON-NLS-1$
+ String JAR_PREFIX = "source."; //$NON-NLS-1$
/**
* The prefix for any key denoting output folders for a particular
* JAR. The suffix will be the name of the JAR.
*/
- public static final String OUTPUT_PREFIX = "output."; //$NON-NLS-1$
+ String OUTPUT_PREFIX = "output."; //$NON-NLS-1$
/**
* The name of the key that lists all the folders and files
* to be included in the binary build.
*/
- public static final String BIN_INCLUDES = "bin.includes"; //$NON-NLS-1$
+ String BIN_INCLUDES = "bin.includes"; //$NON-NLS-1$
/**
* The name of the key that lists all the folders and files
* to be included in the source build.
*/
- public static final String SRC_INCLUDES = "src.includes"; //$NON-NLS-1$
+ String SRC_INCLUDES = "src.includes"; //$NON-NLS-1$
/**
* The name of the key that declares extra library entries to be added
* to the class path at build time only..
*/
- public static final String JARS_EXTRA_CLASSPATH = "jars.extra.classpath"; //$NON-NLS-1$
+ String JARS_EXTRA_CLASSPATH = "jars.extra.classpath"; //$NON-NLS-1$
/**
* The name of the key that declares additional plug-in dependencies to augment development classpath
*
* @since 3.2
*/
- public static final String SECONDARY_DEPENDENCIES = "additional.bundles"; //$NON-NLS-1$
+ String SECONDARY_DEPENDENCIES = "additional.bundles"; //$NON-NLS-1$
/**
* Adds the token to the list of token for this entry.
@@ -89,6 +89,19 @@ public interface IBuildEntry extends IWritable {
*/
String[] getTokens();
+ /**
+ * Returns the first token for this entry
+ *
+ * @since 3.21
+ */
+ default String getFirstToken() {
+ String[] tokens = getTokens();
+ if (tokens == null || tokens.length == 0) {
+ return null;
+ }
+ return tokens[0];
+ }
+
/**
* Returns true if the provided token exists in this entry.
* @param token the string token to look for
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ClasspathUtilCore.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ClasspathUtilCore.java
index ee2d779b46..1f2b92432c 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ClasspathUtilCore.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ClasspathUtilCore.java
@@ -152,9 +152,14 @@ protected static Stream classpathEntriesForModelBundle(IPluginM
}
boolean isJarShape = new File(location).isFile();
IPluginLibrary[] libraries = model.getPluginBase().getLibraries();
- if (isJarShape || libraries.length == 0) {
+ if (isJarShape) {
return Stream.of(getEntryForPath(IPath.fromOSString(location), extra));
}
+ if (libraries.length == 0) {
+ List entries = new ArrayList<>();
+ PDEClasspathContainer.addExternalPlugin(model, null, entries);
+ return entries.stream();
+ }
return Arrays.stream(libraries).filter(library -> !IPluginLibrary.RESOURCE.equals(library.getType()))
.map(library -> {
String name = library.getName();
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PDEClasspathContainer.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PDEClasspathContainer.java
index 3615375ebf..182617b345 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PDEClasspathContainer.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PDEClasspathContainer.java
@@ -30,9 +30,13 @@
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.osgi.service.resolver.BundleDescription;
+import org.eclipse.pde.core.build.IBuild;
+import org.eclipse.pde.core.build.IBuildEntry;
+import org.eclipse.pde.core.plugin.IPluginBase;
import org.eclipse.pde.core.plugin.IPluginLibrary;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.PluginRegistry;
+import org.eclipse.pde.internal.core.build.ExternalBuildModel;
import org.osgi.resource.Resource;
public class PDEClasspathContainer {
@@ -78,7 +82,8 @@ public static IClasspathEntry[] getExternalEntries(IPluginModelBase model) {
}
protected static void addExternalPlugin(IPluginModelBase model, List rules, List entries) {
- boolean isJarShape = new File(model.getInstallLocation()).isFile();
+ File file = new File(model.getInstallLocation());
+ boolean isJarShape = file.isFile();
if (isJarShape) {
IPath srcPath = ClasspathUtilCore.getSourceAnnotation(model, ".", isJarShape); //$NON-NLS-1$
if (srcPath == null) {
@@ -93,14 +98,19 @@ protected static void addExternalPlugin(IPluginModelBase model, List rules
addLibraryEntry(path, path, rules, getClasspathAttributes(model), entries);
}
} else {
- IPluginLibrary[] libraries = model.getPluginBase().getLibraries();
+ IPluginBase pluginBase = model.getPluginBase();
+ IPluginLibrary[] libraries = pluginBase.getLibraries();
if (libraries.length == 0) {
- // If there are no libraries, assume the root of the plug-in is the library '.'
- IPath srcPath = ClasspathUtilCore.getSourceAnnotation(model, ".", isJarShape); //$NON-NLS-1$
- if (srcPath == null) {
- srcPath = IPath.fromOSString(model.getInstallLocation());
+ if (!addEntriesFromHostEclipse(model, rules, entries, file)) {
+ // If there are no libraries, assume the root of the plug-in
+ // is the library '.'
+ IPath srcPath = ClasspathUtilCore.getSourceAnnotation(model, ".", isJarShape); //$NON-NLS-1$
+ if (srcPath == null) {
+ srcPath = IPath.fromOSString(model.getInstallLocation());
+ }
+ addLibraryEntry(IPath.fromOSString(model.getInstallLocation()), srcPath, rules,
+ getClasspathAttributes(model), entries);
}
- addLibraryEntry(IPath.fromOSString(model.getInstallLocation()), srcPath, rules, getClasspathAttributes(model), entries);
} else {
for (IPluginLibrary library : libraries) {
if (IPluginLibrary.RESOURCE.equals(library.getType())) {
@@ -125,6 +135,41 @@ protected static void addExternalPlugin(IPluginModelBase model, List rules
}
}
+ protected static boolean addEntriesFromHostEclipse(IPluginModelBase model, List rules,
+ List entries, File file) {
+ boolean hasBuildEntries = false;
+ // if build properties exits in folder than this is a local
+ // project from an Eclipse started from a workspace
+ if (new File(file, ICoreConstants.BUILD_FILENAME_DESCRIPTOR).isFile()) {
+ IBuild build = new ExternalBuildModel(model.getInstallLocation()).getBuild();
+ IBuildEntry[] buildEntries = build.getBuildEntries();
+ for (IBuildEntry entry : buildEntries) {
+ String name = entry.getName();
+ if (name.startsWith(IBuildEntry.OUTPUT_PREFIX)) {
+ String folder = entry.getFirstToken();
+ if (folder != null) {
+ File outputFolder = new File(file, folder);
+ if (outputFolder.isDirectory()) {
+ hasBuildEntries = true;
+ IBuildEntry sourceEntry = build.getEntry(IBuildEntry.JAR_PREFIX
+ + name.substring(IBuildEntry.OUTPUT_PREFIX.length()));
+ IPath srcPath = null;
+ if (sourceEntry != null) {
+ String firstToken = sourceEntry.getFirstToken();
+ if (firstToken != null) {
+ srcPath = IPath.fromOSString(new File(file, firstToken).getAbsolutePath());
+ }
+ }
+ addLibraryEntry(IPath.fromOSString(outputFolder.getAbsolutePath()), srcPath, rules,
+ getClasspathAttributes(model), entries);
+ }
+ }
+ }
+ }
+ }
+ return hasBuildEntries;
+ }
+
protected static void addLibraryEntry(IPath path, IPath srcPath, List rules, IClasspathAttribute[] attributes,
List entries) {
IClasspathEntry entry = null;
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/BndProjectManager.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/BndProjectManager.java
index b1974a515b..86d490dcbc 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/BndProjectManager.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/BndProjectManager.java
@@ -14,11 +14,15 @@
package org.eclipse.pde.internal.core.bnd;
import java.io.File;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -33,6 +37,8 @@
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.osgi.service.resolver.BundleDescription;
+import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.internal.core.PDECore;
import org.eclipse.pde.internal.core.annotations.OSGiAnnotationsClasspathContributor;
import org.eclipse.pde.internal.core.natures.BndProject;
@@ -44,6 +50,7 @@
import aQute.bnd.build.Workspace;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Processor;
+import biz.aQute.resolve.Bndrun;
public class BndProjectManager {
@@ -96,8 +103,13 @@ static synchronized Workspace getWorkspace() throws Exception {
if (workspace == null) {
try (Processor run = new Processor()) {
run.setProperty(Constants.STANDALONE, TRUE);
- IPath path = PDECore.getDefault().getStateLocation().append(Project.BNDCNF);
- workspace = Workspace.createStandaloneWorkspace(run, path.toFile().toURI());
+ IPath path = PDECore.getDefault().getStateLocation().append(Constants.DEFAULT_BND_EXTENSION)
+ .append(Project.BNDCNF);
+ File file = path.toFile();
+ file.mkdirs();
+ workspace = Workspace.createStandaloneWorkspace(run, file.toURI());
+ workspace.setBase(file.getParentFile());
+ workspace.setBuildDir(file);
workspace.set("workspaceName", Messages.BndProjectManager_WorkspaceName); //$NON-NLS-1$
workspace.set("workspaceDescription", Messages.BndProjectManager_WorkspaceDescription); //$NON-NLS-1$
workspace.addBasicPlugin(TargetRepository.getTargetRepository());
@@ -111,8 +123,8 @@ static synchronized Workspace getWorkspace() throws Exception {
return workspace;
}
- static void publishContainerEntries(List entries, Collection containers,
- boolean isTest, Set mapped) {
+ static void publishContainerEntries(List entries, Collection containers, boolean isTest,
+ Set mapped) {
for (Container container : containers) {
TYPE type = container.getType();
if (type == TYPE.PROJECT) {
@@ -159,4 +171,37 @@ public static List getClasspathEntries(Project project, IWorksp
return entries;
}
+ public static Optional createBndrun(IProject project) throws Exception {
+ if (BndProject.isBndProject(project) || PluginProject.isPluginProject(project)) {
+ IPluginModelBase model = PDECore.getDefault().getModelManager().findModel(project);
+ if (model == null) {
+ return Optional.empty();
+ }
+ Workspace workspace = getWorkspace();
+ Path base = workspace.getBase().toPath();
+ String name = project.getName();
+ Path file = Files.createTempFile(base, name, Constants.DEFAULT_BNDRUN_EXTENSION);
+ file.toFile().deleteOnExit();
+ Files.createDirectories(file.getParent());
+ Properties properties = new Properties();
+ BundleDescription description = model.getBundleDescription();
+ properties.setProperty(Constants.RUNREQUIRES, String.format("bnd.identity;id=%s;version=%s", //$NON-NLS-1$
+ description.getSymbolicName(), description.getVersion()));
+ try (OutputStream stream = Files.newOutputStream(file)) {
+ properties.store(stream,
+ String.format("Bndrun generated by PDE for project %s", name)); //$NON-NLS-1$
+ }
+ Bndrun bndrun = new PdeBndrun(workspace, file.toFile(), String.format("Bndrun for %s", name)); //$NON-NLS-1$
+ bndrun.addClose(new AutoCloseable() {
+
+ @Override
+ public void close() throws Exception {
+ Files.delete(file);
+ }
+ });
+ return Optional.of(bndrun);
+ }
+ return Optional.empty();
+ }
+
}
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBndAdapter.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBndAdapter.java
index b7730d2041..2d3886171f 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBndAdapter.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBndAdapter.java
@@ -23,9 +23,11 @@
import org.osgi.service.component.annotations.ReferencePolicy;
import aQute.bnd.build.Project;
+import biz.aQute.resolve.Bndrun;
@Component(service = IAdapterFactory.class)
-@AdapterTypes(adaptableClass = IProject.class, adapterNames = { Project.class, VersionControlIgnoresManager.class })
+@AdapterTypes(adaptableClass = IProject.class, adapterNames = { Project.class, VersionControlIgnoresManager.class,
+ Bndrun.class })
public class PdeBndAdapter implements IAdapterFactory {
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
@@ -33,8 +35,8 @@ public class PdeBndAdapter implements IAdapterFactory {
@Override
public T getAdapter(Object adaptableObject, Class adapterType) {
- if (adapterType == Project.class) {
- if (adaptableObject instanceof IProject project) {
+ if (adaptableObject instanceof IProject project) {
+ if (adapterType == Project.class) {
try {
return adapterType.cast(BndProjectManager.getBndProject(project).orElse(null));
} catch (Exception e) {
@@ -42,9 +44,17 @@ public T getAdapter(Object adaptableObject, Class adapterType) {
return null;
}
}
- }
- if (adapterType == VersionControlIgnoresManager.class) {
- return adapterType.cast(versionControlIgnoresManager);
+ if (adapterType == VersionControlIgnoresManager.class) {
+ return adapterType.cast(versionControlIgnoresManager);
+ }
+ if (adapterType == Bndrun.class) {
+ try {
+ return adapterType.cast(BndProjectManager.createBndrun(project).orElse(null));
+ } catch (Exception e) {
+ // can't adapt then...
+ return null;
+ }
+ }
}
return null;
}
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBndrun.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBndrun.java
new file mode 100644
index 0000000000..7a39440817
--- /dev/null
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBndrun.java
@@ -0,0 +1,113 @@
+/*******************************************************************************
+ * 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.pde.internal.core.bnd;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+
+import aQute.bnd.build.Container;
+import aQute.bnd.build.Container.TYPE;
+import aQute.bnd.build.DownloadBlocker;
+import aQute.bnd.build.Workspace;
+import aQute.bnd.osgi.Verifier;
+import aQute.bnd.service.RepositoryPlugin;
+import aQute.bnd.service.Strategy;
+import aQute.bnd.version.Version;
+import aQute.bnd.version.VersionRange;
+import biz.aQute.resolve.Bndrun;
+
+class PdeBndrun extends Bndrun {
+
+ private String name;
+
+ PdeBndrun(Workspace workspace, File propertiesFile, String name) throws Exception {
+ super(workspace, propertiesFile);
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Container getBundle(String bsn, String range, Strategy strategy, Map attrs)
+ throws Exception {
+ Container bundle = super.getBundle(bsn, range, strategy, attrs);
+ if (bundle == null || bundle.getType() == TYPE.ERROR) {
+ // workaround for https://github.com/bndtools/bnd/issues/6481
+ // and https://github.com/bndtools/bnd/issues/6482
+ // derived from the super class
+ List plugins = getPlugins(RepositoryPlugin.class);
+ range = Objects.requireNonNullElse(range, "0"); //$NON-NLS-1$
+ attrs = Objects.requireNonNullElse(attrs, Collections.emptyMap());
+ if (strategy == Strategy.EXACT) {
+ if (!Verifier.isVersion(range)) {
+ return bundle;
+ }
+ Version version = new Version(range);
+ for (RepositoryPlugin plugin : plugins) {
+ DownloadBlocker blocker = new DownloadBlocker(this);
+ File result = plugin.get(bsn, version, attrs, blocker);
+ if (result != null) {
+ return toContainer(bsn, range, attrs, result, blocker);
+ }
+ }
+ } else {
+ VersionRange versionRange = VERSION_ATTR_LATEST.equals(range) ? new VersionRange("0") //$NON-NLS-1$
+ : new VersionRange(range);
+ SortedMap versions = new TreeMap<>();
+ for (RepositoryPlugin plugin : plugins) {
+ try {
+ SortedSet vs = plugin.versions(bsn);
+ if (vs != null) {
+ for (Version v : vs) {
+ if (!versions.containsKey(v) && versionRange.includes(v)) {
+ versions.put(v, plugin);
+ }
+ }
+ }
+ } catch (UnsupportedOperationException ose) {
+ }
+ }
+ if (!versions.isEmpty()) {
+ Version provider = switch (strategy) {
+ case HIGHEST -> versions.lastKey();
+ case LOWEST -> versions.firstKey();
+ case EXACT -> null;
+ };
+ if (provider != null) {
+ RepositoryPlugin repo = versions.get(provider);
+ if (repo != null) {
+ String version = provider.toString();
+ DownloadBlocker blocker = new DownloadBlocker(this);
+ File result = repo.get(bsn, provider, attrs, blocker);
+ if (result != null) {
+ return toContainer(bsn, version, attrs, result, blocker);
+ }
+ }
+ }
+ }
+ }
+ }
+ return bundle;
+ }
+
+}
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBuildJar.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBuildJar.java
new file mode 100644
index 0000000000..a432d8d71b
--- /dev/null
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeBuildJar.java
@@ -0,0 +1,96 @@
+/*******************************************************************************
+ * 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.pde.internal.core.bnd;
+
+import java.util.Objects;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.pde.core.build.IBuild;
+import org.eclipse.pde.core.build.IBuildEntry;
+import org.eclipse.pde.internal.core.ICoreConstants;
+import org.eclipse.pde.internal.core.build.WorkspaceBuildModel;
+
+import aQute.bnd.osgi.Jar;
+
+/**
+ * This jar behaves like a pde-build on the java project that is including all
+ * compiled class files from the binaries, and evaluate the bin includes
+ */
+public class PdeBuildJar extends Jar {
+
+ public PdeBuildJar(IJavaProject javaProject) throws CoreException {
+ this(javaProject, false);
+ }
+
+ public PdeBuildJar(IJavaProject javaProject, boolean includeTest) throws CoreException {
+ super(javaProject.getProject().getName());
+ IProject project = javaProject.getProject();
+ IFile buildFile = project.getFile(ICoreConstants.BUILD_FILENAME_DESCRIPTOR);
+ if (buildFile.exists()) {
+ IBuild build = new WorkspaceBuildModel(buildFile).getBuild();
+ IBuildEntry[] buildEntries = build.getBuildEntries();
+ for (IBuildEntry entry : buildEntries) {
+ String name = entry.getName();
+ if (name.startsWith(IBuildEntry.OUTPUT_PREFIX)) {
+ String folder = entry.getFirstToken();
+ if (folder != null) {
+ IFolder outputFolder = project.getFolder(folder);
+ if (outputFolder.exists()) {
+ // TODO if the library is not '.' then it should
+ // actually become an embedded jar!
+ include(outputFolder, ""); //$NON-NLS-1$
+ }
+ }
+ }
+ }
+ IBuildEntry entry = build.getEntry(IBuildEntry.BIN_INCLUDES);
+ if (entry != null) {
+ // TODO
+ }
+ }
+ if (includeTest) {
+ // TODO
+ IClasspathEntry[] classpath = javaProject.getRawClasspath();
+ IPath outputLocation = javaProject.getOutputLocation();
+ for (IClasspathEntry entry : classpath) {
+ System.out.println(entry);
+ if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE && entry.isTest()) {
+ IPath testOutput = Objects.requireNonNullElse(entry.getOutputLocation(), outputLocation);
+ System.out.println(testOutput);
+ IFolder otherOutputFolder = project.getWorkspace().getRoot().getFolder(testOutput);
+ include(otherOutputFolder, ""); //$NON-NLS-1$
+ }
+ }
+ }
+
+ }
+
+ private void include(IFolder folder, String prefix) throws CoreException {
+ for (IResource resource : folder.members()) {
+ if (resource instanceof IFile file) {
+ putResource(prefix + file.getName(), new FileResource(file));
+ } else if (resource instanceof IFolder subfolder) {
+ include(subfolder, prefix + subfolder.getName() + "/"); //$NON-NLS-1$
+ }
+ }
+ }
+
+}
diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/TargetRepository.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/TargetRepository.java
index f4f75be900..c906e1f112 100644
--- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/TargetRepository.java
+++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/TargetRepository.java
@@ -57,6 +57,7 @@
import org.osgi.service.repository.RepositoryContent;
import aQute.bnd.osgi.Instruction;
+import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.repository.BaseRepository;
import aQute.bnd.osgi.resource.CapReqBuilder;
import aQute.bnd.osgi.resource.ResourceUtils;
@@ -73,6 +74,7 @@ private TargetRepository() {
@Override
public File get(String bsn, aQute.bnd.version.Version version, Map properties,
DownloadListener... listeners) throws Exception {
+ System.out.println("GET: " + bsn);
Optional description = getTargetPlatformState()
.map(state -> state.getBundle(bsn, convert(version)));
if (description.isEmpty()) {
@@ -100,7 +102,28 @@ public File get(String bsn, aQute.bnd.version.Version version, Map
+
+
+
+
JUNIT5_BUNDLES = List.of("junit-jupiter-api", "junit-jupiter-engine", "junit-jupiter-migrationsupport", "junit-jupiter-params", "junit-platform-commons", "junit-platform-engine", "junit-platform-launcher", "junit-platform-runner", "junit-platform-suite-api", "junit-vintage-engine", "org.opentest4j", "org.apiguardian.api", "org.junit", "org.hamcrest");
+ private static final List JUNIT4_BUNDLES = List.of("org.junit", "org.hamcrest");
+
+ @Override
+ public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException {
+ System.out.println("######## Launch ######");
+ try {
+ IJavaProject javaProject = verifyJavaProject(configuration);
+ IProject project = javaProject.getProject();
+ try (Bndrun bndrun = Adapters.adapt(project, Bndrun.class)) {
+ if (bndrun == null) {
+ throw new CoreException(Status.error("Can't get bndrun template from project " + project.getName()));
+ }
+ bndrun.set(Constants.RUNTRACE, "true");
+ Path testJar = createTestJar(javaProject, bndrun);
+ FileSetRepository repository = new FileSetRepository("extra", List.of(testJar.toFile()));
+ bndrun.addBasicPlugin(repository);
+ setupFramework(bndrun);
+ setupRunRequires(configuration, bndrun);
+ setupJava(configuration, bndrun);
+ //TODO create a fragment to our project that imports all packages so we do not need any manual imports e.g.
+ // for junit (should be providede by the junit classpath container already!)
+ //also there might be other things used e.g. extra-classpath or additional bundles!
+ Collection runbundles = doResolve(bndrun);
+ for (Container container : runbundles) {
+ System.out.println("runbundle: " + container.getFile());
+ }
+ ProjectTester tester = bndrun.getProjectTester();
+ tester.getProjectLauncher().addRunBundle(testJar.toString());
+// tester.getProjectLauncher().addBasicPlugin(repository);
+ tester.addTest("osgitest.HelloOSGiTest");
+ //TODO add tests
+ tester.prepare();
+ int errors = tester.test();
+ printErrors(bndrun);
+ System.out.println("Errors: " + errors);
+ //TODO connect to the running process and show results in the junit view, for this we should migrate the code from bndtools about launching so it can be shared with pde
+ }
+ } catch (CoreException e) {
+ throw e;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private Path createTestJar(IJavaProject javaProject, Bndrun bndrun) throws Exception {
+ Path jarFile = Files.createTempFile("test-probe", ".jar");
+ bndrun.addClose(() -> Files.delete(jarFile));
+ try (PdeBuildJar jar = new PdeBuildJar(javaProject, true); Analyzer analyzer = new Analyzer(jar)) {
+ analyzer.setBundleVersion("1.0.0");
+ analyzer.setBundleSymbolicName("test-probe");
+ analyzer.set("Test-Cases", "osgitest.HelloOSGiTest");
+ Manifest manifest = jar.getManifest();
+ jar.remove(JarFile.MANIFEST_NAME);
+ jar.setManifest(new Manifest());
+ //TODO find all testclasses!
+ //TODO make fragment host if project is a fragment
+ jar.setManifest(analyzer.calcManifest());
+ jar.write(Files.newOutputStream(jarFile));
+ }
+ System.out.println("Testprobe written to " + jarFile);
+ return jarFile;
+ }
+
+ protected void printErrors(Report bndrun) {
+ //TODO throw on error?
+ bndrun.getErrors().forEach(err -> System.out.println("ERROR:" + err));
+ bndrun.getWarnings().forEach(err -> System.out.println("WARN:" + err));
+ }
+
+ protected void setupFramework(Bndrun bndrun) {
+ bndrun.setRunfw("org.eclipse.osgi"); //$NON-NLS-1$ //TODO fetch from selection
+ }
+
+ protected Collection doResolve(Bndrun bndrun) throws Exception {
+ String resolved = bndrun.resolve(false, false);
+ bndrun.set(Constants.RUNBUNDLES, resolved);
+ return bndrun.getRunbundles();
+ }
+
+ protected void setupJava(ILaunchConfiguration configuration, Bndrun bndrun) throws CoreException {
+ IVMInstall vmInstall = verifyVMInstall(configuration);
+ File executable = getJavaExecutable(vmInstall);
+ if (executable != null) {
+ bndrun.setProperty("java", executable.getAbsolutePath()); //$NON-NLS-1$
+ }
+ }
+
+ protected void setupRunRequires(ILaunchConfiguration configuration, Bndrun bndrun) {
+ Parameters runrequires = bndrun.getParameters(Constants.RUNREQUIRES);
+ String tester = addTestFramework(runrequires, configuration);
+ //TODO check if JUnit Classpath container is given
+ OSGiTestClasspathContributor.bundles(true).forEach(b -> {
+ runrequires.add(BND_IDENTITY, Attrs.create(ATTR_ID, b));
+ });
+ runrequires.add(BND_IDENTITY, Attrs.create(ATTR_ID, "test-probe"));
+ //TODO add extra bundles selected by the user!
+ StringBuilder builder = new StringBuilder();
+ runrequires.append(builder);
+ bndrun.setRunRequires(builder.toString());
+ bndrun.set(Constants.TESTER, tester);
+ }
+
+ @SuppressWarnings("restriction")
+ private String addTestFramework(Parameters runrequires, ILaunchConfiguration configuration) {
+ ITestKind testKind = JUnitLaunchConfigurationConstants.getTestRunnerKind(configuration);
+ if (TestKindRegistry.JUNIT5_TEST_KIND_ID.equals(testKind.getId())) {
+ runrequires.add(BND_IDENTITY, Attrs.create(ATTR_ID, TESTER_JUNIT_PLATFORM));
+ for (String bundle : JUNIT5_BUNDLES) {
+ runrequires.add(BND_IDENTITY, Attrs.create(ATTR_ID, bundle));
+ }
+ return TESTER_JUNIT_PLATFORM;
+ }
+ runrequires.add(BND_IDENTITY, Attrs.create(ATTR_ID, TESTER_CLASSIC));
+ for (String bundle : JUNIT4_BUNDLES) {
+ runrequires.add(BND_IDENTITY, Attrs.create(ATTR_ID, bundle));
+ }
+ return TESTER_CLASSIC;
+ }
+
+ @SuppressWarnings("restriction")
+ private static File getJavaExecutable(IVMInstall vmInstall) {
+ File installLocation = vmInstall.getInstallLocation();
+ if (installLocation != null) {
+ return StandardVMType.findJavaExecutable(installLocation);
+ }
+ return null;
+ }
+
+}
diff --git a/ui/org.eclipse.pde.ui/icons/osgi/osgi.png b/ui/org.eclipse.pde.ui/icons/osgi/osgi.png
new file mode 100644
index 0000000000..f688893ce9
Binary files /dev/null and b/ui/org.eclipse.pde.ui/icons/osgi/osgi.png differ
diff --git a/ui/org.eclipse.pde.ui/icons/osgi/osgi@2x.png b/ui/org.eclipse.pde.ui/icons/osgi/osgi@2x.png
new file mode 100644
index 0000000000..4aff61da91
Binary files /dev/null and b/ui/org.eclipse.pde.ui/icons/osgi/osgi@2x.png differ
diff --git a/ui/org.eclipse.pde.ui/plugin.xml b/ui/org.eclipse.pde.ui/plugin.xml
index cb7928f446..492108135e 100644
--- a/ui/org.eclipse.pde.ui/plugin.xml
+++ b/ui/org.eclipse.pde.ui/plugin.xml
@@ -687,6 +687,21 @@
id="org.eclipse.pde.ui.JunitLaunchImage">
+
+
+
+
+
+
+
+
+