There is a new maven plugin, maven-bundle-plugin, which we can use in place of custom assembly steps and ant scripts, to build eclipse plugins. There is a way to do the uimaj-ep-runtime plugin so that it works correctly when it is open in the workspace, too. This tip describes the approach and design rationale.
Note: We presume Eclipse 3.2 level as the base. This implies OSGi R4 conventions as the base. But note that Eclipse 3.2 doesn't properly support the "uses" directive in the Export-Package (see following for workaround).
The basic goal is to do things the maven way and reduce custom configuration code in our build, making our build more reliable, understandable, and maintainable over the long run. A second goal is to solve a long-standing problem where the uimaj-ep-runtime plugin, if open in the workspace, made compiling and launching of other plugins that depended on it, not work.
The basic information for how to do this comes from the maven-bundle-plugin documentation: http://felix.apache.org/site/maven-bundle-plugin-bnd.html
This plugin supports "library project" plugins which are just collections of other jars, put into a plugin - exactly what our uimaj-ep-runtime plugin is.
A basic principle is to put all the bundle info that normally would go into the MANIFEST.MF part of the Eclpse plugin into the configuration in the POM for the maven-bundle-plugin, and use that to generate the manifest.mf. This gives just one place to maintain this information. The plugin.xml (used to describe extension points) is still needed if the plugin defines extension points. Some plugins have "blank" plugin.xml - these can just be deleted. The build.properties file is not used, either. (it is used only for Eclipse building the plugins, and we instead have Maven do that).
- A downside to this is that the process for checking out plugin-projects and getting them to work in Eclipse is now: 1) check out, 2) mvn install (needed to create the MANIFEST.MF), mvn eclipse:eclipse, (and then, if needed, "import" into Eclipse workspace). Steps 2 and 3 can be done in one command: mvn install eclipse:eclipse.
What does the maven-bundle-plugin do?
It builds the actual plugin jar file. Along the way, takes the information in the POM and uses it to construct the MANIFEST.MF file; we arrange to have this file in the spot where Eclipse PDE expects it, so the plugin can be developed in Eclipse.
Some details:
- The POM dependencies are used when maven compiles the code for the plugin, and are used by the bundling to find dependencies and do the right "imports" for them in the generated manifest.
- The configuration for the bundle plugin is where the specifics of what goes into the plugin, what gets exported, what needs to be imported, are specified.
- Any additional things that need to go into the resulting manifest.mf to support the Eclipse plugin can be put in the POM as configuration specs for the bundle plugin; it will pass these along.
The uimaj-ep-runtime plugin - a special case
This is a library plugin, consisting of jars from other projects. It is built using the maven-bundle-plugin capabilities to specify what packages to export, using the <_exportcontents> directive. This directive avoids actually including those packages in the bundle as direct classes, (versus using the normal <Export-Package> directive), because a later instruction, <Embed-Dependency>, specifies using the dependency information to embed the Jar files maven builds for those, in the bundle being built. Since the jars already have the packages, we use <_exportcontents> to avoid duplicating these contents.
The inline=true flag is set when embedding dependencies, to avoid having the bundle jar contain within itself, other jars. This flag puts the contents of the inner jars into the containing jars. This is needed because the OSGi or Eclipse mechanisms doesn't see exports coming from these jars within jars. See the statements about "Unzipping JARs" in the Eclipse Help.
One thing the bundle plugin does is to follow dependency chains in the things it's including. When the uimaj-adapter-soap was included, the bundle plugin determined this needed the axis jars; these are not however part of our distribution. Because of this, the uimaj-adapter-soap dependency is commented out at this point. It's left in as a comment, in case a user wants to build a version of this with the axis files included.
Making the uimaj-ep-runtime plugin work in Eclipse when open
This plugin has an empty src and empty target/classes because all the source/class files were in other projects, it just was a collector of their Jars. This problem is avoided now by changing other projects that previously <depended> on the runtime plugin to depend instead on the individual components that are in the runtime plugin.
These dependencies allow both maven and Eclipse to compile the source of the plugins.
The mechanism in the maven-bundle-plugin that creates "uses" clauses for packages from the dependencies is turned off, because that feature isn't properly supported in Eclipse 3.2. Therefore, if there are multiple versions of these packages that are present, be aware that mis-wiring may occur. For more on this, see http://underlap.blogspot.com/2007/10/osgi-type-safety-and-uses-directive.html
Sometimes additions are needed for <Import-Package>
The bundle plugin uses <Import-Package> in preference to <Require-Bundle>. This approach has a finer granularity (often there are many "packages" within one bundle), and seems to be the preferred approach for managing OSGi component integration.
To compute the needed imports, the bundle plugin scans the byte-code of the classes it put into the plugin. Sometimes, this can miss needed things. An example is when your source uses org.eclipse.swt.SWT class, which has mainly "constants" declared as final int - these are compiled-away and only the resulting int appears in the byte code.
To handle these cases, those plugins which don't compile after the bundle builds the manifest.mf, because they are missing includes, have the required includes added to the POM.
Some issues solved
The first issue we hit concerned "wrong wiring". What would happen would be that several testers would try out the plugins, on various platforms (Windows, Linux, etc), and things would work. An then one would report a failure. We traced this to Eclipse's use of split packages. (Background - You can see the list of Eclipse's split packages by going to the Eclipse "Help" and then searching for the terms: split packages map)
Our first attempt was to wire a package to a particular bundle. This worked, but "incrementally" - it would solve one problem, only to have some testing done later reveal yet another (different) instance of the split package problem.
Since, in general, it could be the case that "both" sources of a package would need to be included, we changed the approach to use Require-Bundle for all the bundles that could supply classes for the split-package. This, by itself, didn't work - we think it was because the OSGi spec says that if you have both an "import-package" and a "Require-Bundle", the Import-Package takes precedence. Here's the quote from the OSGi spec on this:
A bundle may both import packages (via Import-Package) and require one
or more bundles (via Require-Bundle), but if a package is imported via
Import-Package, it is not also visible via Require-Bundle: Import-Package
takes priority over Require-Bundle, and packages which are exported by a
required bundle and imported via Import-Package must not be treated as
split packages.
So - the next work-around was to add a negative pattern to the Import-Package for just those packages which were split - to avoid having them imported. This seemed to work.
The next issue we ran into happened when we tried to use our plugins in Eclipse 3.2.x, which doesn't support the "uses" clause. The fix here was to eliminate the "uses" clauses, by (a) using version 1.4.0 of the maven-bundle-plugin, and (b) adding a <_nouses>true</_nouses> instruction. We also found a further motivation found for not generating the "uses" clause: this note from the Eclipse project developers mailing list: http://dev.eclipse.org/mhonarc/lists/stp-dev/msg01624.html (concerning performance issues with "uses" clauses).
Other issues we tackled: the maven build of these plugins would work, but the Eclipse build would often give the following kinds of errors:
Access restriction: The method <some-method-name>() from the type <some-class-name> is not accessible due to restriction on required project <some-project-name>
If you opened the project properties and looked at the Java Build Path, and expanded the plugin-dependencies, you could, indeed, see there were "Access rules" for the <some-project-name>, but these rules would be missing the pattern which covered the case for the package where <some-class-name> lived.
The fix for this (probably really a workaround?) was to add an explicit "import" for these packages to the POM's maven-bundle-plugin instruction: <Import-Package> to import these packages. I don't really understand why this worked, or why the maven-bundle-plugin didn't import these automagically (the Import-Package list of packages included "*" as one of the items, which I think means to import all the packages that were referenced, but not locally available).
Scopes and <optional> in dependencies
Scopes are used to control dependency transitivity and to set up classpaths, and for other things (other plugins can behave differently depending on scope).
We make our Eclipse dependencies "provided", and our uima dependencies "compile". This makes the eclipse:eclipse plugin create links for the uima dependencies to the local maven repo for the jars, and puts these on the classpath. The Eclipse dependencies are "provided" - meaning they come from the OSGi container (the current Eclipse runtime environment).
When building bundles, typically scopes used are:
scope |
meaning |
---|---|
"compile" |
contains classes/resources being embedded because my code needs them to load and I don't expect them to be provided by another bundle. |
"runtime" |
contains classes/resources that to embed because my code might need them during runtime (via reflection, etc.) and I don't expect them to be provided by another bundle |
"provided" |
contains classes/resources that I expect to be provided by another bundle or the framework |
"test" |
contains classes/resources used in any unit or integration tests (ie. not needed to load/run the bundle - just needed to test with) |
Scopes and transitivity
Here's how they work in practice. Consider 4 components, A, B, C, & D, where A is stand-alone, B depends on A, C depends on B and D depends on C.
Dependency |
Scope |
resulting classpath |
|
---|---|---|---|
B -> A |
compile |
B, A |
|
C -> B |
provided |
C, B, A |
because B is compile scoped to A |
D -> C |
provided |
D, C |
because C is provided scoped to B |
Any dependencies of a "provided" dependency are treated as
if they were also "provided" in later projects - so A is treated
as a "provided" dependency in C, which then means it doesn't
appear in D because "provided" dependencies aren't transitive.
<optional>
If you mark a dependency <optional>, this breaks the transitivity for other components that depend on the POM containing the <optional>. If you have a compile|runtime scope dependency, but you don't need components which depend on you to depend also on what you have as a compile|runtime scope dependency on, use the <optional> to break the dependency.
For plugins, if you include the code of a dependency directly into the plugin, then you mark that dependency <optional>. This saves builds that use your component from downloading dependencies they won't use.