From 74b33115c59aaee7ec56c4d5f33a244a70d3ece3 Mon Sep 17 00:00:00 2001 From: Keith Bui Date: Sat, 27 Sep 2025 19:05:07 -0700 Subject: [PATCH 1/3] Added Update Header from Source Annotation checkbox to Organize Manifest Wizard. --- .../eclipse/pde/internal/ui/PDEUIMessages.java | 4 ++++ .../pde/internal/ui/pderesources.properties | 2 ++ .../tools/IOrganizeManifestsSettings.java | 1 + .../tools/OrganizeManifestsWizardPage.java | 17 ++++++++++++++++- 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java index 575ac6fd8fa..2398c914811 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java @@ -293,6 +293,10 @@ public class PDEUIMessages extends NLS { public static String OrganizeManifestsWizardPage_calculateUses; + public static String OrganizeManifestsWizardPage_updateHeader; + + public static String OrganizeManifestsOperation_updateHeader; + public static String OverviewPage_contentDescription; public static String OverviewPage_contentTitle; diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties index 8b45f594b30..0285ee05eea 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties @@ -2381,6 +2381,8 @@ OrganizeManifestsWizardPage_exportedGroup=Exported Packages OrganizeManifestsWizardPage_markInternal=Mark as &internal all packages that match the following filter: OrganizeManifestsWizardPage_packageFilter=Package fil&ter: OrganizeManifestsWizardPage_calculateUses=Calculate 'uses' directive for public packages (this may be a long-running operation) +OrganizeManifestsWizardPage_updateHeader=Update Header from Source Annotations +OrganizeManifestsOperation_updateHeader=updating header for missing required capabilities from source annotations... {0} OrganizeManifestsWizardPage_markOptional=&marking them as optional OrganizeManifestsWizardPage_removeUnused=Remo&ve unused dependencies (this may be a long-running operation) OrganizeManifestsWizardPage_generalGroup=General Manifest Cleanup diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/IOrganizeManifestsSettings.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/IOrganizeManifestsSettings.java index 7c096027e2a..725dff9f966 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/IOrganizeManifestsSettings.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/IOrganizeManifestsSettings.java @@ -21,6 +21,7 @@ public interface IOrganizeManifestsSettings { public static final String VALUE_DEFAULT_FILTER = "*.internal*"; //$NON-NLS-1$ public static final String PROP_REMOVE_UNRESOLVED_EX = "OrganizeManifests.ExportedPackages.removeUnresolved"; //$NON-NLS-1$ public static final String PROP_CALCULATE_USES = "OrganizeManifests.calculateUses"; //$NON-NLS-1$ + public static final String PROP_UPDATE_HEADER = "OrganizeManifests.updateHeader"; //$NON-NLS-1$ public static final String PROP_MODIFY_DEP = "OrganizeManifests.RequireImport.modifyDep"; //$NON-NLS-1$ public static final String PROP_RESOLVE_IMP_MARK_OPT = "OrganizeManifests.RequireImport.resolve:markOptional"; //$NON-NLS-1$ public static final String PROP_UNUSED_DEPENDENCIES = "OrganizeManifests.RequireImport.findRemoveUnused"; //$NON-NLS-1$ diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/OrganizeManifestsWizardPage.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/OrganizeManifestsWizardPage.java index 2babe0c8fc0..a04c50eecfa 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/OrganizeManifestsWizardPage.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/OrganizeManifestsWizardPage.java @@ -47,6 +47,7 @@ public class OrganizeManifestsWizardPage extends UserInputWizardPage implements private Button fRemoveUnresolved; private Button fCalculateUses; + private Button fUpdateHeader; private Button fAddMissing; private Button fMarkInternal; private Text fPackageFilter; @@ -164,6 +165,13 @@ private void createExportedPackagesGroup(Composite container) { gd = new GridData(); gd.verticalIndent = 5; fCalculateUses.setLayoutData(gd); + + // issue-1816 + fUpdateHeader = new Button(group, SWT.CHECK); + fUpdateHeader.setText(PDEUIMessages.OrganizeManifestsWizardPage_updateHeader); + gd = new GridData(); + gd.verticalIndent = 5; + fUpdateHeader.setLayoutData(gd); } private void createRequireImportGroup(Composite container) { @@ -241,6 +249,10 @@ private void presetOptions() { fCalculateUses.setSelection(selection); fProcessor.setCalculateUses(selection); + selection = settings.getBoolean(PROP_UPDATE_HEADER); + fUpdateHeader.setSelection(selection); + fProcessor.setUpdateHeader(selection); + selection = !settings.getBoolean(PROP_MODIFY_DEP); fModifyDependencies.setSelection(selection); fProcessor.setModifyDep(selection); @@ -290,6 +302,7 @@ protected void performOk() { settings.put(PROP_INTERAL_PACKAGE_FILTER, fPackageFilter.getText()); settings.put(PROP_REMOVE_UNRESOLVED_EX, !fRemoveUnresolved.getSelection()); settings.put(PROP_CALCULATE_USES, fCalculateUses.getSelection()); + settings.put(PROP_UPDATE_HEADER, fUpdateHeader.getSelection()); settings.put(PROP_MODIFY_DEP, !fModifyDependencies.getSelection()); settings.put(PROP_RESOLVE_IMP_MARK_OPT, fOptionalImport.getSelection()); @@ -318,7 +331,7 @@ private void setEnabledStates() { private void setButtonArrays() { fTopLevelButtons = new Button[] { fRemoveUnresolved, fAddMissing, fModifyDependencies, fMarkInternal, fUnusedDependencies, fAdditonalDependencies, fComputeImportPackages, fFixIconNLSPaths, - fRemovedUnusedKeys, fRemoveLazy, fRemoveUselessFiles, fCalculateUses }; + fRemovedUnusedKeys, fRemoveLazy, fRemoveUselessFiles, fCalculateUses, fUpdateHeader }; } private void setPageComplete() { @@ -360,6 +373,8 @@ private void doProcessorSetting(Object source) { fProcessor.setRemoveUnresolved(fRemoveUnresolved.getSelection()); } else if (fCalculateUses.equals(source)) { fProcessor.setCalculateUses(fCalculateUses.getSelection()); + } else if (fUpdateHeader.equals(source)) { + fProcessor.setUpdateHeader(fUpdateHeader.getSelection()); } else if (fModifyDependencies.equals(source)) { fProcessor.setModifyDep(fModifyDependencies.getSelection()); } else if (fOptionalImport.equals(source)) { From 608d54233ab8c299c055f9e3481ad098b056f01e Mon Sep 17 00:00:00 2001 From: Keith Bui Date: Sat, 27 Sep 2025 19:07:44 -0700 Subject: [PATCH 2/3] Implemented logic for Update Header from Source checkbox. --- .../tools/OrganizeManifestsProcessor.java | 17 ++ .../UpdateHeaderFromAnnotationsOperation.java | 181 ++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/UpdateHeaderFromAnnotationsOperation.java diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/OrganizeManifestsProcessor.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/OrganizeManifestsProcessor.java index 1e0f1b270e1..4aaeb1c794a 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/OrganizeManifestsProcessor.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/OrganizeManifestsProcessor.java @@ -56,6 +56,11 @@ public class OrganizeManifestsProcessor extends RefactoringProcessor implements // export-package protected boolean fCalculateUses = false; // calculate the 'uses' directive // for exported packages + + protected boolean fUpdateHeader = false; // automatically adds missing + // required + // capabilities + protected boolean fModifyDep = true; // modify import-package / // require-bundle protected boolean fRemoveDependencies = true; // if true: remove, else mark @@ -210,6 +215,14 @@ private void runCleanup(IProgressMonitor monitor, IBundlePluginModelBase modelBa } } + if (fUpdateHeader) { + subMonitor.subTask(NLS.bind(PDEUIMessages.OrganizeManifestsOperation_updateHeader, projectName)); + if (!subMonitor.isCanceled()) { + UpdateHeaderFromAnnotationsOperation.updateHeadersFromAnnotations(fCurrentProject, currentBundle); + subMonitor.worked(3); + } + } + if (fAddDependencies) { subMonitor.subTask(NLS.bind(PDEUIMessages.OrganizeManifestsOperation_additionalDeps, projectName)); if (!subMonitor.isCanceled()) { @@ -316,6 +329,10 @@ public void setCalculateUses(boolean calculateUses) { fCalculateUses = calculateUses; } + public void setUpdateHeader(boolean updateHeader) { + fUpdateHeader = updateHeader; + } + public void setModifyDep(boolean modifyDep) { fModifyDep = modifyDep; } diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/UpdateHeaderFromAnnotationsOperation.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/UpdateHeaderFromAnnotationsOperation.java new file mode 100644 index 00000000000..f6d8aefacfd --- /dev/null +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/UpdateHeaderFromAnnotationsOperation.java @@ -0,0 +1,181 @@ +/******************************************************************************* + * Copyright (c) 2025 Keith Bui 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: + * Keith Bui - Issue #1816 + *******************************************************************************/ + +package org.eclipse.pde.internal.ui.wizards.tools; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +//import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.pde.internal.core.ibundle.IBundle; +import org.osgi.framework.Constants; +import org.osgi.framework.namespace.ExecutionEnvironmentNamespace; +import org.osgi.resource.Capability; + +import aQute.bnd.header.OSGiHeader; +import aQute.bnd.header.Parameters; +import aQute.bnd.osgi.Analyzer; +import aQute.bnd.osgi.Jar; +import aQute.bnd.osgi.resource.CapReqBuilder; + +/* + * Utility to enhance bundle MANIFEST.MF headers from source annotations, + * similar to Tycho's deriveHeaderFromSource feature. Currently, only Require-Capability + * items are processed. + */ +public class UpdateHeaderFromAnnotationsOperation { + + public static void updateHeadersFromAnnotations(IProject project, IBundle bundle) { + IJavaProject javaProject = JavaCore.create(project); + if (javaProject == null || !javaProject.exists()) { + System.out.println("Not a Java project: " + project.getName()); + return; + } + + try { + IPath outputLocation = javaProject.getOutputLocation(); + IPath fullPath = project.getWorkspace().getRoot().getFolder(outputLocation).getLocation(); + File outputDir = fullPath.toFile(); + System.out.println("Resolved outputDir: " + outputDir.getAbsolutePath()); + if (!outputDir.exists()) { + System.out.println("Output dir does not exist."); + return; + } + + File manifestFile = new File(project.getLocation().toFile(), "META-INF/MANIFEST.MF"); + Manifest manifest = new Manifest(); + + if (manifestFile.exists()) { + try (var is = new java.io.FileInputStream(manifestFile)) { + manifest = new Manifest(is); + System.out.println("Loaded existing MANIFEST.MF"); + } + } + + // Copy PDE bundle headers into working manifest (to sync PDE model + // state) + for (Object key : bundle.getManifestHeaders().keySet()) { + manifest.getMainAttributes().putValue( + key.toString(), + bundle.getManifestHeaders().get(key).toString() + ); + } + + try (Jar jar = new Jar(project.getName(), outputDir); + Analyzer analyzer = new Analyzer(jar)) { + // Add project classpath + for (IClasspathEntry entry : javaProject.getResolvedClasspath(true)) { + File f = entry.getPath().toFile(); + System.out.println("Classpath entry: " + f.getAbsolutePath()); + if (f.exists() && f.length() > 0) { + try { + analyzer.addClasspath(f); + } catch (IOException e) { + // ignore bad entries + System.out.println("Failed to add classpath: " + f); + } + } + } + + Manifest calcManifest = analyzer.calcManifest(); + String reqCap = calcManifest.getMainAttributes().getValue(Constants.REQUIRE_CAPABILITY); + System.out.println("Bnd computed Require-Capability: " + reqCap); + mergeRequireCapability(manifest.getMainAttributes(), calcManifest.getMainAttributes()); + System.out.println("Merged capabilities into bundle headers."); + } + IFile manifestIFile = project.getFile("META-INF/MANFIEST.MF"); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + manifest.write(baos); + byte[] bytes = baos.toByteArray(); + if (manifestIFile.exists()) { + manifestIFile.setContents(new ByteArrayInputStream(bytes), true, false, null); + } else { + manifestIFile.create(new ByteArrayInputStream(bytes), true, null); + } + manifestIFile.refreshLocal(IResource.DEPTH_ZERO, null); // sync + // Eclipse + // workspace + System.out.println("Updated MANIFEST.MF with merged headers"); + } + + // Update only the headers we touched in PDE's IBundle model + String reqCapValue = manifest.getMainAttributes().getValue(Constants.REQUIRE_CAPABILITY); + if (reqCapValue != null) { + bundle.setHeader(Constants.REQUIRE_CAPABILITY, reqCapValue); + } + // remove BRE if replaced by osgi.ee + manifest.getMainAttributes().remove(new Attributes.Name(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT)); + + } catch (Exception e) { + String message = "Cannot derive header from source"; + System.out.println(message); + System.out.println(e); + } + } + + @SuppressWarnings("deprecation") + private static void mergeRequireCapability(Attributes mainAttributes, Attributes calcAttributes) { + String existingValue = mainAttributes.getValue(Constants.REQUIRE_CAPABILITY); + String newValue = calcAttributes.getValue(Constants.REQUIRE_CAPABILITY); + + if (newValue == null) { + return; + } + + Parameters additional = OSGiHeader.parseHeader(newValue); + if (additional.containsKey(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE)) { + // remove deprecated header but use the EE namespace + mainAttributes.remove(new Attributes.Name(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT)); + } + + if (existingValue == null) { + mainAttributes.putValue(Constants.REQUIRE_CAPABILITY, newValue); + } else { + Parameters current = OSGiHeader.parseHeader(existingValue); + if (current.containsKey(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE)) { + // strip duplicate osgi.ee + additional.remove(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE); + } + + List initialCapabilities = CapReqBuilder.getCapabilitiesFrom(current); + List newCapabilities = CapReqBuilder.getCapabilitiesFrom(additional); + if (newCapabilities.isEmpty()) { + return; + } + + Set mergedCapabilities = new LinkedHashSet<>(); + mergedCapabilities.addAll(initialCapabilities); + mergedCapabilities.addAll(newCapabilities); + + String merged = mergedCapabilities.stream().map(cap -> String.valueOf(cap).replace("'", "\"")) //$NON-NLS-1$ //$NON-NLS-2$ + .collect(Collectors.joining(",")); //$NON-NLS-1$ + mainAttributes.putValue(Constants.REQUIRE_CAPABILITY, merged); + } + } +} \ No newline at end of file From fb7d2982bb35ef74932f84d9bdf5270cb0e96071 Mon Sep 17 00:00:00 2001 From: Keith Bui Date: Fri, 3 Oct 2025 13:30:03 -0700 Subject: [PATCH 3/3] Removed unnecessary print statements --- .../tools/UpdateHeaderFromAnnotationsOperation.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/UpdateHeaderFromAnnotationsOperation.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/UpdateHeaderFromAnnotationsOperation.java index f6d8aefacfd..c39e00a26ae 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/UpdateHeaderFromAnnotationsOperation.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/wizards/tools/UpdateHeaderFromAnnotationsOperation.java @@ -54,7 +54,6 @@ public class UpdateHeaderFromAnnotationsOperation { public static void updateHeadersFromAnnotations(IProject project, IBundle bundle) { IJavaProject javaProject = JavaCore.create(project); if (javaProject == null || !javaProject.exists()) { - System.out.println("Not a Java project: " + project.getName()); return; } @@ -62,9 +61,7 @@ public static void updateHeadersFromAnnotations(IProject project, IBundle bundle IPath outputLocation = javaProject.getOutputLocation(); IPath fullPath = project.getWorkspace().getRoot().getFolder(outputLocation).getLocation(); File outputDir = fullPath.toFile(); - System.out.println("Resolved outputDir: " + outputDir.getAbsolutePath()); if (!outputDir.exists()) { - System.out.println("Output dir does not exist."); return; } @@ -74,7 +71,6 @@ public static void updateHeadersFromAnnotations(IProject project, IBundle bundle if (manifestFile.exists()) { try (var is = new java.io.FileInputStream(manifestFile)) { manifest = new Manifest(is); - System.out.println("Loaded existing MANIFEST.MF"); } } @@ -92,22 +88,18 @@ public static void updateHeadersFromAnnotations(IProject project, IBundle bundle // Add project classpath for (IClasspathEntry entry : javaProject.getResolvedClasspath(true)) { File f = entry.getPath().toFile(); - System.out.println("Classpath entry: " + f.getAbsolutePath()); if (f.exists() && f.length() > 0) { try { analyzer.addClasspath(f); } catch (IOException e) { // ignore bad entries - System.out.println("Failed to add classpath: " + f); } } } Manifest calcManifest = analyzer.calcManifest(); String reqCap = calcManifest.getMainAttributes().getValue(Constants.REQUIRE_CAPABILITY); - System.out.println("Bnd computed Require-Capability: " + reqCap); mergeRequireCapability(manifest.getMainAttributes(), calcManifest.getMainAttributes()); - System.out.println("Merged capabilities into bundle headers."); } IFile manifestIFile = project.getFile("META-INF/MANFIEST.MF"); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { @@ -121,7 +113,6 @@ public static void updateHeadersFromAnnotations(IProject project, IBundle bundle manifestIFile.refreshLocal(IResource.DEPTH_ZERO, null); // sync // Eclipse // workspace - System.out.println("Updated MANIFEST.MF with merged headers"); } // Update only the headers we touched in PDE's IBundle model