Montag, 13. Juli 2009

OSGi-Bundles bauen mit Eclipse und Maven


Neu: Ich habe jetzt mal Tycho ausprobiert, damit ist alles besser :)

Allerdings mache ich dafür kein neues Tutorial, das von Mattias Holmqvist (Teil 1, Teil 2 und Teil 3) ist schon ziemlich perfekt. Meine Erfahrungen damit habe ich unter OSGi-Bundles bauen mit Tycho beschrieben.


---
Alter Post (Vorsicht, mir hat's Groß- und Kleinschreibung zerlegt und das Aufräumen lohnt aus oben beschreibenem Grund nicht mehr) :


OSGi-Bundles und Eclipse Plugins zu bauen war bisher immer sehr umständlich und fehleranfällig. Das bringt mich ja zu der Haltung, dass ich OSGi eher vermeiden würde. Insbesondere bringt es ja in der Regel auch keinen Business-Value. Anderes Thema, ich bin nun mal durch das Umfeld gezwungen, Plugins für die Eclipse Rich Client Platform zu bauen.

Meine Randbedingungen sind Folgende: Ich möchte in Eclipse entwickeln und dort auch alle Möglichkeiten der PDE nutzen. Andererseits ist es für mich keine Option, fertige Bundles durch einen Entwickler aus seiner IDE exportieren zu lassen. Ich nutze nämlich sowieso ein automatisches Build-System (schließlich bin ich nicht alleine auf der Welt, sondern kooperiere mit anderen, zum Teil verteilt sitzenden Kollegen), um Continuous Builds und Nightly Builds zu machen (siehe mein letzter Post). Dort möchte ich die Bundles über Maven bauen. Bisher haben wir das hier in der Firma über den automatisierten PDE-Build gemacht, der Prozess ist aber höchst umständlich und fehleranfällig.

Mit dem bnd-Tool und dem maven-bundle-plugin aus dem Apache Felix-Projekt gibt es jetzt aber eine spannende und benutzbare Option. Spannend deswegen, weil bnd einen völlig neuen Weg geht - es basiert nämlich nicht (nur) auf Deklarationen, sondern analysiert den compilierten Code, um Abhängigkeiten zu entdecken und in die MANIFEST.MF einzutragen.

Jetzt zur Lösung: Da ich nicht nur ein Bundle verwende, habe ich eine "common-pom.xml" als gemeinsame Parent-POM meiner Bundle-Projekte.

Dort verwende ich folgenden Eintrag unter Plugins:



<plugin>
  <groupid>org.apache.felix</groupid>
  <artifactid>maven-bundle-plugin</artifactid>
  <extensions>true</extensions>
  <configuration>
    <manifestlocation>META-INF</manifestlocation>
    <instructions>
      <bundle-symbolicname>${pom.groupId}.${pom.artifactId};singleton:=true</bundle-symbolicname>
      <bundle-activator>${bundle.activator}</bundle-activator>
      <embed-dependency>*;scope=compile|runtime;type=!pom,inline=false</embed-dependency>
      <embed-transitive>true</embed-transitive>
      <embed-directory>lib</embed-directory>
      <import-package>${bundle.import-packages}*;resolution:=optional</import-package>
    </instructions>
  </configuration>
</plugin>

Die notwendigen Properties habe ich in der Parent-POM wie folgt definiert:


<properties>
  <bundle.activator>${pom.groupId}.${pom.artifactId}.internal.Activator</bundle.activator>
  <bundle.import-packages></bundle.import-packages>
</properties>


Für das Zusammenspiel mit Eclipse als IDE sorgen dabei einige Einträge: Zunächst verwende ich als manifestLocation natürlich den "default"-Pfad von Eclipse (META-INF) und nicht den maven-default unter target.

Zum anderen verwende ich ein "Embed-Directory" (lib), in das ich per maven-dependency-plugin alle notwendigen Abhängigkeiten (d.h. 3rd-Party-Libraries, die ich nicht zu OSGi-Bundles konvertiere) kopiere. Der entsprechende Abschnitt der Parent-POM sieht wie folgt aus:

<profiles>
  <profile>
    <id>eclipse</id>
    <build>
      <plugins>
        <plugin>
          <artifactid>maven-clean-plugin</artifactid>
          <executions>
            <execution>
              <id>clean-dependencies-eclipse</id>
              <phase>process-resources</phase>
              <goals>
                <goal>clean</goal>
              </goals>
              <configuration>
                <filesets>
                  <fileset>
                    <directory>${basedir}/lib</directory>
                    <includes>
                      <include>**/*</include>
                    </includes>
                  </fileset>
                </filesets>
              </configuration>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <artifactid>maven-dependency-plugin</artifactid>
            <executions>
              <execution>
                <id>copy-dependencies-eclipse</id>
                <phase>process-resources</phase>
                <goals>
                  <goal>copy-dependencies</goal>
                </goals>
                <configuration>
                <outputdirectory>${basedir}/lib</outputdirectory>
                <includescope>compile</includescope>
                <includescope>runtime</includescope>
                <excludetypes>pom</excludetypes>
              </configuration>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
  </profile>
  ...
</profiles>


Für ein konkretes Bundle sind jetzt "nur noch" relativ wenige Schritte notwendig:

Natürlich muss das Projekt die Parent-POM entsprechend referenzieren (ich mache das bei mir per relative-path wie folgt):



<parent>
  <groupid>de.vizmind</groupid>
  <artifactid>common</artifactid>
  <version>0.0.1-SNAPSHOT</version>
  <relativepath>../de.vizmind.common/pom.xml
</parent>


Als Packaging muss man bundle verwenden:

<packaging>bundle</packaging>


Jetzt kann man ganz wie gewohnt seine Dependencies eintragen, wobei die Compile und Runtime-Dependencies wie oben gesagt in den lib-Ordner kopiert werden und so für den PDE-Mechanismus von Eclipse verwendbar sind - dieser hat nämlich so seine Probleme mit den "Maven Dependencies" des m2eclipse-Plugins.

Abhängigkeiten zu anderen Bundles markiert man einfach mit dem Scope "provided", diese werden dann herangezogen, um die "import-packages" zu bestimmen, aber nicht mit in das Bundle kopiert.

Jetzt muss man gegebenenfalls noch die Properties aus der Parent-POM "überdefinieren", z.B. wenn man keinen Activator definiert hat (der default wird unter ${pom.groupId}.${pom.artifactId}.internal.Activator erwartet).

Wenn man Abhängigkeiten definieren muss, die sich nicht aus den class-Dateien ergeben, so ist das manuell über das Property "bundle.import-packages" zu tun. Da dieses Property einfach vor die automatisch bestimmten gehängt wird, muss man es (noch) mit einem Komma beenden, sofern es nicht leer ist. (Ein weiterer Punkt für Verbesserungen). In unserem Beispiel hier verwenden wir in einem Bundle spring und referenzieren in der context.xml einen PropertyPlaceholderConfigurer. Der Import für diese Klasse kann natürlich nicht automatisch aus .class-Dateien bestimmt werden, also muss er manuell hinzugefügt werden. (Ein weiterer Punkt für Verbesserungen - Automatische Analyse von Spring-Dateien).

Zusammen sieht das dann wie folgt aus:


<properties>
  <bundle.activator></bundle.activator>
  <bundle.import-packages>org.springframework.beans.factory.config,</bundle.import-packages>
  <!-- Close with "," if you have a non-empty list -->
</properties>


Jetzt ist mein Projekt fertig. Die Verwendung eines Profils (in der Parent-POM, siehe oben) sorgt dafür, dass ich mit folgendem Maven-Kommando ein Eclipse-Projekt anlegen kann, ohne dem eigentlichen Build später in die Quere zu kommen.

mvn eclipse:clean eclipse:m2eclipse process-resources compiler:compile bundle:manifest -Declipse.pde=true -Peclipse

Das Kommando ist noch etwas umständlich, evtl. mache ich später hier noch etwas "Feinarbeit". Für jetzt funktioniert es erst mal.

Das Projekt kann nun in Eclipse importiert werden. Einziger Wermutstropfen: Aktuell wird die .classpath-Datei noch nicht so aufgebaut, dass die embedded 3rd-Party-Libraries auch referenziert werden. Die muss man noch einmal "von Hand" in den Build-Path aufnehmen.

Damit ist der Weg frei, das Bundle in der Eclipse IDE zu verwenden und zu entwickeln.

Mit

mvn package

kann man nun das Bundle auch bauen, so dass auch dem automatisierten Build nichts mehr im Wege steht.


Ciao

Chris