Skip to content

Commit 79b22ee

Browse files
committed
Do not follow generic requirements when computing the classpath
OSGi uses a generic Provide-Capability/Require-Capability model that allows to declare requirements beyond the classic Import-Package and Require-Bunlde. A common example is a bundle that provides a DS component and needs an extender to actually construct the component and inject the services. Another is a logging API that needs a provider. These are especially needed because in such scenario a provider/consumer do not know each other and should not depend directly (only loosely coupled). Currently such providers are still pulled in even though they are not contribute to the compilation classpath as they are completely invisible at runtime - there is a wiring but no classloader access granted. This now only follows build time requirements to avoid the situation.
1 parent 26089c7 commit 79b22ee

File tree

24 files changed

+254
-11
lines changed

24 files changed

+254
-11
lines changed

ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/MinimalState.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public StateDelta resolveState(String[] symbolicNames) {
271271
// now we need to check all bundles if there is any fragment. If a
272272
// fragment should be resolved its host and all already attached
273273
// fragments must be re-resolved again
274-
for (BundleDescription bundleDescription : bundles) {
274+
for (BundleDescription bundleDescription : bundles.toArray(BundleDescription[]::new)) {
275275
HostSpecification host = bundleDescription.getHost();
276276
if (host != null) {
277277
BundleRequirement requirement = host.getRequirement();

ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/RequiredPluginsClasspathContainer.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -683,8 +683,7 @@ private void addImplicitDependencies(BundleDescription desc, Set<BundleDescripti
683683
*/
684684
private void addTransitiveDependenciesWithForbiddenAccess(Set<BundleDescription> added,
685685
List<IClasspathEntry> entries) throws CoreException {
686-
Set<BundleDescription> closure = DependencyManager.findRequirementsClosure(added,
687-
INCLUDE_OPTIONAL_DEPENDENCIES);
686+
Collection<BundleDescription> closure = ClasspathComputer.collectBuildRelevantDependencies(added);
688687
String systemBundleBSN = TargetPlatformHelper.getPDEState().getSystemBundle();
689688
Iterator<BundleDescription> transitiveDeps = closure.stream()
690689
.filter(desc -> !desc.getSymbolicName().equals(systemBundleBSN))

ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/core/tests/internal/classpath/ClasspathResolutionTest2.java

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import org.eclipse.jdt.core.IClasspathEntry;
3030
import org.eclipse.jdt.core.IJavaModelMarker;
3131
import org.eclipse.jdt.core.compiler.IProblem;
32+
import org.eclipse.osgi.service.resolver.BundleDescription;
33+
import org.eclipse.osgi.service.resolver.ResolverError;
3234
import org.eclipse.pde.core.plugin.IPluginModelBase;
3335
import org.eclipse.pde.internal.core.ClasspathComputer;
3436
import org.eclipse.pde.internal.core.PDECore;
@@ -180,8 +182,12 @@ public class ClasspathResolutionTest2 {
180182
*/
181183
static final List<String> TRANSITIVE_FORBIDDEN_BUNDLES = List.of("C", "D", "E", "F", "H");
182184

183-
/** All test bundles other than A. */
184-
static final List<String> ALL_TEST_BUNDLES = List.of("B", "C", "D", "E", "F", "G", "H");
185+
static final List<String> ALL_TEST_BUNDLES = List.of("B", "C", "D", "E", "F", "G", "H", "cyclic-capabilities/Af",
186+
"cyclic-capabilities/cyclic.capabilities.provider");
187+
188+
private static IProject projectConsumer;
189+
190+
private static IClasspathEntry[] consumerClasspathEntries;
185191

186192
@BeforeClass
187193
public static void setupBeforeClass() throws Exception {
@@ -195,6 +201,8 @@ public static void setupBeforeClass() throws Exception {
195201
projectA = ProjectUtils.importTestProject("tests/projects/A");
196202
projectAe = ProjectUtils.importTestProject("tests/projects/Ae");
197203
projectAd = ProjectUtils.importTestProject("tests/projects/Ad");
204+
projectConsumer = ProjectUtils
205+
.importTestProject("tests/projects/cyclic-capabilities/cyclic.capabilities.consumer");
198206

199207
// Trigger a full workspace build. The workspace builder respects
200208
// project build order, so dependency bundles (B-H, X) are built
@@ -214,6 +222,7 @@ public static void setupBeforeClass() throws Exception {
214222
projectA.build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
215223
projectAe.build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
216224
projectAd.build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
225+
projectConsumer.build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
217226
TestUtils.waitForJobs("ClasspathResolutionTest2.rebuild", 200, 30_000);
218227

219228
// Compute PDE classpath entries (only plugin dependencies, no
@@ -225,6 +234,10 @@ public static void setupBeforeClass() throws Exception {
225234
IPluginModelBase modelAd = PDECore.getDefault().getModelManager().findModel(projectAd);
226235
assertNotNull("PDE model for project Ad must be available", modelAd);
227236
classpathEntriesAd = ClasspathComputer.computeClasspathEntries(modelAd, projectAd);
237+
238+
IPluginModelBase consumerModel = PDECore.getDefault().getModelManager().findModel(projectConsumer);
239+
consumerClasspathEntries = ClasspathComputer.computeClasspathEntries(consumerModel,
240+
projectConsumer);
228241
}
229242

230243
// =========================================================================
@@ -566,9 +579,9 @@ record ForbiddenRef(int line, String qualifiedType, String project) {
566579
// A has forbiddenReference=warning → WARNING
567580
// Ae has forbiddenReference=error → ERROR
568581
assertThat(m.getAttribute(IMarker.SEVERITY, -1))
569-
.as("Line %d in project %s: must be %s " + "severity (forbiddenReference=%s in "
570-
+ "project settings)",
571-
ref.line, project.getName(), severityLabel, severityLabel.toLowerCase())
582+
.as("Line %d in project %s: must be %s " + "severity (forbiddenReference=%s in "
583+
+ "project settings)",
584+
ref.line, project.getName(), severityLabel, severityLabel.toLowerCase())
572585
.isEqualTo(expectedSeverity);
573586

574587
// Problem ID must be ForbiddenReference because all
@@ -675,8 +688,8 @@ public void testDiscouragedMarkersOnProjectAd() throws Exception {
675688
.as("Discouraged marker must be on line 44 " + "(x.internal.MyObject)").isEqualTo(44);
676689

677690
assertThat(m.getAttribute(IMarker.SEVERITY, -1))
678-
.as("Discouraged access must be WARNING " + "(discouragedReference=warning in "
679-
+ "project settings)")
691+
.as("Discouraged access must be WARNING " + "(discouragedReference=warning in "
692+
+ "project settings)")
680693
.isEqualTo(IMarker.SEVERITY_WARNING);
681694

682695
// DiscouragedReference vs ForbiddenReference: K_DISCOURAGED
@@ -714,7 +727,13 @@ public void testDiscouragedMarkersOnProjectAd() throws Exception {
714727
*/
715728
@Test
716729
public void testDependencyBundlesBuildClean() throws Exception {
717-
for (String bundleName : ALL_TEST_BUNDLES) {
730+
for (String projectPath : ALL_TEST_BUNDLES) {
731+
String bundleName;
732+
if (projectPath.contains("/")) {
733+
bundleName = projectPath.split("/")[1];
734+
} else {
735+
bundleName = projectPath;
736+
}
718737
IProject project = getProject(bundleName);
719738
IMarker[] markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
720739

@@ -746,6 +765,40 @@ public void testClasspathComputationIsDeterministic() throws Exception {
746765
.containsExactlyInAnyOrderElementsOf(originalNames);
747766
}
748767

768+
// =========================================================================
769+
// Section 7: Cyclic capability resolution for fragments
770+
// =========================================================================
771+
772+
/**
773+
* A fragment that uses {@code Require-Capability} to depend on a bundle
774+
* that itself {@code Require-Bundle}s the fragment's host must not end up
775+
* with the capability provider on its classpath — that would create a
776+
* cyclic dependency. Only the host bundle ({@code Af}) should appear.
777+
* <p>
778+
* <b>Test bundle dependency graph:</b>
779+
* <pre>
780+
* Af: simple host bundle
781+
* cyclic.capabilities.provider: Provide-Capability: some.test.capability
782+
* Require-Bundle: Af
783+
* cyclic.capabilities.consumer: Require-Capability: some.test.capability
784+
* Fragment-Host: Af
785+
* </pre>
786+
*/
787+
@Test
788+
public void testRequiredPluginsViaCapabilityForFragment() throws Exception {
789+
List<String> entryNames = Arrays.stream(consumerClasspathEntries)
790+
.map(e -> e.getPath().lastSegment()).toList();
791+
IPluginModelBase consumerModel = PDECore.getDefault().getModelManager().findModel(projectConsumer);
792+
assertThat(consumerModel).isNotNull();
793+
BundleDescription bundleDescription = consumerModel.getBundleDescription();
794+
ResolverError[] resolverErrors = bundleDescription.getContainingState().getResolverErrors(bundleDescription);
795+
assertThat(resolverErrors).isEmpty();
796+
assertThat(entryNames).as("Fragment host Af must be on the consumer's classpath").contains("Af");
797+
assertThat(entryNames)
798+
.as("cyclic.capabilities.provider must NOT be on the classpath (would create cycle)")
799+
.noneMatch(name -> name.contains("cyclic.capabilities.provider"));
800+
}
801+
749802
// =========================================================================
750803
// Utility methods
751804
// =========================================================================
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<classpath>
3+
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
4+
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
5+
<classpathentry kind="src" path="src"/>
6+
<classpathentry kind="output" path="bin"/>
7+
</classpath>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<projectDescription>
3+
<name>Af</name>
4+
<comment></comment>
5+
<projects>
6+
</projects>
7+
<buildSpec>
8+
<buildCommand>
9+
<name>org.eclipse.jdt.core.javabuilder</name>
10+
<arguments>
11+
</arguments>
12+
</buildCommand>
13+
<buildCommand>
14+
<name>org.eclipse.pde.ManifestBuilder</name>
15+
<arguments>
16+
</arguments>
17+
</buildCommand>
18+
<buildCommand>
19+
<name>org.eclipse.pde.SchemaBuilder</name>
20+
<arguments>
21+
</arguments>
22+
</buildCommand>
23+
</buildSpec>
24+
<natures>
25+
<nature>org.eclipse.pde.PluginNature</nature>
26+
<nature>org.eclipse.jdt.core.javanature</nature>
27+
</natures>
28+
</projectDescription>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
eclipse.preferences.version=1
2+
encoding/<project>=UTF-8
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
eclipse.preferences.version=1
2+
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
3+
org.eclipse.jdt.core.compiler.compliance=17
4+
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
5+
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
6+
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
7+
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
8+
org.eclipse.jdt.core.compiler.release=enabled
9+
org.eclipse.jdt.core.compiler.source=17
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Manifest-Version: 1.0
2+
Bundle-ManifestVersion: 2
3+
Bundle-Name: Af
4+
Bundle-SymbolicName: Af
5+
Bundle-Version: 1.0.0.qualifier
6+
Export-Package: a.f
7+
Automatic-Module-Name: Af
8+
Bundle-RequiredExecutionEnvironment: JavaSE-17
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
source.. = src/
2+
output.. = bin/
3+
bin.includes = META-INF/,\
4+
.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package a.f;
2+
3+
public interface ServiceInterface {
4+
5+
}

0 commit comments

Comments
 (0)