Build-Systeme Teil 4: Boost.Build v2



  • Boost.Build v2

    In diesem Artikel geht es um das C++-Build-System von Boost

    1 Einleitung
    2 Ein guter Grund
    3 Architektur
    4 Grundlagen
    4.1 Installation
    4.2 Unser erstes Script
    4.3 Die bjam-Optionen
    5 Bibliotheken bauen
    6 Targets
    7 Komplexe Projekte
    8 Schlusswort
    A Links

    1 Einleitung

    Eine typische Situation als C++-Programmierer: Ich finde im Web eine interessante Bibliothek, sie kann mir weiterhelfen, mich unterstützen, mein aktuelles Projekt könnte damit in der Entwicklung um einiges schneller voranschreiten. Ich lade mir diese also herunter, entpacke sie und lese erstmal die Dokumentation der Bibliothek. Wie baue ich sie für meine Plattform? Wie baue ich sie für meinen Compiler? Wie baue ich Debug-Varianten? Welche Include-Pfade muss ich in meiner IDE setzen? Welche Lib-Dateien muss ich verlinken? Wenn ich diese Infos in der Dokumentation finde, haben die Bibliotheksentwickler gute Arbeit geleistet. Noch besser ist es, wenn die Entwickler ein Build-Script oder Projektdatei bereitstellen, das ich mit einer Kommandozeile oder einem Mausklick in meiner IDE ausführen kann. Nach ein paar Minuten kann ich die Bibliothek für mein Projekt nutzen.

    Diese Szenarios sind leider meistens Utopie, selbst bei renommierten Open-Source-Bibliotheken. Ja, einige sind sogar auf bestimmte Compiler beschränkt, was die Dokumentation betrifft. Ob es ideologische Gründe sind? Oder fehlende Resourcen in Form von Manpower? Als interessierter Anwender eines nicht unterstützten Compilers ist man bei komplexen Bibliotheken jedenfalls verloren.

    2 Ein guter Grund

    Diese Tatsache sollte einem doch zu denken geben! Genau das hat sich wohl auch die Mannschaft hinter den Boost-C++-Librarys gedacht. Denn Boost ist eine Sammlung von Bibliotheken für verschiedene Plattformen (Windows, Linux, BSD, Solaris u.a.) und wiederum verschiedenen Compilern (MS VC, GCC, Intel Compiler u.a.). Jede Boost-Bibliothek muss die Anforderung erfüllen, auf mind. zwei verschiedenen Compilern zu arbeiten, um in Boost aufgenommen zu werden. Den Aufwand für Dokumentation und Scriptpflege für so viele Bibliotheken kann man sich vorstellen...

    Hier greift das vom Boost-Team entwickelte Boost.Build ein. Jeder der schon mal Boost benutzt hat (was ich nur empfehlen kann) kennt den Build-Vorgang: Boost-Archiv herunterladen, entpacken, Kommandofenster öffnen, in das Boost-Verzeichnis wechseln, eine bestimmte, aber einfache Kommandozeile eingeben... fertig! Ja, und das funktioniert mit allen Boost-Bibliotheken, auf allen unterstützten Compilern, unter allen unterstützten Plattformen. Ohne dass der Anwender für jede Compiler- und Plattform-Kombination in eine Dokumentation schauen muss. Sollte das nicht mit allen C++-Bibliotheken und allen C++-Projekten der Fall sein?

    Lange Rede, kurzer Sinn: Boost.Build ist ein Tool für C und C++, um endlich die Komplexität von Build-Vorgängen zu erleichtern.

    3 Architektur

    Zurzeit (März 2006) wird Boost.Build Version 2 (kurz BBv2) entwickelt, und diese Version werde ich hier behandeln. Das hat den Grund, dass es ggü. Version 1 signifikante Änderungen gibt, die auch den Anwender betreffen. So müssen Leser später nicht umlernen. Weiterhin befindet sich BBv2 in einem sehr weiten Stadium und das Release rückt immer näher.

    BBv2 ist ein Frontend und baut auf das Backend bjam (Boost Jam) auf. bjam wiederum ist aus dem Build-Tool jam von der Firma Perforce entstanden. Für uns ist nur wichtig zu wissen, das bjam und Boost.Build zusammengehören. Wir selbst werden aber nur mit BBv2 direkt in Kontakt kommen, da bjam zur Laufzeit auf BBv2 zugreift.

    Folgende Compiler werden zurzeit unterstützt:

    • Comeau C/C++.
    • Borland's Compiler, auf NT und Cygwin.
    • Digital Mars C++.
    • GNU gcc (diverse Versionen), auf Linux, NT, Cygwin und MacOS X.
    • GNU Fortran.
    • HP C++ Version 7.1 für Tru64 UNIX.
    • HP Fortran.
    • IBM VisualAge C++.
    • Intel C++ Compiler, auf Linux und NT.
    • Metrowerks Code Warrior, auf NT.
    • Microsoft VC++ Compiler (diverse Versionen), auf NT und Cygwin.
    • QCC Compiler für QNX (ist in Arbeit).
    • Sun C++ Compiler.

    Da bjam in C implementiert wird und auch als Sourcecode vorhanden ist, können Sie es auch für exotische Plattformen kompilieren und anpassen.

    4 Grundlagen

    4.1 Installation

    Das Nightly build Package boost-build herunterladen:

    http://boost.org/boost-build2/boost-build.zip
    http://boost.org/boost-build2/boost-build.tar.bz2

    Entpacken, nach boost-build/jam_src wechseln und das bjam bauen lassen, indem man build.bat bzw. build.sh aufruft. Windows-Benutzer müssen vorher ihre Umgebungsvariablen des Compilers setzen. Bei VC++ am besten über Start->Programme->MS VC++->VS Tools->VS Command Prompt.

    Je nach Plattform wird man das bjam-Binary in einem eigens neu erstellten Unterverzeichnis finden. Um bjam einfach nutzen zu können, gibt es zwei Möglichkeiten:
    1. Das bjam-Binary sollte man dorthin kopieren, wo es ohne Pfadangabe gefunden wird.
    2. Oder man fügt das bjam-Verzeichnis der PATH-Variable hinzu. Unter Windows wie folgt:

    set PATH=C:\boost-build\jam_src\bin.ntx86\;%PATH%
    

    Auch BBv2 muss gefunden werden; dazu gibt es zwei Möglichkeiten:
    1. Sie richten eine Umgebungsvariable BOOST_BUILD_PATH ein, welche auf boost-build zeigt. Unter Windows wie folgt:

    set BOOST_BUILD_PATH=C:\boost-build\
    

    2. Oder Sie legen in jedes Ihrer Projekte eine Datei namens boost-build.jam an, in der drinsteht, wo sich das Verzeichnis boost-build befindet. Ein Beispiel dieser Datei findet man im selbigen Verzeichnis.

    Das war es auch schon mit der Installation. Damit bjam in Zukunft aber immer bauen kann, müssen immer die Umgebungsvariablen des Compilers bekannt sein. Das setzen der Pfade sollten Sie im eigenen Interesse in eine Batch-Datei schreiben.

    4.2 Unser erstes Script

    Wollen wir nun langsam zur Praxis schreiten. Wir wollen eine eigene Anwendung von BBv2 bauen (kompilieren und linken) lassen.

    Im Gegensatz zu make u.a. Build-Tools muss man als Script-Schreiber die Kommandos von Compilern nicht kennen. Für uns ist die Benutzung völlig transparent und wir stellen uns ganz unwissend. Denn unser Ziel ist es ja, mit einer einzigen Script-Version so viele Compiler und Plattformen wie möglich zu unterstützen. Da wäre es selbstverständlich destruktiv, wenn wir z.B. spezielle GCC-Parameter benutzen.

    Am besten schreiben wir ein "Hello World!"-Projekt. Wir brauchen eine hello.cpp-Datei, die kompiliert werden soll:

    #include <iostream>
    
    int main() {
      std::cout << "Hello bjam!" << std::endl;
    }
    

    Ein dazugehöriges BBv2 Projekt-Script würde wie folgt aussehen:

    exe hello : hello.cpp ;
    

    Dieses Script speichern Sie unter dem Dateinamen Jamroot in Ihr Quellverzeichnis. Die Verzeichnisstruktur kann dabei so aussehen:

    top/
      |
      +- Jamroot
      +- hello.cpp
    

    bjam schaut in die Jamroot-Datei rein und weiß damit, dass eine Executable (exe) mit dem Namen hello erstellt werden soll. Als Sourcedateien muss es dafür hello.cpp kompilieren. Wenn Sie mehrere Sourcedateien in einer Anwendung haben, was ja eher der Fall ist, können Sie natürlich mehrere Dateien angeben:

    exe hello : hello.cpp hello2.cpp hello3.cpp ;
    

    Wollen Sie alle Dateien eines Verzeichnisses angeben, ist dies mit dem Stern-Wildcard möglich:

    exe hello : [ glob *.cpp ] ;
    

    Auch für den Anwender des Scripts ist alles transparent. Der Anwender muss BBv2 lediglich über ein so genanntes Property (Eigenschaft) mitteilen, welchen Compiler er verwendet. Haben Sie einen MS VisualC++ Compiler, starten Sie den Buildprozess wie folgt:

    bjam toolset=msvc
    

    Dieses Kommando müssen Sie in dem Verzeichnis aufrufen, in dem sich das Jamroot befindet, also im Quellverzeichnis.

    Sollten Sie einen GCC-Compiler nutzen, ist dieses Kommando nötig:

    bjam toolset=gcc
    

    Ist Ihnen die Kommandozeile zu lang oder unflexibel, können Sie Ihren gewünschten Compiler auch in der Konfigurationsdatei user-config.jam von BBv2 fest einstellen (was ich sehr empfehlen kann). Dann reicht nur noch der Aufruf von:

    bjam
    

    Der Buildprozess gibt bei erfolgreichem Durchlauf folgende Meldungen aus (beim MSVC):

    ...found 11 targets...
    ...updating 7 targets...
    MkDir1 bin
    MkDir1 bin\msvc
    MkDir1 bin\msvc\debug
    msvc.compile.c++ bin\msvc\debug\hello.obj
    hello.cpp
    msvc.link bin\msvc\debug\hello.exe
    

    Sie sehen, die Anwendung wurde im Verzeichnis bin\msvc\debug erstellt. Sollten Sie z.B. zwei Compiler haben und für beide auch einen Build laufen lassen wollen, würde es keine Konflikte oder Überschreibungen geben. Ohne jegliche Property wurde auch "nur" eine debug-Variante erstellt. Sie können auch explizit eine Release-Variante erstellen:

    bjam variant=release
    

    Aber auch beide Varianten sind in einem Vorgang möglich:

    bjam variant=debug variant=release
    

    Es ist also sehr einfach und intuitiv zu handhaben.

    Übrigens, bjam rufen wir bisher nach folgendem Muster auf:

    bjam [i]property-key=property-value[/i]
    

    Doch bjam kann auch einfach nur mit Property-Values arbeiten, hier ein paar Beispiele:

    bjam release
    bjam msvc release
    bjam release debug
    

    Ich werde im restlichen Artikel auf das komplette Muster verzichten. Die Property-Keys sind in der BBv2-Dokumentation zu finden, sollte man diese benötigen.

    4.3 Die bjam Optionen

    Neben den Propertys kann bjam auch mit Optionen aufgerufen werden, die zur Folge haben, dass diese vor dem Build-Prozess etwas auswirken oder einfach nur Informationen anzeigen.

    Die Optionen haben folgendes Muster:

    bjam [i]--option[/i]
    

    Die wichtigsten Optionen sollten Sie kennen:
    --version zeigt die bjam- und BBv2-Version an
    --clean löscht alle automatisch erzeugten Dateien vom Typ .o, .obj, .lib, .exe usw.
    --help zeigt weitere Optionen an

    Manchmal ist es ratsam, vor einem Build noch mal eine Bereinigung zu starten. Bei größeren Projekten sollte man das natürlich aus Zeitgründen (bzgl. des kompletten Buildvorgangs) mit Bedacht wählen. Aber es ist manchmal leider nötig. Auch in diesem Tutorial, wo wir immer wieder die gleichen Scripts aufrufen, sind Bereinigungen empfehlenswert. Schließlich wollen Sie ja die Auswirkungen sehen! Die Clean-Option lässt sich selbstverständlich auch mit Propertys kombinieren. Hier ein paar Beispiele:

    bjam --clean
    bjam --clean release
    

    Letztere Variante bereinigt explizit die Release-Ordner, da auch bei clean von Haus aus nur Debug beachtet wird.

    5 Bibliotheken bauen

    Neben den Anwendungsprojekten erstellen Sie sicherlich auch Bibliotheken, um Ihre Projekte modularer und somit flexibler zu halten. Mit dem bisherigen Wissen ist dies jedoch nicht zu realisieren. Natürlich hat auch BBv2 hier eine Lösung im Angebot. Erstellen wir uns also eine Minibibliothek in einem neuen Quellverzeichnis:

    // myLib.hpp
    void foo();
    
    // myLib.cpp
    
    #ifdef _WIN32
    __declspec(dllexport)
    #endif
    void foo() {
    };
    

    Unsere Bibliothek hat somit eine Funktion foo(), die wir später auch von unserem HelloWorld aufrufen und verlinken lassen wollen. Aber zuerst unser Jamroot-\1:

    lib myLib : myLib.cpp ;
    

    Sie sehen es sicherlich selbst, es hat sich ggü. einer Anwendung nicht viel geändert. Anstatt EXE steht dort jedoch LIB für Library. Gebaut wird die Bibliothek wieder durch Aufruf von bjam in der Konsole, und wir sehen, wie unsere Bibliothek gebaut wird:

    ...found 11 targets...
    ...updating 8 targets...
    MkDir1 bin
    MkDir1 bin\msvc
    MkDir1 bin\msvc\debug
    msvc.compile.c++ bin\msvc\debug\lib1.obj
    lib1.cpp
    msvc.link.dll bin\msvc\debug\myLib.dll bin\msvc\debug\myLib.lib
       Bibliothek 'bin\msvc\debug\myLib.lib' und Objekt 'bin\msvc\debug\myLib.exp' wird erstellt
    ...updated 8 targets...
    

    BBv2 nimmt einem hier sehr viel Arbeit ab und das Wissen über ein Compiler-System ist unnötig. Besonders Einsteigern ohne IDE kann hier der C++-Einstieg um einiges erleichtert werden.

    6 Targets

    Sie werden es gemerkt haben, wir haben für unsere Anwendung und Bibliothek unterschiedliche Quellen - im Build-Fachjargon Targets (Ziele) genannt. Das gute ist, wir haben alles getrennt und somit modular. Doch ist es lästig, immer zwei Scripts separat anzusteuern, denn oft wird an mehreren Fronten gleichzeitig entwickelt. BBv2 hat deshalb eine Projektverwaltung, die mit Targets arbeitet, die für komplexere Projekte unerlässlich sind. Ich will den Effekt der Targets erstmal ohne komplexes BBv2-Projekt zeigen, um den Einstieg zu vereinfachen.

    Bisher haben wir zwei unterschiedliche Quellverzeichnisse, wo unser hello-Anwendungsprojekt und myLib-Bibliotheksprojekt liegt. hello und myLib sind dabei jeweils durch einen Target-Namen gekennzeichnet, im Jamroot fett dargestellt:

    exe [b]hello[/b] : hello.cpp ;
    

    Targets sind da, um gezielt angesteuert bzw. ausgewählt werden zu können. Deshalb können wir auch bedenkenlos das hier in einem Jamroot-Script machen:

    exe hello : hello.cpp ;
    lib myLib : myLib.cpp ;
    

    Natürlich müssen Sie auch alle nötigen Sourcedateien in das Quellverzeichnis kopieren, damit diese gebaut werden können.

    top/
     |
     +- Jamroot
     +- hello.cpp
     +- myLib.cpp
     +- myLib.hpp
    

    Wenn Sie jetzt bjam in der Konsole aufrufen, werden implizit alle Targets gebaut. Die Reihenfolge ist dabei nicht festgelegt, das entscheidet bjam. Wollen Sie jedoch z.B. nur hello bauen lassen, ist dies möglich:

    bjam hello
    

    Wollen Sie hello als release bauen, ist wie immer eine Kombination von Propertys möglich:

    bjam hello release
    

    Mit dem Wissen über Targets können wir nun zu den Projekten übergehen.

    7 Komplexe Projekte

    Bisher hatten wir ein Verzeichnis pro Projekt, in dem ein oder mehrere Targets waren. Das ist leider sehr unübersichtlich oder auch unflexibel. Entweder müssen wir jedes Projekt separat behandeln, was in vielen Arbeitsschritten ausartet. Oder wir haben alle in einem Verzeichnis und Script, was unübersichtlich und überladen ist. Optimal ist es, wenn man eine Projektmappe hat. Jedes Projekt in einem eigenen Unterverzeichnis, durch eigene Targets identifizierbar, und doch kann alles mit einem Aufruf gebaut werden. Deshalb hier ein Beispiel, wie die Verzeichnisstruktur aussehen kann:

    top/
     |
     +- Jamroot
     |
     +- hello/
     |	|
     |	+- Jamfile
     |	+- hello.cpp
     |
     +- libs/
    	|
    	+- lib1/
    	.	|
    	.	+- Jamfile
    	.	+- myLib.cpp
    	.	+- myLib.hpp
    

    Ein Jamroot haben wir immer noch, da es die Wurzel für alle Projekte ist. Wir haben jedoch die Targets hello und myLib unterhalb von Jamroot in eigene Unterverzeichnisse (Unterprojekte) verlegt. In diesen ist kein Jamroot-Script, sondern ein Jamfile genanntes Script. Da Jamroot die Wurzel ist, erben alle Unterprojekte (also Jamfiles) automatisch die Anforderungen und Eigenschaften von Jamroot.

    Soll Ihre Projektmappe Abhängigkeiten (dependencies) zu externen Bibliotheken haben, können Sie diese in Jamroot definieren. Automatisch haben hello und myLib auch diese Abhängigkeit. So erspart man sich viel Arbeit, diese in jedem Jamfile zu definieren. Hat z.B. nur myLib eine Abhängigkeit, kann man diese natürlich in dem einen speziellen Jamfile definieren. Alle anderen Projekte wären dann davon unbetroffen.

    In unserer gesamten Projektmappe ist unsere Anwendung "hello" das Kernprojekt. Wenn wir also bjam aufrufen, soll am Ende unser hello.exe zusammengebaut werden. Dafür schreiben Sie in Jamroot:

    build-project hello ;
    

    Durch einen bjam Aufruf wird hello gebaut. Sollte aber hello.cpp einen Funktionsaufruf foo() aus myLib haben, wie hier:

    #include "myLib.hpp"
    
    int main()
    {
    	foo();
    }
    

    stehen wir vor einem Compile- und Linker-Problem.

    Wir wollen also, dass hello eine Abhängigkeit zu myLib hat. Wie definieren wir aber eine Abhängigkeit? In Ihrer top/hello/Jamfile schreiben Sie Folgendes:

    exe hello : hello.cpp ../libs/lib1//myLib ;
    

    Das ../libs/lib1 ist eine ganz normale relative Pfadangabe, die auf das lib1-Projekt zeigt. Danach sind die doppelten Schrägstriche zu beachten! Diese leiten den Namen des Targets ein, der in dem Projekt definiert ist. Denn es könnten ja auch mehrere Targets drinstehen, deshalb muss das gewünschte genannt werden. In unserem Fall ist das Dependency-Target "myLib".

    Eine Sache fehlt noch. Die Header-Datei unserer myLib wird so nicht ohne weiteres gefunden werden. Sie müssen dem Projekt eine Includes-Anforderung mitgeben:

    [b]project : usage-requirements <include>. ;[/b]
    lib myLib : myLib.cpp ;
    

    Die Nutzungsanforderung für includes gibt an, wo die Header-Dateien zu diesem Projekt zu finden sind. Durch den Punkt somit im aktuellen Verzeichnis. Sie könnten auch die Header-Dateien in ein Unterverzeichnis ablegen und den relativen Pfad angeben. Die Projekt-Anforderungen sind übrigens für alle Targets in diesem Projekt gültig.

    Wenn Sie jetzt bjam im Jamroot-Verzeichnis ausführen, werden Sie feststellen, dass nicht nur hello gebaut wird, sondern auch myLib.

    Die Sache funktioniert, hat aber einen Nachteil: Wenn irgendwann lib1 in ein anderes Verzeichnis verlegt wird, müssen alle Jamfiles angepasst werden, die vom Target myLib abhängig sind. Hier kommt Jamroot ins Spiel, in dem wir Projekt-IDs definieren. Da alle Projekte bzw. Jamfiles die Eigenschaften von Jamroot erben, können diese die Projekt-IDs anstatt Pfadangaben benutzen. Als Beispiel schreiben wir in Jamroot:

    build-project hello ;
    [b]use-project /my-lib-id/lib1 : libs/lib1 ;[/b]
    

    Hinter use-project steht die Projekt-ID und nach dem Doppelpunkt der Pfad, in dem sich das Projekt mit den Targets befindet. Nun können Sie in hellos Jamfile schreiben:

    exe hello : hello.cpp /my-lib-id/lib1//myLib ;
    

    /my-lib-id/lib1 ist die ID des Projektes, nach den doppelten Schrägstrichen folgt wie gewohnt das gewünschte Target. In Zukunft muss nur noch Jamroot angepasst werden, sollte sich an der Verzeichnisstruktur etwas ändern.

    8 Schlusswort

    Mit diesem bisherigen Wissen können Sie schon mal Ihre eigenen Projekte sehr einfach und komfortabel bauen. Meiner Meinung nach ist BBv2 eines der einfachsten und doch leistungsfähigsten Buildtools. Natürlich konnte dieser Artikel nur die Grundlagen vermitteln. BBv2 bietet noch viel mehr Möglichkeiten, z.B. externe Bibliotheken oder fertige Bibliotheken, ohne vorhandenen Sourcecode mit einzubeziehen. Auch können in den Jamfiles Ausnahmen für spezielle Compiliervarianten gebildet werden, z.B. können für Debug-Builds ganz andere Bibliotheken verlinkt werden als in einem Release-Build. Auch können Optimierungsoptionen für bestimmte Targets ein- und ausgeschaltet werden. Die Möglichkeiten sind schlicht in diesem Artikel nicht aufzählbar, ich will Sie auf die bereits sehr gute BBv2-Dokumentation hinweisen.

    Noch ein paar Punkte, die Aufgaben neben dem reinen Build sind und trotzdem von BBv2 unterstützt werden:

    • BBv2 unterstützt Unit-Tests
    • BBv2 kann Dokumentation mittels Boost.Book oder Doxygen aus Ihren Sourcen erzeugen
    • Sie können BBv2 um eigene Funktionalitäten mittels bjam-Scripts erweitern
    • aus einem CVS-Repository den aktuellen Entwicklungsstand auschecken und diesen für den Build benutzen
    • und einige andere Dinge mehr

    BBv2 ist ein Build-System, das sich intuitiv anwenden lässt und sich hoffentlich in der C++-Community verbreiten wird. Viel Spaß mit BBv2.

    A Links

    Boost.Build v2 Homepage
    Boost.Build v2 Wiki
    Boost C++ Libraries Homepage



  • Scheint recht interessant zu sein. Wenn ich mal wieder etwas mehr Zeit schaue ich es mir auf jeden Fall mal an.

    Danke für den Artikel. 🙂



  • Toller Artikel. Wenn ich mal mit automatischen Builds anfangen werde, wird Boost.Build V2 meine erste Anlaufstelle sein.

    Ich bin übrigens durch dich auf Boost gestossen. Das war das beste was mir passieren konnte. Danke.

    Thomas



  • Falls es jemanden interessiert, wie man BBv2 auch als Build Tool in der MSVC2005 IDE benutzen kann, sollte sich das hier mal anschauen.



  • Als ergänzende Dokumentation (englisch): Boost.Build: Building C++ projects, Boris Schäling, 20 July 2009.


Log in to reply