Skip to content

Commit 59546e7

Browse files
Copilotlaeubi
authored andcommitted
Add Require-Bundle dependency checking to DependencyCheckMojo
1 parent a3cd089 commit 59546e7

File tree

12 files changed

+687
-185
lines changed

12 files changed

+687
-185
lines changed

tycho-baseline-plugin/src/main/java/org/eclipse/tycho/baseline/DependencyCheckMojo.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.eclipse.tycho.baseline.analyze.ImportPackageChecker;
4848
import org.eclipse.tycho.baseline.analyze.JrtClasses;
4949
import org.eclipse.tycho.baseline.analyze.MethodSignature;
50+
import org.eclipse.tycho.baseline.analyze.RequireBundleChecker;
5051
import org.eclipse.tycho.core.MarkdownBuilder;
5152
import org.eclipse.tycho.core.TychoProjectManager;
5253
import org.eclipse.tycho.core.maven.OSGiJavaToolchain;
@@ -56,6 +57,7 @@
5657
import org.eclipse.tycho.model.manifest.MutableBundleManifest;
5758
import org.osgi.framework.BundleException;
5859
import org.osgi.framework.InvalidSyntaxException;
60+
import org.osgi.framework.namespace.BundleNamespace;
5961
import org.osgi.framework.namespace.PackageNamespace;
6062
import org.osgi.resource.Namespace;
6163

@@ -148,6 +150,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
148150

149151
// Create checkers that maintain their own state
150152
ImportPackageChecker importPackageChecker = new ImportPackageChecker(context, units, usages);
153+
RequireBundleChecker requireBundleChecker = new RequireBundleChecker(context, units, usages);
151154

