Skip to content

Commit abb2903

Browse files
committed
Support Eclipse features on -buildpath and container paths (#7322)
Features are now first-class citizens on -buildpath, -testpath, -runpath and -runbundles. The canonical syntax is: -buildpath: org.eclipse.e4.rcp;version='4.40.0';type=org.eclipse.update.feature A feature expands to its member bundles: <plugin> references and <includes> recursively, with platform filtering and exact→highest version fallback. Core changes (biz.aQute.bndlib): - Container.TYPE.FEATURE with member expansion via Project.getFeatureMembers() - Type-aware version lookup via Repository.findProviders (not RepositoryPlugin.versions) - EclipsePlatform matcher for os/ws/arch platform filters - Project.getBundles() flattens features for all consumers (classpath, launcher, IDE) Index changes (biz.aQute.repository): - Feature.toResource() enrichment with bnd.relation/id/version/type/os/ws/arch attributes - P2Indexer self-healing reindex for stale caches + .feature.jar cache-link suffix UI changes (bndtools.core): - RepositoryBundleSelectionPart: canonical clause on DND and wizard add - SelectionDragAdapter: text drag produces canonical clause - VersionedClauseLabelProvider: feature icon in buildpath list - RepoBundleSelectionWizardPage: type-aware selection keying SWTBot infrastructure (bndtools.core.test): - launch.rendered.bnd: RenderedLauncher for real Eclipse widgets - swtbot.tests.bnd: SWTBot test bundle with fixture workspace - FeatureBuildPathWizardSwtbotTest: Add Bundle dialog feature selection - FeatureBuildPathDndSwtbotTest: Repositories view drag→Build Path Tests: - FeatureBuildpathTest (6 cases: nested, platform filter, cycles, fallback, optional, error) - FeatureParserTest.testMemberRelationAttributes (enrichment contract) - SWTBot UI tests (green on win32) API changes: - aQute.bnd.build 4.7.1 → 4.8.0 (Container.TYPE.FEATURE) - aQute.bnd.osgi.resource 5.1.0 → 5.2.0 (TYPE_ECLIPSE_FEATURE, FEATURE_RELATION_*) - aQute.p2.provider 1.0.0 → 1.1.0 (relation attributes) All changes additive; full backward compatibility. Signed-off-by: Peter Kirschner <peter@klib.io>
1 parent b5c4c58 commit abb2903

45 files changed

Lines changed: 2816 additions & 43 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/specs/Support_Eclipse-Feature_buildpath/README.md

Lines changed: 238 additions & 0 deletions
Large diffs are not rendered by default.

biz.aQute.bnd.exporters/src/aQute/bnd/exporter/subsystem/SubsystemExporter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ private List<File> getBundles(Collection<Container> containers, Processor report
260260
files.add(container.getFile());
261261
break;
262262
case LIBRARY :
263+
case FEATURE :
263264
container.contributeFiles(files, reporter);
264265
break;
265266
}
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
package test;
2+
3+
import java.io.File;
4+
import java.util.Collection;
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.SortedSet;
9+
import java.util.TreeSet;
10+
11+
import org.assertj.core.api.SoftAssertions;
12+
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
13+
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.ExtendWith;
16+
import org.osgi.framework.namespace.IdentityNamespace;
17+
import org.osgi.resource.Capability;
18+
import org.osgi.resource.Requirement;
19+
20+
import aQute.bnd.build.Container;
21+
import aQute.bnd.build.Project;
22+
import aQute.bnd.build.Workspace;
23+
import aQute.bnd.osgi.Jar;
24+
import aQute.bnd.osgi.repository.BaseRepository;
25+
import aQute.bnd.osgi.repository.ResourcesRepository;
26+
import aQute.bnd.osgi.resource.CapReqBuilder;
27+
import aQute.bnd.osgi.resource.ResourceBuilder;
28+
import aQute.bnd.osgi.resource.ResourceUtils;
29+
import aQute.bnd.service.RepositoryPlugin;
30+
import aQute.bnd.service.Strategy;
31+
import aQute.bnd.test.jupiter.InjectTemporaryDirectory;
32+
import aQute.bnd.version.Version;
33+
import aQute.lib.io.IO;
34+
35+
/**
36+
* Test the expansion of Eclipse features on -buildpath like paths. A feature
37+
* is a container of included bundles ({@code <plugin>}) and included features
38+
* ({@code <includes>}); on a path it expands to its member bundles. Required
39+
* features/plugins ({@code <requires>}) are dependencies and are not
40+
* expanded.
41+
*/
42+
@ExtendWith(SoftAssertionsExtension.class)
43+
public class FeatureBuildpathTest {
44+
private static final String FEATURE_TYPE = "org.eclipse.update.feature";
45+
46+
@InjectSoftAssertions
47+
SoftAssertions softly;
48+
49+
@InjectTemporaryDirectory
50+
File tmp;
51+
52+
/**
53+
* A repository mimicking the P2 repository semantics: bundles and
54+
* features can share bsn+version, get() dispatches on the requested
55+
* identity type, versions() only lists bundles, and the index is
56+
* queryable via the OSGi Repository API.
57+
*/
58+
static class FeatureRepo extends BaseRepository implements RepositoryPlugin {
59+
final ResourcesRepository index = new ResourcesRepository();
60+
final Map<String, File> files = new HashMap<>();
61+
final Map<String, TreeSet<Version>> bundleVersions = new HashMap<>();
62+
63+
void addBundle(String bsn, String version, File file) throws Exception {
64+
ResourceBuilder rb = new ResourceBuilder();
65+
rb.addCapability(new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE)
66+
.addAttribute(IdentityNamespace.IDENTITY_NAMESPACE, bsn)
67+
.addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, IdentityNamespace.TYPE_BUNDLE)
68+
.addAttribute(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version(version)));
69+
index.add(rb.build());
70+
files.put(bsn + ":" + version + ":" + IdentityNamespace.TYPE_BUNDLE, file);
71+
bundleVersions.computeIfAbsent(bsn, k -> new TreeSet<>())
72+
.add(new Version(version));
73+
}
74+
75+
void addFeature(String id, String version, File file, Collection<Requirement> members) throws Exception {
76+
ResourceBuilder rb = new ResourceBuilder();
77+
rb.addCapability(new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE)
78+
.addAttribute(IdentityNamespace.IDENTITY_NAMESPACE, id)
79+
.addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, FEATURE_TYPE)
80+
.addAttribute(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version(version)));
81+
for (Requirement member : members) {
82+
rb.addRequirement(CapReqBuilder.clone(member));
83+
}
84+
index.add(rb.build());
85+
files.put(id + ":" + version + ":" + FEATURE_TYPE, file);
86+
}
87+
88+
@Override
89+
public File get(String bsn, Version version, Map<String, String> properties, DownloadListener... listeners)
90+
throws Exception {
91+
String requestedType = properties != null ? properties.get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)
92+
: null;
93+
String type = requestedType != null ? requestedType : IdentityNamespace.TYPE_BUNDLE;
94+
File file = files.get(bsn + ":" + version + ":" + type);
95+
if (file == null)
96+
return null;
97+
for (DownloadListener listener : listeners) {
98+
listener.success(file);
99+
}
100+
return file;
101+
}
102+
103+
@Override
104+
public SortedSet<Version> versions(String bsn) throws Exception {
105+
TreeSet<Version> versions = bundleVersions.get(bsn);
106+
return versions != null ? versions : new TreeSet<>();
107+
}
108+
109+
@Override
110+
public List<String> list(String pattern) throws Exception {
111+
return List.copyOf(bundleVersions.keySet());
112+
}
113+
114+
@Override
115+
public Map<Requirement, Collection<Capability>> findProviders(
116+
Collection<? extends Requirement> requirements) {
117+
return index.findProviders(requirements);
118+
}
119+
120+
@Override
121+
public PutResult put(java.io.InputStream stream, PutOptions options) throws Exception {
122+
throw new UnsupportedOperationException();
123+
}
124+
125+
@Override
126+
public boolean canWrite() {
127+
return false;
128+
}
129+
130+
@Override
131+
public String getName() {
132+
return "FeatureRepo";
133+
}
134+
135+
@Override
136+
public String getLocation() {
137+
return "test";
138+
}
139+
}
140+
141+
static Requirement pluginMember(String id, String version, Map<String, String> extra) throws Exception {
142+
CapReqBuilder builder = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE)
143+
.addDirective("filter", "(&(osgi.identity=" + id + ")(version=" + version + "))")
144+
.addAttribute(ResourceUtils.FEATURE_RELATION_ATTRIBUTE, ResourceUtils.FEATURE_RELATION_PLUGIN)
145+
.addAttribute("id", id)
146+
.addAttribute("version", new Version(version));
147+
if (extra != null) {
148+
extra.forEach(builder::addAttribute);
149+
}
150+
return builder.buildSyntheticRequirement();
151+
}
152+
153+
static Requirement includeMember(String id, String version, boolean optional) throws Exception {
154+
CapReqBuilder builder = new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE)
155+
.addDirective("filter",
156+
"(&(osgi.identity=" + id + ")(type=" + FEATURE_TYPE + ")(version=" + version + "))")
157+
.addAttribute(ResourceUtils.FEATURE_RELATION_ATTRIBUTE, ResourceUtils.FEATURE_RELATION_INCLUDE)
158+
.addAttribute("id", id)
159+
.addAttribute(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, FEATURE_TYPE)
160+
.addAttribute("version", new Version(version));
161+
if (optional) {
162+
builder.addDirective("resolution", "optional");
163+
}
164+
return builder.buildSyntheticRequirement();
165+
}
166+
167+
static Requirement requireMember(String id) throws Exception {
168+
return new CapReqBuilder(IdentityNamespace.IDENTITY_NAMESPACE)
169+
.addDirective("filter", "(osgi.identity=" + id + ")")
170+
.addAttribute(ResourceUtils.FEATURE_RELATION_ATTRIBUTE, ResourceUtils.FEATURE_RELATION_REQUIRE)
171+
.addAttribute("id", id)
172+
.buildSyntheticRequirement();
173+
}
174+
175+
File dummyJar(String name) throws Exception {
176+
File file = new File(tmp, name + ".jar");
177+
try (Jar jar = new Jar(name)) {
178+
jar.putResource("about.txt", new aQute.bnd.osgi.EmbeddedResource(name.getBytes(), 0L));
179+
jar.write(file);
180+
}
181+
return file;
182+
}
183+
184+
private Workspace getWorkspace() throws Exception {
185+
File wsDir = new File(tmp, "ws");
186+
IO.copy(IO.getFile("testresources/ws"), wsDir);
187+
return new Workspace(wsDir);
188+
}
189+
190+
@Test
191+
public void expandFeatureWithNestedIncludeOnBuildpath() throws Exception {
192+
try (Workspace ws = getWorkspace()) {
193+
FeatureRepo repo = new FeatureRepo();
194+
repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0"));
195+
repo.addBundle("bundle.b", "2.0.0", dummyJar("bundle.b-2.0.0"));
196+
repo.addBundle("bundle.c", "3.0.0", dummyJar("bundle.c-3.0.0"));
197+
repo.addFeature("sub.feature", "2.0.0", dummyJar("sub.feature-2.0.0"),
198+
List.of(pluginMember("bundle.c", "3.0.0", null)));
199+
repo.addFeature("test.feature", "1.0.0", dummyJar("test.feature-1.0.0"), List.of(
200+
pluginMember("bundle.a", "1.0.0", null), //
201+
pluginMember("bundle.b", "2.0.0", null), //
202+
includeMember("sub.feature", "2.0.0", false), //
203+
requireMember("required.bundle.not.in.repo")));
204+
ws.addBasicPlugin(repo);
205+
206+
try (Project project = ws.getProject("p1")) {
207+
List<Container> containers = project.getBundles(Strategy.LOWEST,
208+
"test.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath");
209+
210+
softly.assertThat(project.check())
211+
.as("no errors or warnings expected")
212+
.isTrue();
213+
softly.assertThat(containers)
214+
.extracting(Container::getBundleSymbolicName)
215+
.containsExactly("bundle.a", "bundle.b", "bundle.c");
216+
softly.assertThat(containers)
217+
.allMatch(c -> c.getType() == Container.TYPE.REPO)
218+
.allMatch(c -> c.getError() == null);
219+
}
220+
}
221+
}
222+
223+
@Test
224+
public void expandFeatureSkipsForeignPlatformMembers() throws Exception {
225+
try (Workspace ws = getWorkspace()) {
226+
FeatureRepo repo = new FeatureRepo();
227+
repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0"));
228+
repo.addBundle("bundle.foreign", "1.0.0", dummyJar("bundle.foreign-1.0.0"));
229+
repo.addFeature("test.feature", "1.0.0", dummyJar("test.feature-1.0.0"), List.of(
230+
pluginMember("bundle.a", "1.0.0", null), //
231+
pluginMember("bundle.foreign", "1.0.0", Map.of("os", "qnx"))));
232+
ws.addBasicPlugin(repo);
233+
234+
try (Project project = ws.getProject("p1")) {
235+
List<Container> containers = project.getBundles(Strategy.LOWEST,
236+
"test.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath");
237+
238+
softly.assertThat(project.check())
239+
.isTrue();
240+
softly.assertThat(containers)
241+
.extracting(Container::getBundleSymbolicName)
242+
.containsExactly("bundle.a");
243+
}
244+
}
245+
}
246+
247+
@Test
248+
public void expandFeatureCycleTerminates() throws Exception {
249+
try (Workspace ws = getWorkspace()) {
250+
FeatureRepo repo = new FeatureRepo();
251+
repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0"));
252+
repo.addBundle("bundle.b", "1.0.0", dummyJar("bundle.b-1.0.0"));
253+
repo.addFeature("feature.a", "1.0.0", dummyJar("feature.a-1.0.0"), List.of(
254+
pluginMember("bundle.a", "1.0.0", null), //
255+
includeMember("feature.b", "1.0.0", false)));
256+
repo.addFeature("feature.b", "1.0.0", dummyJar("feature.b-1.0.0"), List.of(
257+
pluginMember("bundle.b", "1.0.0", null), //
258+
includeMember("feature.a", "1.0.0", false)));
259+
ws.addBasicPlugin(repo);
260+
261+
try (Project project = ws.getProject("p1")) {
262+
List<Container> containers = project.getBundles(Strategy.LOWEST,
263+
"feature.a;version='1.0.0';type=org.eclipse.update.feature", "-buildpath");
264+
265+
softly.assertThat(project.check("Detected a cycle in the includes of feature"))
266+
.isTrue();
267+
softly.assertThat(containers)
268+
.extracting(Container::getBundleSymbolicName)
269+
.containsExactly("bundle.a", "bundle.b");
270+
}
271+
}
272+
}
273+
274+
@Test
275+
public void expandFeatureFallsBackToHighestWhenExactVersionMissing() throws Exception {
276+
try (Workspace ws = getWorkspace()) {
277+
FeatureRepo repo = new FeatureRepo();
278+
repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0"));
279+
repo.addFeature("test.feature", "1.0.0", dummyJar("test.feature-1.0.0"),
280+
List.of(pluginMember("bundle.a", "9.9.9", null)));
281+
ws.addBasicPlugin(repo);
282+
283+
try (Project project = ws.getProject("p1")) {
284+
List<Container> containers = project.getBundles(Strategy.LOWEST,
285+
"test.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath");
286+
287+
softly.assertThat(project
288+
.check("Member bundle.a;version=9.9.9 of feature test.feature:1.0.0 not found with the exact"))
289+
.isTrue();
290+
softly.assertThat(containers)
291+
.extracting(Container::getBundleSymbolicName)
292+
.containsExactly("bundle.a");
293+
softly.assertThat(containers.get(0)
294+
.getVersion())
295+
.isEqualTo("1.0.0");
296+
}
297+
}
298+
}
299+
300+
@Test
301+
public void expandFeatureSkipsMissingOptionalInclude() throws Exception {
302+
try (Workspace ws = getWorkspace()) {
303+
FeatureRepo repo = new FeatureRepo();
304+
repo.addBundle("bundle.a", "1.0.0", dummyJar("bundle.a-1.0.0"));
305+
repo.addFeature("test.feature", "1.0.0", dummyJar("test.feature-1.0.0"), List.of(
306+
pluginMember("bundle.a", "1.0.0", null), //
307+
includeMember("no.such.feature", "1.0.0", true)));
308+
ws.addBasicPlugin(repo);
309+
310+
try (Project project = ws.getProject("p1")) {
311+
List<Container> containers = project.getBundles(Strategy.LOWEST,
312+
"test.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath");
313+
314+
softly.assertThat(project.check())
315+
.as("missing optional include must not cause errors")
316+
.isTrue();
317+
softly.assertThat(containers)
318+
.extracting(Container::getBundleSymbolicName)
319+
.containsExactly("bundle.a");
320+
}
321+
}
322+
}
323+
324+
@Test
325+
public void missingFeatureReportsError() throws Exception {
326+
try (Workspace ws = getWorkspace()) {
327+
ws.addBasicPlugin(new FeatureRepo());
328+
329+
try (Project project = ws.getProject("p1")) {
330+
List<Container> containers = project.getBundles(Strategy.LOWEST,
331+
"no.such.feature;version='1.0.0';type=org.eclipse.update.feature", "-buildpath");
332+
333+
softly.assertThat(project.check())
334+
.as("the error is carried by the container and reported by doPath later")
335+
.isTrue();
336+
softly.assertThat(containers)
337+
.hasSize(1);
338+
softly.assertThat(containers.get(0)
339+
.getType())
340+
.isEqualTo(Container.TYPE.ERROR);
341+
softly.assertThat(containers.get(0)
342+
.getError())
343+
.contains("Not found in");
344+
}
345+
}
346+
}
347+
}

0 commit comments

Comments
 (0)