diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeProjectAnalyzer.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeProjectAnalyzer.java index d1e54445e80..cb6a80233e3 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeProjectAnalyzer.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeProjectAnalyzer.java @@ -14,6 +14,11 @@ package org.eclipse.pde.internal.core.bnd; import java.io.File; +import java.io.InputStream; +import java.util.Collection; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.jar.Manifest; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.IPath; @@ -23,6 +28,8 @@ import org.eclipse.pde.internal.core.natures.PluginProject; import aQute.bnd.osgi.Analyzer; +import aQute.bnd.osgi.Jar; +import aQute.bnd.osgi.Resource; /** * An analyzer that is initialized by a {@link PdeProjectJar} and with the @@ -30,7 +37,14 @@ */ public class PdeProjectAnalyzer extends Analyzer { + public static final Set DEFAULT_COPY_HEADER = Set.of(BUNDLE_CLASSPATH, BUNDLE_SYMBOLICNAME, BUNDLE_VERSION, + BUNDLE_NAME, BUNDLE_LOCALIZATION, BUNDLE_VENDOR, BUNDLE_ACTIVATOR); + public PdeProjectAnalyzer(IProject project, boolean includeTest) throws Exception { + this(project, includeTest, DEFAULT_COPY_HEADER); + } + + public PdeProjectAnalyzer(IProject project, boolean includeTest, Collection copyHeader) throws Exception { super(includeTest ? new PdeTestProjectJar(project) : new PdeProjectJar(project)); set(NOEXTRAHEADERS, "true"); //$NON-NLS-1$ if (PluginProject.isJavaProject(project)) { @@ -47,6 +61,26 @@ public PdeProjectAnalyzer(IProject project, boolean includeTest) throws Exceptio } } } + Jar jar = getJar(); + Resource manifestResource = jar.getResource(JarFile.MANIFEST_NAME); + if (manifestResource != null) { + if (copyHeader.isEmpty()) { + jar.remove(JarFile.MANIFEST_NAME); + } else { + Manifest mf; + try (InputStream in = manifestResource.openInputStream()) { + mf = new Manifest(in); + } + jar.remove(JarFile.MANIFEST_NAME); + for (String header : copyHeader) { + String value = mf.getMainAttributes().getValue(header); + if (value != null) { + set(header, value); + } + } + } + + } } } diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeProjectJar.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeProjectJar.java index fc4d7f98f0a..6b7fd570fb5 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeProjectJar.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/bnd/PdeProjectJar.java @@ -13,17 +13,26 @@ *******************************************************************************/ package org.eclipse.pde.internal.core.bnd; +import java.util.HashMap; +import java.util.Map; + 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.resources.IWorkspaceRoot; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; 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 org.eclipse.pde.internal.core.natures.PluginProject; import aQute.bnd.osgi.Jar; +import aQute.bnd.osgi.JarResource; /** * A {@link PdeProjectJar} packages a project into a jar like it would be @@ -32,42 +41,137 @@ * result of such operation has to be used in the project directly, use * {@link ProjectJar} instead that explodes all additional data into the output * folder. + *

+ * The jar content is driven by the {@code bin.includes} property in + * {@code build.properties}: + *

*/ public class PdeProjectJar extends Jar { + private static final String DOT = "."; //$NON-NLS-1$ + public PdeProjectJar(IProject project) throws CoreException { super(project.getName()); 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$ - } - } + if (!buildFile.exists()) { + return; + } + IBuild build = new WorkspaceBuildModel(buildFile).getBuild(); + // Build a map from library name to its output build entry + Map outputEntries = new HashMap<>(); + for (IBuildEntry entry : build.getBuildEntries()) { + String name = entry.getName(); + if (name.startsWith(IBuildEntry.OUTPUT_PREFIX)) { + String library = name.substring(IBuildEntry.OUTPUT_PREFIX.length()); + outputEntries.put(library, entry); + } + } + // bin.includes defines what goes into the jar + IBuildEntry binIncludes = build.getEntry(IBuildEntry.BIN_INCLUDES); + if (binIncludes == null) { + return; + } + for (String token : binIncludes.getTokens()) { + if (DOT.equals(token)) { + includeDotEntry(project, outputEntries.get(DOT)); + } else { + IBuildEntry outputEntry = outputEntries.get(token); + if (outputEntry != null) { + includeLibraryEntry(project, token, outputEntry); + } else { + includeFileEntry(project, token); } } - IBuildEntry entry = build.getEntry(IBuildEntry.BIN_INCLUDES); - if (entry != null) { - // TODO adding bin included here! + } + } + + /** + * Handles the {@code .} (dot) entry in {@code bin.includes}. The dot entry + * means the compiled classes should be included at the jar root. + *

+ * If an {@code output.} entry exists, its folders are included. Otherwise, + * for Java projects the default JDT output folder is used. + */ + private void includeDotEntry(IProject project, IBuildEntry outputEntry) throws CoreException { + if (outputEntry != null) { + includeOutputFolders(project, outputEntry); + } else if (PluginProject.isJavaProject(project)) { + // '.' is in bin.includes but no output. entry exists; + // fall back to the default Java output folder + IJavaProject javaProject = JavaCore.create(project); + IWorkspaceRoot workspaceRoot = project.getWorkspace().getRoot(); + IPath defaultOutput = javaProject.getOutputLocation(); + IFolder defaultOutputFolder = workspaceRoot.getFolder(defaultOutput); + FileResource.addResources(this, defaultOutputFolder, null); + } + } + + /** + * Handles a library entry (e.g. {@code lib.jar}) in {@code bin.includes} + * that has a matching {@code output.lib.jar} entry. The compiled output is + * packaged as an embedded inner jar. + */ + private void includeLibraryEntry(IProject project, String libraryName, IBuildEntry outputEntry) + throws CoreException { + Jar innerJar = new Jar(libraryName); + includeOutputFolders(project, outputEntry, innerJar); + putResource(libraryName, new JarResource(innerJar)); + } + + /** + * Includes all output folders from the given entry into this jar. + */ + private void includeOutputFolders(IProject project, IBuildEntry outputEntry) throws CoreException { + includeOutputFolders(project, outputEntry, this); + } + + /** + * Includes all output folders from the given entry into the specified target + * jar. + */ + private static void includeOutputFolders(IProject project, IBuildEntry outputEntry, Jar targetJar) + throws CoreException { + for (String folder : outputEntry.getTokens()) { + String folderPath = folder.endsWith("/") ? folder.substring(0, folder.length() - 1) : folder; //$NON-NLS-1$ + IFolder outputFolder = project.getFolder(folderPath); + if (outputFolder.exists()) { + FileResource.addResources(targetJar, outputFolder, null); } } } - private void include(IFolder folder, String prefix) throws CoreException { + /** + * Handles a regular file or folder entry from {@code bin.includes} that + * does not have a matching {@code output.} entry. The file or folder is + * included directly from the project root. + */ + private void includeFileEntry(IProject project, String token) throws CoreException { + String path = token.endsWith("/") ? token.substring(0, token.length() - 1) : token; //$NON-NLS-1$ + IResource resource = project.findMember(path); + if (resource instanceof IFile file) { + putResource(token, new FileResource(file)); + } else if (resource instanceof IFolder folder) { + String prefix = token.endsWith("/") ? token : token + "/"; //$NON-NLS-1$ //$NON-NLS-2$ + includeFolder(folder, prefix); + } + } + + private void includeFolder(IFolder folder, String prefix) throws CoreException { + if (!folder.exists()) { + return; + } 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$ + includeFolder(subfolder, prefix + subfolder.getName() + "/"); //$NON-NLS-1$ } } } diff --git a/ui/org.eclipse.pde.launching/META-INF/MANIFEST.MF b/ui/org.eclipse.pde.launching/META-INF/MANIFEST.MF index 45e93654824..4d331f7d01d 100644 --- a/ui/org.eclipse.pde.launching/META-INF/MANIFEST.MF +++ b/ui/org.eclipse.pde.launching/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %name Bundle-SymbolicName: org.eclipse.pde.launching;singleton:=true -Bundle-Version: 3.13.600.qualifier +Bundle-Version: 3.13.700.qualifier Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-Vendor: %provider-name Require-Bundle: org.eclipse.jdt.junit.core;bundle-version="[3.6.0,4.0.0)", diff --git a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/JUnitLaunchConfigurationDelegate.java b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/JUnitLaunchConfigurationDelegate.java index dfd2610cdf1..467a5928547 100644 --- a/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/JUnitLaunchConfigurationDelegate.java +++ b/ui/org.eclipse.pde.launching/src/org/eclipse/pde/launching/JUnitLaunchConfigurationDelegate.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.stream.Collectors; @@ -232,7 +233,7 @@ protected void collectExecutionArguments(ILaunchConfiguration configuration, Lis if (isJUnitContainerProject(javaProject)) { //if this is a junit container project it means a user can use additional classes (from the junit container and possible other) that //are not required to be imported, we compute a fragment manifest here to add additional imports ... - try (PdeProjectAnalyzer analyzer = new PdeProjectAnalyzer(javaProject.getProject(), true)) { + try (PdeProjectAnalyzer analyzer = new PdeProjectAnalyzer(javaProject.getProject(), true, Set.of())) { analyzer.setImportPackage("*"); //$NON-NLS-1$ String bsn = testPlugin.getId() + "-additional-test-probe-imports"; //$NON-NLS-1$ analyzer.setBundleSymbolicName(bsn);