Skip to content

Commit c6b3c7d

Browse files
laeubieclipse-tycho-bot
authored andcommitted
Support fragment host bundles in dependency check
Add SWT fragment resolution tests for dependency checker Add three integration test bundles for SWT host/fragment handling: - require-bundle-swt-direct: direct SWT dependency (no bump) - require-bundle-swt-reexport: SWT through JFace re-export chain (no bump) - require-bundle-swt-bump: Point.clone() detection proves fragment discovery (cherry picked from commit e8268bf)
1 parent 3592a05 commit c6b3c7d

19 files changed

Lines changed: 432 additions & 34 deletions

File tree

tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/DependencyChecker.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,36 @@ protected boolean checkMethodsInCollections(List<ClassCollection> collections, S
144144
String dependencyName, String packageNameFilter, Version version,
145145
Map<MethodSignature, Collection<String>> references, ArtifactVersion v, IInstallableUnit unit,
146146
String versionStr, org.eclipse.equinox.p2.metadata.Version matchedVersion, String dependencyType) {
147+
return checkMethodsInCollections(collections, methods, dependencyName, packageNameFilter, version, references,
148+
v, unit, versionStr, matchedVersion, dependencyType, Map.of());
149+
}
150+
151+
/**
152+
* Checks if methods are present in any of the given collections and reports
153+
* problems for missing ones. This supports checking against a main bundle's
154+
* classes combined with re-exported bundle classes, with optional provenance
155+
* information for re-exported packages.
156+
*
157+
* @param collections the class collections to check (main + re-exported)
158+
* @param methods the methods to find
159+
* @param dependencyName the name of the dependency
160+
* @param packageNameFilter optional filter to restrict provided method list
161+
* @param version the version being checked
162+
* @param references the references to the methods
163+
* @param v the artifact version
164+
* @param unit the installable unit
165+
* @param versionStr the version string from the manifest
166+
* @param matchedVersion the matched version
167+
* @param dependencyType the type of dependency (e.g., "Require-Bundle")
168+
* @param reexportProvenance map from package name to provenance description
169+
* (e.g., "re-exported from `org.eclipse.swt [3.133.0,4.0.0)`")
170+
* @return true if all methods were found
171+
*/
172+
protected boolean checkMethodsInCollections(List<ClassCollection> collections, Set<MethodSignature> methods,
173+
String dependencyName, String packageNameFilter, Version version,
174+
Map<MethodSignature, Collection<String>> references, ArtifactVersion v, IInstallableUnit unit,
175+
String versionStr, org.eclipse.equinox.p2.metadata.Version matchedVersion, String dependencyType,
176+
Map<String, String> reexportProvenance) {
147177
boolean ok = true;
148178
Set<MethodSignature> set = new HashSet<>();
149179
for (ClassCollection cc : collections) {
@@ -170,14 +200,16 @@ protected boolean checkMethodsInCollections(List<ClassCollection> collections, S
170200
}
171201
}
172202
}
203+
String provenance = reexportProvenance.getOrDefault(mthd.packageName(), "");
204+
String provenanceSuffix = provenance.isEmpty() ? "" : " (package `" + mthd.packageName() + "` " + provenance + ")";
173205
context.addProblem(new DependencyVersionProblem(dependencyName + "_" + version,
174206
String.format(
175-
"%s `%s %s` (compiled against `%s` provided by `%s %s`) includes `%s` (provided by `%s`) but this version is missing the method `%s#%s`",
207+
"%s `%s %s` (compiled against `%s` provided by `%s %s`) includes `%s` (provided by `%s`) but this version is missing the method `%s#%s`%s",
176208
dependencyType, dependencyName, versionStr,
177209
matchedVersion != null ? matchedVersion.toString()
178210
: org.eclipse.equinox.p2.metadata.Version.emptyVersion.toString(),
179211
unit.getId(), unit.getVersion(), version, v.getProvider(), mthd.className(),
180-
getMethodRef(mthd)),
212+
getMethodRef(mthd), provenanceSuffix),
181213
references.get(mthd), provided));
182214
ok = false;
183215
withError.add(dependencyName);

tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/analyze/RequireBundleChecker.java