152155
for (GenericInfo genericInfo : requirements) {
153156
if (PackageNamespace.PACKAGE_NAMESPACE.equals(genericInfo.getNamespace())) {
@@ -156,6 +159,13 @@ public void execute() throws MojoExecutionException, MojoFailureException {
156159
String packageVersion = pkgInfo.getOrDefault(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, "0.0.0");
157160
String packageName = pkgInfo.get(PackageNamespace.PACKAGE_NAMESPACE);
158161
importPackageChecker.check(packageName, packageVersion);
162+
} else if (BundleNamespace.BUNDLE_NAMESPACE.equals(genericInfo.getNamespace())) {
163+
Map<String, String> bundleInfo = getVersionInfo(genericInfo,
164+
BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE);
165+
String bundleVersionStr = bundleInfo.getOrDefault(BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE,
166+
"0.0.0");
167+
String bundleName = bundleInfo.get(BundleNamespace.BUNDLE_NAMESPACE);
168+
requireBundleChecker.check(bundleName, bundleVersionStr);
159169
}
160170
}
161171
List<DependencyVersionProblem> dependencyProblems = context.getProblems();
@@ -166,6 +176,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
166176
try {
167177
MutableBundleManifest manifest = MutableBundleManifest.read(manifestFile);
168178
boolean changed = importPackageChecker.applySuggestions(manifest);
179+
changed |= requireBundleChecker.applySuggestions(manifest);
169180
if (changed) {
170181
MutableBundleManifest.write(manifest, manifestFile);
171182
}
@@ -213,7 +224,8 @@ public void execute() throws MojoExecutionException, MojoFailureException {
213224
}
214225
results.add("");
215226
}
216-
importPackageChecker.reportSuggestions(results, log);
227+
importPackageChecker.reportSuggestions(results, log);
228+
requireBundleChecker.reportSuggestions(results, log);
217229
results.write();
218230
}
219231

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Christoph Läubrich - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.tycho.baseline.analyze;
14+
15+
import java.nio.file.Path;
16+
import java.util.Collection;
17+
import java.util.HashMap;
18+
import java.util.HashSet;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.Optional;
22+
import java.util.Set;
23+
import java.util.TreeSet;
24+
import java.util.jar.JarFile;
25+
import java.util.jar.Manifest;
26+
import java.util.stream.Collectors;
27+
28+
import org.apache.maven.plugin.MojoFailureException;
29+
import org.apache.maven.plugin.logging.Log;
30+
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
31+
import org.eclipse.osgi.util.ManifestElement;
32+
import org.eclipse.tycho.artifacts.ArtifactVersion;
33+
import org.eclipse.tycho.core.MarkdownBuilder;
34+
import org.eclipse.tycho.core.resolver.target.ArtifactMatcher;
35+
import org.eclipse.tycho.model.manifest.MutableBundleManifest;
36+
import org.osgi.framework.BundleException;
37+
import org.osgi.framework.Constants;
38+
import org.osgi.framework.Version;
39+
import org.osgi.framework.VersionRange;
40+
41+
/**
42+
* Checker for Require-Bundle dependencies.
43+
*/
44+
public class RequireBundleChecker extends DependencyChecker {
45+
46+
private final Collection<IInstallableUnit> units;
47+
private final List<ClassUsage> usages;
48+
49+
/**
50+
* Creates a new Require-Bundle checker.
51+
*
52+
* @param context the check context
53+
* @param units the installable units
54+
* @param usages the class usages from the project
55+
*/
56+
public RequireBundleChecker(CheckContext context, Collection<IInstallableUnit> units, List<ClassUsage> usages) {
57+
super(context);
58+
this.units = units;
59+
this.usages = usages;
60+
}
61+
62+
/**
63+
* Checks a Require-Bundle dependency.
64+
*
65+
* @param bundleName the symbolic name of the required bundle
66+
* @param bundleVersionStr the version range string
67+
* @throws MojoFailureException if checks failed
68+
*/
69+
public void check(String bundleName, String bundleVersionStr) throws MojoFailureException {
70+
Log log = context.getLog();
71+
Optional<IInstallableUnit> bundleProvidingUnit = ArtifactMatcher.findBundle(bundleName, units);
72+
if (bundleProvidingUnit.isEmpty()) {
73+
return;
74+
}
75+
IInstallableUnit unit = bundleProvidingUnit.get();
76+
org.eclipse.equinox.p2.metadata.Version matchedBundleVersion = unit.getVersion();
77+
if (matchedBundleVersion.isOSGiCompatible()) {
78+
Version current = new Version(matchedBundleVersion.toString());
79+
allVersions.computeIfAbsent(bundleName, nil -> new TreeSet<>()).add(current);
80+
lowestVersion.put(bundleName, current);
81+
}
82+
VersionRange versionRange = VersionRange.valueOf(bundleVersionStr);
83+
List<ArtifactVersion> list = context.getVersionProviders().stream()
84+
.flatMap(avp -> avp.getBundleVersions(unit, bundleName, versionRange, context.getProject())).toList();
85+
if (log.isDebugEnabled()) {
86+
log.debug("== Bundle " + bundleName + " " + bundleVersionStr + " is provided by " + unit
87+
+ " with version range " + versionRange + ", matching versions: "
88+
+ list.stream().map(av -> av.getVersion()).map(String::valueOf).collect(Collectors.joining(", ")));
89+
}
90+
for (ArtifactVersion v : list) {
91+
Version version = v.getVersion();
92+
if (version == null) {
93+
continue;
94+
}
95+
if (!allVersions.computeIfAbsent(bundleName, nil -> new TreeSet<>()).add(version)) {
96+
continue;
97+
}
98+
Path artifact = v.getArtifact();
99+
log.debug(v + "=" + artifact);
100+
if (artifact == null) {
101+
continue;
102+
}
103+
// Determine exported packages for THIS specific version from the JAR manifest
104+
Set<String> exportedPackages = getExportedPackagesFromJar(artifact);
105+
if (exportedPackages.isEmpty()) {
106+
continue;
107+
}
108+
// Collect methods our code uses from these exported packages
109+
Set<MethodSignature> bundleMethods = new TreeSet<>();
110+
Map<MethodSignature, Collection<String>> references = new HashMap<>();
111+
for (String packageName : exportedPackages) {
112+
bundleMethods.addAll(collectMethodsForPackage(usages, packageName));
113+
references.putAll(collectReferencesForPackage(usages, packageName));
114+
}
115+
if (bundleMethods.isEmpty()) {
116+
continue;
117+
}
118+
if (log.isDebugEnabled()) {
119+
for (MethodSignature signature : bundleMethods) {
120+
log.debug("Referenced from bundle " + bundleName + " version " + version + ": " + signature.id());
121+
}
122+
}
123+
ClassCollection collection = context.getClassCollection(artifact);
124+
// For Require-Bundle, pass null as packageNameFilter since methods can come
125+
// from different packages
126+
boolean ok = checkMethodsInCollection(collection, bundleMethods, bundleName, null, version, references, v,
127+
unit, bundleVersionStr, matchedBundleVersion, "Require-Bundle");
128+
if (ok) {
129+
lowestVersion.merge(bundleName, version, (v1, v2) -> v1.compareTo(v2) > 0 ? v2 : v1);
130+
}
131+
}
132+
}
133+
134+
private Set<String> getExportedPackagesFromJar(Path jarPath) {
135+
Set<String> packages = new HashSet<>();
136+
try (JarFile jar = new JarFile(jarPath.toFile())) {
137+
Manifest manifest = jar.getManifest();
138+
if (manifest != null) {
139+
String exportPackage = manifest.getMainAttributes().getValue(Constants.EXPORT_PACKAGE);
140+
if (exportPackage != null) {
141+
ManifestElement[] elements = ManifestElement.parseHeader(Constants.EXPORT_PACKAGE, exportPackage);
142+
if (elements != null) {
143+
for (ManifestElement element : elements) {
144+
packages.add(element.getValue());
145+
}
146+
}
147+
}
148+
}
149+
} catch (BundleException | java.io.IOException e) {
150+
context.getLog().debug("Could not read exported packages from " + jarPath + ": " + e);
151+
}
152+
return packages;
153+
}
154+
155+
@Override
156+
public boolean applySuggestions(MutableBundleManifest manifest) {
157+
if (withError.isEmpty()) {
158+
return false;
159+
}
160+
Map<String, String> requiredBundleVersions = manifest.getRequiredBundleVersions();
161+
Map<String, String> bundleUpdates = new HashMap<>();
162+
Map<String, Version> lowestBundleVersion = getLowestVersions();
163+
for (String bundleName : withError) {
164+
Version lowestVersion = lowestBundleVersion.getOrDefault(bundleName, Version.emptyVersion);
165+
String current = requiredBundleVersions.get(bundleName);
166+
if (current == null) {
167+
bundleUpdates.put(bundleName,
168+
String.format("[%s,%d)", lowestVersion, (lowestVersion.getMajor() + 1)));
169+
} else {
170+
VersionRange range = VersionRange.valueOf(current);
171+
Version right = range.getRight();
172+
bundleUpdates.put(bundleName,
173+
String.format("[%s,%s%c", lowestVersion, right, range.getRightType()));
174+
}
175+
}
176+
manifest.updateRequiredBundleVersions(bundleUpdates);
177+
return true;
178+
}
179+
180+
@Override
181+
public void reportSuggestions(MarkdownBuilder results, Log log) {
182+
reportVersionContrainSuggestions("bundle", results, log);
183+
}
184+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Christoph Läubrich - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.tycho.baseline.provider;
14+
15+
import java.io.IOException;
16+
import java.io.OutputStream;
17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Objects;
22+
import java.util.Optional;
23+
24+
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
25+
import org.eclipse.equinox.p2.metadata.Version;
26+
import org.eclipse.tycho.artifacts.ArtifactVersion;
27+
import org.eclipse.tycho.copyfrom.oomph.P2Index.Repository;
28+
29+
/**
30+
* Base class for Eclipse p2 index based {@link ArtifactVersion} implementations
31+
* that share common artifact download and unit lookup logic.
32+
*/
33+
abstract class AbstractEclipseArtifactVersion implements ArtifactVersion {
34+
35+
private final EclipseIndexArtifactVersionProvider provider;
36+
protected final Version version;
37+
protected final List<Repository> repositories;
38+
private final org.osgi.framework.Version osgiVersion;
39+
private Path tempFile;
40+
protected Optional<IInstallableUnit> unit;
41+
protected Repository unitRepo;
42+
43+
protected AbstractEclipseArtifactVersion(EclipseIndexArtifactVersionProvider provider,
44+
List<Repository> repositories, Version version) {
45+
this.provider = provider;
46+
this.repositories = repositories;
47+
this.version = version;
48+
this.osgiVersion = org.osgi.framework.Version.parseVersion(version.getOriginal());
49+
}
50+
51+
protected EclipseIndexArtifactVersionProvider getVersionProvider() {
52+
return provider;
53+
}
54+
55+
@Override
56+
public Path getArtifact() {
57+
if (tempFile == null) {
58+
IInstallableUnit iu = getUnit().orElse(null);
59+
if (iu != null) {
60+
Path file;
61+
try {
62+
file = Files.createTempFile(iu.getId(), ".jar");
63+
} catch (IOException e) {
64+
return null;
65+
}
66+
file.toFile().deleteOnExit();
67+
List<Repository> list = new ArrayList<>(repositories);
68+
if (unitRepo != null) {
69+
list.remove(unitRepo);
70+
list.add(0, unitRepo);
71+
}
72+
for (Repository repository : list) {
73+
try {
74+
org.apache.maven.model.Repository r = new org.apache.maven.model.Repository();
75+
r.setUrl(repository.getLocation().toString());
76+
try (OutputStream stream = Files.newOutputStream(file)) {
77+
provider.repositoryManager.downloadArtifact(iu,
78+
provider.repositoryManager.getArtifactRepository(r), stream);
79+
return tempFile = file;
80+
}
81+
} catch (Exception e) {
82+
provider.logger.error("Fetch artifact for unit " + iu.getId() + " from "
83+
+ repository.getLocation() + " failed: " + e);
84+
}
85+
}
86+
file.toFile().delete();
87+
}
88+
}
89+
return tempFile;
90+
}
91+
92+
/**
93+
* Resolves the {@link IInstallableUnit} for this artifact version from the
94+
* available repositories.
95+
*
96+
* @return an Optional containing the unit if found
97+
*/
98+
protected abstract Optional<IInstallableUnit> findUnit();
99+
100+
private Optional<IInstallableUnit> getUnit() {
101+
if (unit == null) {
102+
unit = findUnit();
103+
}
104+
return Objects.requireNonNullElse(unit, Optional.empty());
105+
}
106+
107+
@Override
108+
public org.osgi.framework.Version getVersion() {
109+
return osgiVersion;
110+
}
111+
112+
@Override
113+
public String toString() {
114+
if (unit != null && unit.isPresent()) {
115+
return getVersion() + " (" + unit.get() + ")";
116+
}
117+
return getVersion().toString();
118+
}
119+
120+
@Override
121+
public String getProvider() {
122+
return getUnit().map(u -> u.getId() + " " + u.getVersion()).orElse(null);
123+
}
124+
}

0 commit comments

Comments
 (0)