Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions biz.aQute.bndlib.tests/test/test/AnalysisPluginTest.java
Original file line number Diff line number Diff line change
@@ -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<VersionReport> versionReports = new ArrayList<>();
final List<AnalysisReport> 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 + "]";
}
}
}
28 changes: 28 additions & 0 deletions biz.aQute.bndlib/src/aQute/bnd/osgi/Analyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

//
Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions biz.aQute.bndlib/src/aQute/bnd/service/AnalysisPlugin.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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
}
}
108 changes: 108 additions & 0 deletions docs/_plugins/analysisplugin.md
Original file line number Diff line number Diff line change
@@ -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.
Loading