Lines changed: 132 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public class RequireBundleChecker extends DependencyChecker {
5757
private final List<BundleCheckData> pendingChecks = new ArrayList<>();
5858
private final Map<Path, Set<String>> exportedPackagesCache = new HashMap<>();
5959
private final Map<Path, Map<String, String>> reexportCache = new HashMap<>();
60-
private final Map<String, Path> lowestArtifactCache = new HashMap<>();
60+
private final Map<String, ArtifactVersion> lowestArtifactCache = new HashMap<>();
6161

6262
private record BundleCheckData(String bundleName, String bundleVersionStr, IInstallableUnit unit,
6363
Version compiledAgainstVersion, org.eclipse.equinox.p2.metadata.Version matchedBundleVersion,
@@ -138,11 +138,16 @@ public void complete() throws MojoFailureException {
138138
// (compiled-against) version for accurate split-package detection
139139
Set<String> visited = new HashSet<>();
140140
visited.add(data.bundleName());
141-
List<Path> reexportArtifacts = resolveReexportChain(data.compiledAgainstArtifact(), visited,
142-
this::findCurrentBundleArtifact);
143-
for (Path reexportArtifact : reexportArtifacts) {
144-
exportedPkgs.addAll(getExportedPackagesFromJar(reexportArtifact));
145-
ClassCollection reCC = context.getClassCollection(reexportArtifact);
141+
List<ArtifactVersion> reexportArtifacts = resolveReexportChain(data.compiledAgainstArtifact(), visited,
142+
this::findCurrentBundleArtifactVersion);
143+
for (ArtifactVersion reexportAV : reexportArtifacts) {
144+
Path reexportPath = reexportAV.getArtifact();
145+
ClassCollection reCC = getClassCollectionWithFragments(reexportAV);
146+
if (reCC.provides().findAny().isEmpty()) {
147+
log.warn("Skip re-exported "+reexportPath+" because it seem not contain any classes");
148+
continue;
149+
}
150+
exportedPkgs.addAll(getExportedPackagesFromJar(reexportPath));
146151
reCC.provides().map(MethodSignature::className).forEach(classNames::add);
147152
}
148153
bundleExportedPackages.put(data.bundleName(), exportedPkgs);
@@ -201,10 +206,26 @@ private void checkBundle(BundleCheckData data, Set<String> splitPackages, Set<St
201206
// Resolve re-exported bundles for this version and include their packages
202207
Set<String> visited = new HashSet<>();
203208
visited.add(bundleName);
204-
List<Path> reexportArtifacts = resolveReexportChain(artifact, visited,
205-
this::findLowestMatchingBundleArtifact);
206-
for (Path reexportArtifact : reexportArtifacts) {
207-
exportedPackages.addAll(getExportedPackagesFromJar(reexportArtifact));
209+
Map<String, String> reexportProvenance = new HashMap<>();
210+
List<ArtifactVersion> reexportArtifacts = resolveReexportChain(artifact, visited,
211+
this::findLowestMatchingBundleArtifactVersion);
212+
List<ArtifactVersion> nonEmptyReexports = new ArrayList<>();
213+
for (ArtifactVersion reexportAV : reexportArtifacts) {
214+
Path reexportPath = reexportAV.getArtifact();
215+
ClassCollection reCC = getClassCollectionWithFragments(reexportAV);
216+
if (reCC.provides().findAny().isEmpty()) {
217+
continue;
218+
}
219+
nonEmptyReexports.add(reexportAV);
220+
String reexportBundleName = findBundleNameFromJar(reexportPath);
221+
for (String pkg : getExportedPackagesFromJar(reexportPath)) {
222+
exportedPackages.add(pkg);
223+
if (!reexportBundleName.isEmpty()) {
224+
reexportProvenance.put(pkg,
225+
String.format("re-exported by `%s` from require-bundle `%s`",
226+
reexportBundleName, bundleName));
227+
}
228+
}
208229
}
209230
Set<MethodSignature> bundleMethods = new TreeSet<>();
210231
Map<MethodSignature, Collection<String>> references = new HashMap<>();
@@ -230,12 +251,12 @@ private void checkBundle(BundleCheckData data, Set<String> splitPackages, Set<St
230251
}
231252
// Check against combined collection: main bundle + re-exported bundles
232253
List<ClassCollection> collections = new ArrayList<>();
233-
collections.add(context.getClassCollection(artifact));
234-
for (Path reexportArtifact : reexportArtifacts) {
235-
collections.add(context.getClassCollection(reexportArtifact));
254+
collections.add(getClassCollectionWithFragments(v));
255+
for (ArtifactVersion reexportAV : nonEmptyReexports) {
256+
collections.add(getClassCollectionWithFragments(reexportAV));
236257
}
237258
boolean ok = checkMethodsInCollections(collections, bundleMethods, bundleName, null, version, references,
238-
v, unit, bundleVersionStr, matchedBundleVersion, "Require-Bundle");
259+
v, unit, bundleVersionStr, matchedBundleVersion, "Require-Bundle", reexportProvenance);
239260
if (ok) {
240261
lowestVersion.merge(bundleName, version, (v1, v2) -> v1.compareTo(v2) > 0 ? v2 : v1);
241262
}
@@ -251,23 +272,24 @@ private void checkBundle(BundleCheckData data, Set<String> splitPackages, Set<St
251272
* @param bundleJarPath the JAR to read re-exports from
252273
* @param visited set of already-visited bundle names (for cycle
253274
* prevention)
254-
* @param artifactFinder strategy to find a bundle artifact given name and range
255-
* @return list of artifact paths for all transitively re-exported bundles
275+
* @param artifactFinder strategy to find a bundle artifact version given name
276+
* and range
277+
* @return list of artifact versions for all transitively re-exported bundles
256278
*/
257-
private List<Path> resolveReexportChain(Path bundleJarPath, Set<String> visited,
258-
BiFunction<String, VersionRange, Path> artifactFinder) {
259-
List<Path> result = new ArrayList<>();
279+
private List<ArtifactVersion> resolveReexportChain(Path bundleJarPath, Set<String> visited,
280+
BiFunction<String, VersionRange, ArtifactVersion> artifactFinder) {
281+
List<ArtifactVersion> result = new ArrayList<>();
260282
Map<String, String> reexports = getReexportedBundlesFromJar(bundleJarPath);
261283
for (Map.Entry<String, String> entry : reexports.entrySet()) {
262284
String reexportName = entry.getKey();
263285
if (!visited.add(reexportName)) {
264286
continue;
265287
}
266288
VersionRange range = VersionRange.valueOf(entry.getValue());
267-
Path reexportArtifact = artifactFinder.apply(reexportName, range);
268-
if (reexportArtifact != null) {
269-
result.add(reexportArtifact);
270-
result.addAll(resolveReexportChain(reexportArtifact, visited, artifactFinder));
289+
ArtifactVersion reexportAV = artifactFinder.apply(reexportName, range);
290+
if (reexportAV != null && reexportAV.getArtifact() != null) {
291+
result.add(reexportAV);
292+
result.addAll(resolveReexportChain(reexportAV.getArtifact(), visited, artifactFinder));
271293
}
272294
}
273295
return result;
@@ -280,9 +302,9 @@ private List<Path> resolveReexportChain(Path bundleJarPath, Set<String> visited,
280302
*
281303
* @param bundleName the symbolic name of the bundle
282304
* @param range the version range to match
283-
* @return the path to the lowest matching artifact, or {@code null}
305+
* @return the lowest matching artifact version, or {@code null}
284306
*/
285-
private Path findLowestMatchingBundleArtifact(String bundleName, VersionRange range) {
307+
private ArtifactVersion findLowestMatchingBundleArtifactVersion(String bundleName, VersionRange range) {
286308
String cacheKey = bundleName + ":" + range;
287309
return lowestArtifactCache.computeIfAbsent(cacheKey, k -> {
288310
Optional<IInstallableUnit> bundleUnit = ArtifactMatcher.findBundle(bundleName, units);
@@ -293,21 +315,21 @@ private Path findLowestMatchingBundleArtifact(String bundleName, VersionRange ra
293315
return context.getVersionProviders().stream()
294316
.flatMap(avp -> avp.getBundleVersions(iu, bundleName, range, context.getProject()))
295317
.filter(av -> av.getVersion() != null && av.getArtifact() != null)
296-
.min(Comparator.comparing(ArtifactVersion::getVersion)).map(ArtifactVersion::getArtifact)
318+
.min(Comparator.comparing(ArtifactVersion::getVersion))
297319
.orElse(null);
298320
});
299321
}
300322

301323
/**
302-
* Finds the current (compiled-against / target platform) artifact for a bundle.
303-
* Used during Phase 1 to determine class names for accurate split-package
304-
* detection with re-exported bundles.
324+
* Finds the current (compiled-against / target platform) artifact version for a
325+
* bundle. Used during Phase 1 to determine class names for accurate
326+
* split-package detection with re-exported bundles.
305327
*
306328
* @param bundleName the symbolic name of the bundle
307329
* @param range the version range (used to filter available versions)
308-
* @return the path to the current version's artifact, or {@code null}
330+
* @return the current version's artifact version, or {@code null}
309331
*/
310-
private Path findCurrentBundleArtifact(String bundleName, VersionRange range) {
332+
private ArtifactVersion findCurrentBundleArtifactVersion(String bundleName, VersionRange range) {
311333
Optional<IInstallableUnit> bundleUnit = ArtifactMatcher.findBundle(bundleName, units);
312334
if (bundleUnit.isEmpty()) {
313335
return null;
@@ -320,13 +342,37 @@ private Path findCurrentBundleArtifact(String bundleName, VersionRange range) {
320342
return context.getVersionProviders().stream()
321343
.flatMap(avp -> avp.getBundleVersions(iu, bundleName, range, context.getProject()))
322344
.filter(av -> av.getVersion() != null && av.getVersion().equals(current) && av.getArtifact() != null)
323-
.findFirst().map(ArtifactVersion::getArtifact).orElse(null);
345+
.findFirst().orElse(null);
324346
}
325347

326348
private Set<String> getExportedPackagesFromJar(Path jarPath) {
327349
return exportedPackagesCache.computeIfAbsent(jarPath, this::readExportedPackagesFromJar);
328350
}
329351

352+
/**
353+
* Reads the {@code Bundle-SymbolicName} from a JAR's manifest.
354+
*
355+
* @param jarPath the JAR file to read
356+
* @return the symbolic name, or an empty string if unavailable
357+
*/
358+
private String findBundleNameFromJar(Path jarPath) {
359+
try (JarFile jar = new JarFile(jarPath.toFile())) {
360+
Manifest manifest = jar.getManifest();
361+
if (manifest != null) {
362+
String bsn = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
363+
if (bsn != null) {
364+
ManifestElement[] elements = ManifestElement.parseHeader(Constants.BUNDLE_SYMBOLICNAME, bsn);
365+
if (elements != null && elements.length > 0) {
366+
return elements[0].getValue();
367+
}
368+
}
369+
}
370+
} catch (BundleException | java.io.IOException e) {
371+
context.getLog().debug("Could not read bundle name from " + jarPath + ": " + e);
372+
}
373+
return "";
374+
}
375+
330376
private Set<String> readExportedPackagesFromJar(Path jarPath) {
331377
Set<String> packages = new HashSet<>();
332378
try (JarFile jar = new JarFile(jarPath.toFile())) {
@@ -348,6 +394,60 @@ private Set<String> readExportedPackagesFromJar(Path jarPath) {
348394
return packages;
349395
}
350396

397+
/**
398+
* Checks whether a JAR's manifest declares {@code Eclipse-ExtensibleAPI: true},
399+
* indicating it is a host bundle that expects fragments to provide classes.
400+
*
401+
* @param jarPath the JAR file to check
402+
* @return {@code true} if the bundle has extensible API
403+
*/
404+
private boolean isExtensibleAPI(Path jarPath) {
405+
try (JarFile jar = new JarFile(jarPath.toFile())) {
406+
Manifest manifest = jar.getManifest();
407+
if (manifest != null) {
408+
return "true".equalsIgnoreCase(manifest.getMainAttributes().getValue("Eclipse-ExtensibleAPI"));
409+
}
410+
} catch (java.io.IOException e) {
411+
context.getLog().debug("Could not read manifest from " + jarPath + ": " + e);
412+
}
413+
return false;
414+
}
415+
416+
/**
417+
* Gets a {@link ClassCollection} for a bundle artifact version, resolving
418+
* fragment classes when the JAR is an empty host bundle with
419+
* {@code Eclipse-ExtensibleAPI: true}. Uses
420+
* {@link ArtifactVersion#fragments()} to find fragments in the same
421+
* repository where the host was found.
422+
*
423+
* @param av the artifact version to analyze
424+
* @return the class collection (potentially augmented with fragment classes)
425+
*/
426+
private ClassCollection getClassCollectionWithFragments(ArtifactVersion av) {
427+
Path jarPath = av.getArtifact();
428+
if (jarPath == null) {
429+
return new ClassCollection();
430+
}
431+
ClassCollection hostCC = context.getClassCollection(jarPath);
432+
if (hostCC.provides().findAny().isPresent()) {
433+
return hostCC;
434+
}
435+
// Empty JAR — check if it's an extensible API host
436+
if (!isExtensibleAPI(jarPath)) {
437+
return hostCC;
438+
}
439+
// Ask the artifact version for fragments from the same repository
440+
ArtifactVersion fragment = av.fragments()
441+
.filter(f -> f.getArtifact() != null)
442+
.findFirst().orElse(null);
443+
if (fragment == null) {
444+
context.getLog().debug("No fragment found for extensible API host " + av);
445+
return hostCC;
446+
}
447+
context.getLog().debug("Resolved fragment for " + av + ": " + fragment);
448+
return context.getClassCollection(fragment.getArtifact());
449+
}
450+
351451
/**
352452
* Reads a JAR's manifest for {@code Require-Bundle} entries with
353453
* {@code visibility:=reexport} and returns them as a map from bundle symbolic

tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/provider/EclipseIndexBundleArtifactVersion.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,27 @@
1212
*******************************************************************************/
1313
package org.eclipse.tycho.baseline.provider;
1414

15+
import java.util.Collection;
1516
import java.util.List;
1617
import java.util.Optional;
18+
import java.util.stream.Stream;
1719

1820
import org.codehaus.plexus.logging.Logger;
21+
import org.eclipse.equinox.internal.p2.metadata.IRequiredCapability;
1922
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
23+
import org.eclipse.equinox.p2.metadata.IRequirement;
2024
import org.eclipse.equinox.p2.metadata.Version;
25+
import org.eclipse.equinox.p2.metadata.VersionRange;
2126
import org.eclipse.equinox.p2.query.QueryUtil;
2227
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
28+
import org.eclipse.tycho.artifacts.ArtifactVersion;
2329
import org.eclipse.tycho.copyfrom.oomph.P2Index.Repository;
30+
import org.eclipse.tycho.p2maven.tmp.BundlesAction;
2431

2532
class EclipseIndexBundleArtifactVersion extends AbstractEclipseArtifactVersion {
2633

2734
private final String bundleName;
35+
private List<ArtifactVersion> cachedFragments;
2836

2937
public EclipseIndexBundleArtifactVersion(EclipseIndexArtifactVersionProvider provider,
3038
List<Repository> repositories, String bundleName, Version version, Logger logger) {
@@ -55,4 +63,61 @@ protected Optional<IInstallableUnit> findUnit() {
5563
}
5664
return Optional.empty();
5765
}
66+
67+
@Override
68+
public Stream<ArtifactVersion> fragments() {
69+
if (cachedFragments != null) {
70+
return cachedFragments.stream();
71+
}
72+
// Ensure the unit is resolved so we know which repo it came from
73+
Optional<IInstallableUnit> hostUnit = unit;
74+
if (hostUnit == null) {
75+
hostUnit = findUnit();
76+
}
77+
if (hostUnit == null || hostUnit.isEmpty() || unitRepo == null) {
78+
cachedFragments = List.of();
79+
return cachedFragments.stream();
80+
}
81+
Version hostVersion = hostUnit.get().getVersion();
82+
try {
83+
org.apache.maven.model.Repository r = new org.apache.maven.model.Repository();
84+
r.setUrl(unitRepo.getLocation().toString());
85+
IMetadataRepository metadataRepository = getVersionProvider().repositoryManager
86+
.getMetadataRepository(r);
87+
Collection<IInstallableUnit> candidates = metadataRepository
88+
.query(QueryUtil.createMatchQuery(
89+
"providedCapabilities.exists(x | x.namespace == $0 && x.name == $1)",
90+
BundlesAction.CAPABILITY_NS_OSGI_FRAGMENT, bundleName),
91+
null)
92+
.toUnmodifiableSet();
93+
cachedFragments = candidates.stream()
94+
.filter(candidate -> fragmentMatchesHostVersion(candidate, bundleName, hostVersion))
95+
.map(fragmentIU -> (ArtifactVersion) new EclipseIndexFragmentArtifactVersion(
96+
getVersionProvider(), unitRepo, fragmentIU))
97+
.toList();
98+
return cachedFragments.stream();
99+
} catch (Exception e) {
100+
getVersionProvider().logger.error(
101+
"Failed to query fragments for " + bundleName + " from " + unitRepo.getLocation() + ": " + e);
102+
cachedFragments = List.of();
103+
return cachedFragments.stream();
104+
}
105+
}
106+
107+
/**
108+
* Checks whether a fragment IU's {@code Fragment-Host} requirement includes the
109+
* given host version.
110+
*/
111+
private static boolean fragmentMatchesHostVersion(IInstallableUnit fragmentIU, String hostBundleName,
112+
Version hostVersion) {
113+
for (IRequirement req : fragmentIU.getRequirements()) {
114+
if (req instanceof IRequiredCapability rc
115+
&& BundlesAction.CAPABILITY_NS_OSGI_BUNDLE.equals(rc.getNamespace())
116+
&& hostBundleName.equals(rc.getName())) {
117+
VersionRange range = rc.getRange();
118+
return range.isIncluded(hostVersion);
119+
}
120+
}
121+
return false;
122+
}
58123
}

0 commit comments

Comments
 (0)