Skip to content

Commit e8268bf

Browse files
committed
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
1 parent 6246117 commit e8268bf

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)