From de6d687709d77a24970222c7906228d44c6c59bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:09:22 +0000 Subject: [PATCH 1/3] Initial plan From 6355f56105ebeb5820c46b279e1f40f8ca89a42b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:30:28 +0000 Subject: [PATCH 2/3] Add AnalysisPlugin interface and implementation Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- .../test/test/AnalysisPluginTest.java | 176 ++++++++++++++++++ .../src/aQute/bnd/osgi/Analyzer.java | 28 +++ .../src/aQute/bnd/service/AnalysisPlugin.java | 46 +++++ 3 files changed, 250 insertions(+) create mode 100644 biz.aQute.bndlib.tests/test/test/AnalysisPluginTest.java create mode 100644 biz.aQute.bndlib/src/aQute/bnd/service/AnalysisPlugin.java diff --git a/biz.aQute.bndlib.tests/test/test/AnalysisPluginTest.java b/biz.aQute.bndlib.tests/test/test/AnalysisPluginTest.java new file mode 100644 index 0000000000..7f5c1589c2 --- /dev/null +++ b/biz.aQute.bndlib.tests/test/test/AnalysisPluginTest.java @@ -0,0 +1,176 @@ +package test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import aQute.bnd.osgi.Analyzer; +import aQute.bnd.osgi.Builder; +import aQute.bnd.osgi.Descriptors.PackageRef; +import aQute.bnd.osgi.Jar; +import aQute.bnd.service.AnalysisPlugin; +import aQute.bnd.test.jupiter.InjectTemporaryDirectory; +import aQute.lib.io.IO; + +public class AnalysisPluginTest { + + @InjectTemporaryDirectory + File tmp; + + /** + * Test that AnalysisPlugin is called during analysis and receives + * notifications about version decisions. + */ + @Test + public void testAnalysisPluginCalled() throws Exception { + // Create a test plugin that captures version decisions + TestAnalysisPlugin plugin = new TestAnalysisPlugin(); + + try (Builder b = new Builder()) { + // Add classpath with org.osgi.framework + b.addClasspath(IO.getFile("jar/osgi.jar")); + // Export org.osgi.service.event which depends on org.osgi.framework + // This will create an import for org.osgi.framework + b.setProperty("Export-Package", "org.osgi.service.event"); + b.setProperty("Import-Package", "*"); + b.getPlugins().add(plugin); + + Jar jar = b.build(); + assertTrue(b.check(), "Builder should have no errors"); + + // Verify the plugin was called + assertThat(plugin.versionReports).as("Version reports should not be empty").isNotEmpty(); + + // Verify we got reports for framework package + assertThat(plugin.versionReports).anyMatch(report -> + report.packageRef.getFQN().equals("org.osgi.framework")); + } + } + + /** + * Test plugin reporting for provider vs consumer types. + */ + @Test + public void testAnalysisPluginProviderConsumerReporting() throws Exception { + TestAnalysisPlugin plugin = new TestAnalysisPlugin(); + + try (Builder b = new Builder()) { + b.addClasspath(IO.getFile("jar/osgi.jar")); + b.setProperty("Export-Package", "org.osgi.service.event"); + b.setProperty("Import-Package", "*"); + b.getPlugins().add(plugin); + + b.build(); + assertTrue(b.check(), "Builder should have no errors"); + + // Check that we have reports with reasons + assertThat(plugin.versionReports).isNotEmpty(); + + boolean hasProviderReport = plugin.versionReports.stream() + .anyMatch(r -> r.reason != null && r.reason.contains("provider")); + boolean hasConsumerReport = plugin.versionReports.stream() + .anyMatch(r -> r.reason != null && r.reason.contains("consumer")); + + // We should have at least one type of report + assertTrue(hasProviderReport || hasConsumerReport, + "Expected at least one provider or consumer report"); + } + } + + /** + * Test plugin ordering. + */ + @Test + public void testAnalysisPluginOrdering() throws Exception { + TestAnalysisPlugin plugin1 = new TestAnalysisPlugin(10); + TestAnalysisPlugin plugin2 = new TestAnalysisPlugin(5); + + try (Builder b = new Builder()) { + b.addClasspath(IO.getFile("jar/osgi.jar")); + b.setProperty("Export-Package", "org.osgi.service.event"); + b.setProperty("Import-Package", "*"); + + // Add plugins in reverse order + b.getPlugins().add(plugin1); + b.getPlugins().add(plugin2); + + b.build(); + assertTrue(b.check(), "Builder should have no errors"); + + // Both plugins should have been called + assertThat(plugin1.versionReports).isNotEmpty(); + assertThat(plugin2.versionReports).isNotEmpty(); + } + } + + /** + * Test analysis plugin implementation. + */ + static class TestAnalysisPlugin implements AnalysisPlugin { + final List versionReports = new ArrayList<>(); + final List analysisReports = new ArrayList<>(); + final int order; + + TestAnalysisPlugin() { + this(0); + } + + TestAnalysisPlugin(int order) { + this.order = order; + } + + @Override + public void reportImportVersion(Analyzer analyzer, PackageRef packageRef, String version, String reason) + throws Exception { + versionReports.add(new VersionReport(packageRef, version, reason)); + } + + @Override + public void reportAnalysis(Analyzer analyzer, String category, String details) throws Exception { + analysisReports.add(new AnalysisReport(category, details)); + } + + @Override + public int ordering() { + return order; + } + } + + static class VersionReport { + final PackageRef packageRef; + final String version; + final String reason; + + VersionReport(PackageRef packageRef, String version, String reason) { + this.packageRef = packageRef; + this.version = version; + this.reason = reason; + } + + @Override + public String toString() { + return "VersionReport[" + packageRef.getFQN() + " -> " + version + " (" + reason + ")]"; + } + } + + static class AnalysisReport { + final String category; + final String details; + + AnalysisReport(String category, String details) { + this.category = category; + this.details = details; + } + + @Override + public String toString() { + return "AnalysisReport[" + category + ": " + details + "]"; + } + } +} diff --git a/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java b/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java index bf3886a47a..20fb1aa8fa 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java +++ b/biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java @@ -2100,6 +2100,10 @@ void augmentImports(Packages imports, Packages exports) throws Exception { if (Strings.nonNullOrTrimmedEmpty(importRange)) { importAttributes.put(VERSION_ATTRIBUTE, importRange); } + + // Notify analysis plugins about the version decision + String reason = buildVersionReason(provider, importAttributes, exportAttributes); + reportImportVersion(packageRef, importRange, reason); } // @@ -2210,6 +2214,30 @@ String applyVersionPolicy(String exportVersion, String importRange, boolean prov return importRange; } + /** + * Build a human-readable reason for why a version range was chosen. + */ + private String buildVersionReason(boolean provider, Attrs importAttributes, Attrs exportAttributes) { + if (importAttributes.containsKey(PROVIDE_DIRECTIVE)) { + return "explicit provide directive: " + importAttributes.get(PROVIDE_DIRECTIVE); + } else if (exportAttributes.containsKey(PROVIDE_DIRECTIVE)) { + return "export provide directive: " + exportAttributes.get(PROVIDE_DIRECTIVE); + } else if (provider) { + return "provider type detected"; + } else { + return "consumer type (default)"; + } + } + + /** + * Report import version decision to analysis plugins. + */ + private void reportImportVersion(PackageRef packageRef, String version, String reason) { + doPlugins(aQute.bnd.service.AnalysisPlugin.class, (plugin) -> { + plugin.reportImportVersion(this, packageRef, version, reason); + }); + } + /** * Find the packages we depend on, where we implement an interface that is a * Provider Type. These packages, when we import them, must use the provider diff --git a/biz.aQute.bndlib/src/aQute/bnd/service/AnalysisPlugin.java b/biz.aQute.bndlib/src/aQute/bnd/service/AnalysisPlugin.java new file mode 100644 index 0000000000..88ae6a4591 --- /dev/null +++ b/biz.aQute.bndlib/src/aQute/bnd/service/AnalysisPlugin.java @@ -0,0 +1,46 @@ +package aQute.bnd.service; + +import aQute.bnd.osgi.Analyzer; +import aQute.bnd.osgi.Descriptors.PackageRef; + +/** + * A plugin that is called during the analysis phase to collect information + * about analysis decisions. This allows implementations to track why certain + * decisions were made, such as why a particular version range was chosen for an + * import. + *

+ * This plugin is called during the {@link Analyzer#analyze()} phase, before the + * manifest is generated. It provides callbacks for various analysis events, + * allowing implementations to build a detailed log of analysis decisions. + */ +public interface AnalysisPlugin extends OrderedPlugin { + + /** + * Called when the analyzer determines a version range for an import package. + * This provides insight into why a particular version range was chosen. + * + * @param analyzer the analyzer performing the analysis + * @param packageRef the package being analyzed + * @param version the version or version range determined + * @param reason a human-readable explanation for why this version was chosen + * (e.g., "provider type", "consumer type", "explicit version + * policy") + * @throws Exception if an error occurs during processing + */ + void reportImportVersion(Analyzer analyzer, PackageRef packageRef, String version, String reason) + throws Exception; + + /** + * Called when the analyzer makes other analysis decisions that may be of + * interest. + * + * @param analyzer the analyzer performing the analysis + * @param category the category of the analysis decision (e.g., "uses", + * "export", "capability") + * @param details detailed information about the decision + * @throws Exception if an error occurs during processing + */ + default void reportAnalysis(Analyzer analyzer, String category, String details) throws Exception { + // Default implementation does nothing + } +} From 06cae530afb4c63ff4e4f4380c8ee19768c8c0c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 08:37:30 +0000 Subject: [PATCH 3/3] Add documentation for AnalysisPlugin Co-authored-by: laeubi <1331477+laeubi@users.noreply.github.com> --- docs/_plugins/analysisplugin.md | 108 ++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 docs/_plugins/analysisplugin.md diff --git a/docs/_plugins/analysisplugin.md b/docs/_plugins/analysisplugin.md new file mode 100644 index 0000000000..87bcfed48f --- /dev/null +++ b/docs/_plugins/analysisplugin.md @@ -0,0 +1,108 @@ +--- +title: Analysis Plugin +layout: default +summary: A plugin to receive callbacks about analysis decisions +--- + +The Analysis Plugin provides a way to track and log analysis decisions made by the Analyzer during bundle creation. This is particularly useful for understanding why certain version ranges were chosen for package imports. + +## Overview + +When building OSGi bundles, bnd analyzes your code and determines appropriate version ranges for imported packages based on various factors: + +- Whether a package contains provider types (annotated with `@ProviderType`) +- Explicit directives in the Import-Package or Export-Package headers +- Default version policies configured in your build + +The Analysis Plugin allows you to observe these decisions and understand the reasoning behind them. + +## Interface + +The plugin implements the `aQute.bnd.service.AnalysisPlugin` interface: + +```java +public interface AnalysisPlugin extends OrderedPlugin { + /** + * Called when the analyzer determines a version range for an import package. + */ + void reportImportVersion(Analyzer analyzer, PackageRef packageRef, + String version, String reason) throws Exception; + + /** + * Called when the analyzer makes other analysis decisions. + */ + default void reportAnalysis(Analyzer analyzer, String category, + String details) throws Exception { + // Default implementation does nothing + } +} +``` + +## Example Implementation + +Here's a simple example that logs all import version decisions: + +```java +package com.example; + +import aQute.bnd.osgi.Analyzer; +import aQute.bnd.osgi.Descriptors.PackageRef; +import aQute.bnd.service.AnalysisPlugin; + +public class LoggingAnalysisPlugin implements AnalysisPlugin { + + @Override + public void reportImportVersion(Analyzer analyzer, PackageRef packageRef, + String version, String reason) throws Exception { + System.out.printf("Import: %s -> %s (%s)%n", + packageRef.getFQN(), version, reason); + } + + @Override + public int ordering() { + return 0; // Default ordering + } +} +``` + +## Usage + +To use an Analysis Plugin in your build: + +### In a bnd.bnd file: + +``` +-plugin: com.example.LoggingAnalysisPlugin +``` + +### Programmatically in Java: + +```java +try (Builder builder = new Builder()) { + builder.addClasspath(/* ... */); + builder.getPlugins().add(new LoggingAnalysisPlugin()); + builder.build(); +} +``` + +## Understanding the Reasons + +The `reason` parameter in `reportImportVersion()` typically contains one of the following: + +- `"provider type detected"` - The imported package contains provider types, requiring a strict version range +- `"consumer type (default)"` - The imported package is used as a consumer, allowing a more flexible version range +- `"explicit provide directive: true/false"` - An explicit `provide:=true` or `provide:=false` directive was specified +- `"export provide directive: ..."` - The provide directive came from the exporting bundle + +## Use Cases + +Analysis Plugins are useful for: + +1. **Debugging** - Understanding why bnd chose a particular version range +2. **Compliance** - Logging analysis decisions for audit purposes +3. **Optimization** - Identifying opportunities to optimize version policies +4. **Documentation** - Generating reports about bundle dependencies + +## Plugin Ordering + +Like other bnd plugins, Analysis Plugins support ordering via the `ordering()` method. Lower values are called before higher values. This is useful when you have multiple plugins that need to coordinate or when one plugin's output depends on another's.