Skip to content

Commit 9fc47e5

Browse files
committed
Add a new tycho-wrap:verify mojo
A common way to ensure compatibility and integration of java libraries is to enable the generation of an OSGi manifest automatically. As one can not expect such project to be OSGi experts there is often a problem that these do not feel comfortable with adding such without any mean to validate the outcome. Also it is often not obvious when using a new dependency if this would hinder integration with OSGi or to ensure the actual result is usable without complex and hard to maintain full blown integration-test scenarios that project hardly can handle over a long time. This now introduces a new `tycho-wrap:verify` mojo that tries to fill the gap here between full integration testing and a basic validation with the intention to give clear hint how to handle issues.
1 parent 74de78e commit 9fc47e5

File tree

3 files changed

+352
-0
lines changed

3 files changed

+352
-0
lines changed

tycho-wrap-plugin/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
<artifactId>biz.aQute.bnd.maven</artifactId>
3737
<version>7.1.0</version>
3838
</dependency>
39+
<dependency>
40+
<groupId>org.eclipse.platform</groupId>
41+
<artifactId>org.eclipse.osgi</artifactId>
42+
</dependency>
3943
</dependencies>
4044
<build>
4145
<plugins>
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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.wrap;
14+
15+
import java.io.File;
16+
import java.io.IOException;
17+
import java.util.Comparator;
18+
import java.util.LinkedHashMap;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.Map.Entry;
22+
import java.util.Optional;
23+
import java.util.Set;
24+
import java.util.TreeMap;
25+
import java.util.function.Predicate;
26+
import java.util.jar.JarFile;
27+
import java.util.jar.Manifest;
28+
29+
import org.apache.felix.resolver.Util;
30+
import org.apache.maven.artifact.Artifact;
31+
import org.apache.maven.plugin.AbstractMojo;
32+
import org.apache.maven.plugin.MojoExecutionException;
33+
import org.apache.maven.plugin.MojoFailureException;
34+
import org.apache.maven.plugin.logging.Log;
35+
import org.apache.maven.plugins.annotations.Component;
36+
import org.apache.maven.plugins.annotations.LifecyclePhase;
37+
import org.apache.maven.plugins.annotations.Mojo;
38+
import org.apache.maven.plugins.annotations.Parameter;
39+
import org.apache.maven.plugins.annotations.ResolutionScope;
40+
import org.apache.maven.project.MavenProject;
41+
import org.eclipse.osgi.container.ModuleContainer;
42+
import org.osgi.framework.Version;
43+
import org.osgi.framework.namespace.ExecutionEnvironmentNamespace;
44+
import org.osgi.framework.namespace.PackageNamespace;
45+
import org.osgi.resource.Capability;
46+
import org.osgi.resource.Requirement;
47+
import org.osgi.resource.Resource;
48+
49+
import aQute.bnd.build.model.EE;
50+
import aQute.bnd.osgi.Analyzer;
51+
import aQute.bnd.osgi.Constants;
52+
import aQute.bnd.osgi.Jar;
53+
import aQute.bnd.osgi.resource.CapReqBuilder;
54+
import aQute.bnd.osgi.resource.ResourceBuilder;
55+
import aQute.bnd.osgi.resource.ResourceUtils;
56+
57+
/**
58+
* This mojos takes the project artifact and verify it can be resolved inside
59+
* OSGiusing the projects dependency artifacts.
60+
*/
61+
@Mojo(name = "verify", requiresProject = true, threadSafe = true, defaultPhase = LifecyclePhase.VERIFY, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
62+
public class VerifyMojo extends AbstractMojo {
63+
64+
private static final String CHECK = " ✓ ";
65+
private static final String FAIL = " ☹ ";
66+
private static final String WARN = " ⚠ ";
67+
68+
@Component
69+
private MavenProject project;
70+
71+
@Parameter(defaultValue = "jar")
72+
private Set<String> packaging;
73+
74+
@Parameter
75+
private Set<String> ignored;
76+
77+
@Override
78+
public void execute() throws MojoExecutionException, MojoFailureException {
79+
Log log = getLog();
80+
if (!packaging.contains(project.getPackaging())) {
81+
log.info("Skipped because package type does not match: " + project.getPackaging());
82+
return;
83+
}
84+
Map<Artifact, Resource> map = analyzeArtifacts();
85+
List<Capability> jvmCapabilities = getJVMResource().getCapabilities(null);
86+
Map<String, String> missingRequirements = new TreeMap<>();
87+
int ignoredProblems = 0;
88+
try {
89+
Resource resource = createProjectResource();
90+
List<Requirement> requirements = resource.getRequirements(PackageNamespace.PACKAGE_NAMESPACE);
91+
if (requirements.isEmpty()) {
92+
log.info("It has requirements specified!");
93+
} else {
94+
log.info("It has " + requirements.size() + " requirements:");
95+
for (Requirement requirement : requirements) {
96+
String req = ModuleContainer.toString(requirement);
97+
Predicate<Capability> matcher = ResourceUtils.matcher(requirement);
98+
if (jvmCapabilities.stream().anyMatch(matcher)) {
99+
log.info(CHECK + req + " (provided by the JVM)");
100+
} else {
101+
Optional<Entry<Artifact, Resource>> artifactMatch = map.entrySet().stream()
102+
.filter(entry -> entry.getValue().getCapabilities(null).stream().anyMatch(matcher))
103+
.sorted(new Comparator<Entry<Artifact, Resource>>() {
104+
105+
@Override
106+
public int compare(Entry<Artifact, Resource> o1, Entry<Artifact, Resource> o2) {
107+
boolean wr1 = o1.getValue() instanceof WrappedResource;
108+
boolean wr2 = o2.getValue() instanceof WrappedResource;
109+
if (wr1 == wr2) {
110+
return 0;
111+
}
112+
if (wr1) {
113+
return -1;
114+
}
115+
if (wr2) {
116+
return 1;
117+
}
118+
return 0;
119+
}
120+
}).findFirst();
121+
if (artifactMatch.isEmpty()) {
122+
log.info(FAIL + req + " not found in the artifacts of the current project!");
123+
if (!Util.isOptional(requirement)) {
124+
if (isIgnored(req)) {
125+
ignoredProblems++;
126+
} else {
127+
missingRequirements.put(req,
128+
"""
129+
Seems not provided anywhere in the project artifacts!
130+
131+
You can exclude the import if this is satisfied otherwise or make it optional if it is provided by some other ways.
132+
""");
133+
}
134+
}
135+
} else {
136+
Entry<Artifact, Resource> entry = artifactMatch.get();
137+
if (entry.getValue() instanceof WrappedResource) {
138+
log.info(WARN + req + " (can be provided by " + entry.getKey().getId()
139+
+ " but artifact is not an OSGi bundle!)");
140+
if (!Util.isOptional(requirement)) {
141+
if (isIgnored(req)) {
142+
ignoredProblems++;
143+
} else {
144+
missingRequirements.put(req,
145+
"""
146+
Not provided by an OSGi bundle!
147+
148+
This does not mean it can not work but is harder to use. You can check if there is an alternative dependency that supplies OSGi metadata already or suggest doing so to the maintainer.
149+
You might also choose to ignore this issue and either let your consumers find a way to provide the missing requirement or ask them to help out with this issue.
150+
""");
151+
}
152+
}
153+
} else {
154+
log.info(CHECK + req + " (provided by " + entry.getKey().getId() + ")");
155+
}
156+
}
157+
}
158+
}
159+
}
160+
List<Capability> capabilities = resource.getCapabilities(PackageNamespace.PACKAGE_NAMESPACE);
161+
if (capabilities.isEmpty()) {
162+
log.info("It has no capabilities specified!");
163+
} else {
164+
log.info("It provides " + capabilities.size() + " capabilities:");
165+
for (Capability capability : capabilities) {
166+
log.info(" - " + ModuleContainer.toString(capability));
167+
}
168+
}
169+
if (ignoredProblems > 0) {
170+
log.info(WARN + ignoredProblems + " problems are currently ignored!");
171+
}
172+
} catch (IOException e) {
173+
throw new MojoExecutionException(e);
174+
}
175+
if (missingRequirements.isEmpty()) {
176+
return;
177+
}
178+
log.error("Problems where detected that will hinder your artifact from being used in an OSGi environment:");
179+
for (Entry<String, String> entry : missingRequirements.entrySet()) {
180+
log.error("\t" + entry.getKey() + " --> " + entry.getValue());
181+
}
182+
log.info(
183+
"To ignore the problem temporary you can add the error to the <ignored> list in the <configuration> section of the plugin."
184+
+ System.lineSeparator());
185+
log.info(
186+
"If you find the provided instructions insufficient please report an issue at https://github.com/eclipse-tycho/tycho/issues so we can enhance them!");
187+
StringBuilder sb = new StringBuilder();
188+
sb.append(missingRequirements.size());
189+
sb.append(
190+
" requirements can possibly not satisfied in an OSGi environment, see the logfile for more details on specific items!");
191+
192+
throw new MojoFailureException(sb.toString());
193+
}
194+
195+
private boolean isIgnored(String req) {
196+
if (ignored != null) {
197+
return ignored.contains(req);
198+
}
199+
return false;
200+
}
201+
202+
private Resource createProjectResource() throws IOException, MojoFailureException {
203+
try (JarFile jar = new JarFile(project.getArtifact().getFile())) {
204+
Manifest manifest = jar.getManifest();
205+
if (manifest == null) {
206+
throw new MojoFailureException("Project artifact does not contain a manifest!");
207+
}
208+
String bsn = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
209+
if (bsn == null) {
210+
throw new MojoFailureException("Bundle-SymbolicName is missing in the manifest!");
211+
}
212+
ResourceBuilder builder = new ResourceBuilder();
213+
String version = manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
214+
getLog().info("The Bundle-SymbolicName is: " + bsn);
215+
checkBSN(bsn);
216+
getLog().info("The Bundle-Version is: " + version == null ? "0" : version);
217+
checkVersion(version);
218+
builder.addManifest(manifest);
219+
Resource resource = builder.build();
220+
return resource;
221+
}
222+
}
223+
224+
private void checkVersion(String version) throws MojoFailureException {
225+
if (version == null) {
226+
getLog().warn(
227+
"The 'Bundle-Version' header is missing, consider adding a version to your bundle ro prevent it getting the default version."
228+
+ System.lineSeparator()
229+
+ "See https://docs.osgi.org/specification/osgi.core/8.0.0/framework.module.html#d0e2103 for details.");
230+
return;
231+
}
232+
try {
233+
Version.parseVersion(version);
234+
} catch (IllegalArgumentException e) {
235+
throw new MojoFailureException("The 'Bundle-Version' value '" + version + "' is not valid!"
236+
+ System.lineSeparator()
237+
+ "See https://docs.osgi.org/specification/osgi.core/8.0.0/framework.module.html#d0e2103 for details!",
238+
e);
239+
}
240+
}
241+
242+
private void checkBSN(String bsn) {
243+
if (!bsn.contains(".")) {
244+
getLog().warn(
245+
"The OSGi specification recommends to use a reverse domain name for the 'Bundle-SymbolicName' but the current value do not seem to match!"
246+
+ System.lineSeparator()
247+
+ "See https://docs.osgi.org/specification/osgi.core/8.0.0/framework.module.html#d0e2086 for details.");
248+
}
249+
}
250+
251+
private Map<Artifact, Resource> analyzeArtifacts() {
252+
Map<Artifact, Resource> map = new LinkedHashMap<>();
253+
Set<Artifact> artifacts = project.getArtifacts();
254+
for (Artifact artifact : artifacts) {
255+
File file = artifact.getFile();
256+
if (file != null && artifact.getArtifactHandler().isAddedToClasspath()) {
257+
ResourceBuilder builder = new ResourceBuilder();
258+
try {
259+
if (builder.addFile(file)) {
260+
map.put(artifact, builder.build());
261+
} else {
262+
try (Analyzer analyzer = new Analyzer(new Jar(file))) {
263+
analyzer.setExportPackage("*");
264+
ResourceBuilder rb = new ResourceBuilder();
265+
rb.addManifest(analyzer.calcManifest());
266+
map.put(artifact, new WrappedResource(rb.build(), artifact));
267+
}
268+
}
269+
} catch (Exception e) {
270+
// we can not use that for the verification process
271+
}
272+
}
273+
}
274+
return map;
275+
}
276+
277+
private Resource getJVMResource() {
278+
ResourceBuilder builder = new ResourceBuilder();
279+
builder.addEE(EE.getEEFromReleaseVersion(0));
280+
CapReqBuilder ee = new CapReqBuilder(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE);
281+
ee.addAttribute(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE, "JavaSE");
282+
ee.addAttribute(ExecutionEnvironmentNamespace.CAPABILITY_VERSION_ATTRIBUTE, Runtime.version().feature());
283+
builder.addCapability(ee);
284+
ModuleLayer.boot().modules().stream().map(Module::getDescriptor).flatMap(desc -> desc.isAutomatic()
285+
? desc.packages().stream()
286+
: desc.exports().stream().filter(Predicate.not(java.lang.module.ModuleDescriptor.Exports::isQualified))
287+
.map(java.lang.module.ModuleDescriptor.Exports::source))
288+
.forEach(pkg -> builder.addExportPackage(pkg, null));
289+
return builder.build();
290+
}
291+
292+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.wrap;
14+
15+
import java.util.List;
16+
17+
import org.apache.maven.artifact.Artifact;
18+
import org.osgi.resource.Capability;
19+
import org.osgi.resource.Requirement;
20+
import org.osgi.resource.Resource;
21+
22+
class WrappedResource implements Resource {
23+
24+
private Resource resource;
25+
private Artifact artifact;
26+
27+
public WrappedResource(Resource resource, Artifact artifact) {
28+
this.resource = resource;
29+
this.artifact = artifact;
30+
}
31+
32+
@Override
33+
public List<Capability> getCapabilities(String namespace) {
34+
return resource.getCapabilities(namespace);
35+
}
36+
37+
@Override
38+
public List<Requirement> getRequirements(String namespace) {
39+
return resource.getRequirements(namespace);
40+
}
41+
42+
@Override
43+
public boolean equals(Object obj) {
44+
return resource.equals(obj);
45+
}
46+
47+
@Override
48+
public int hashCode() {
49+
return resource.hashCode();
50+
}
51+
52+
public Artifact getArtifact() {
53+
return artifact;
54+
}
55+
56+
}

0 commit comments

Comments
 (0)