Bakefiles

Eine kleine Einführung in Bakefile

Makefile

Ich denke, dass jeder von euch schon mal mit Makefiles in Kontakt gekommen sind. Gerade die Linux-User dürften davon ein Lied singen.
Da ich davon ausgehe, dass ein kleiner Anteil keine Ahnung davon hat, oder sie bisher nur benutzt hat, ohne zu wissen was dahinter steckt,
folgt hier eine kleine Erklärung.

Was sind Makefiles?

Makefiles werden von dem Programm make eingelesen. In diesen Makefiles stehen einige Regeln, um Quelldateien mithilfe eines bestimmten Programms (meistens Compiler!) zu übersetzen/verarbeiten. Generell wird es bei OpenSource-Projekten benutzt, damit man das ganze Projekt nicht erst in eine IDE einbinden, oder man den kompletten Code von Hand verarbeiten muss.
Makefiles können relativ komplex werden. Angenommen wir haben eine ausführbare Datei und mehrere dynamische Bibliotheken. Dann müsste man alle Abhängigkeiten für die Datei und für die Bibliothek auflisten und die jeweiligen Compiler-Flags anpassen.
Da man makefiles meist nicht nur für einen Compiler macht, sondern bei einem Projekt gleich mehrere Compiler unterstützen möchte, liegen meistens mehrere Makefiles vor (für jeden Compiler einen) und hier sollte schon das große Problem offensichtlich sein. Wer Makefiles schreiben möchte, der muss sich mit den Compiler-Flags gut auskennen und sich nicht nur mit einem Compiler beschäftigen.

Bakefile

Anstatt Makefiles manuell zu gestalten, können wir auf „Bakefile“ zurückgreifen. Zwar ist das Prinzip ähnlich, man hat eine Datei in der mehrere Regeln für die Compilierung stehen, allerdings kann man mit Bakefile aus dieser Datei Makefiles für beliebige Compiler erstellen.
So hat man nur eine Datei, bei der, für alle Compiler, die Regeln gleich bleiben (da diese von Bakefile umgewandelt werden).
Außerdem liefert Bakefiles ein paar Templates (für eine ausführbare Datei, für eine Biliothek (die ebenfalls von einer Makefile erstellt wird), für eine Systembibliothek, die fürs Linken benötigt wird.. und und und.
Der Vorteil sollte wohl klar geworden sein
Der größte Nachteil dürfte wohl sein, dass Bakefiles extra für die Programmierung mit C / C++ ausgelegt ist.
Andere Programme (z.b. pdflatex und co.) lassen sich nur schwer einbinden (per Erstellung eines Templates).
Wer mal gerne in C++ entwickelt und den Usern eine portable Möglichkeit geben möchte den Quelltext schnell übersetzen zu lassen, für den ist Bakefiles eine gute Alternative.

Aufbau einer Bakefile

Fangen wir am besten einfach an und gucken uns ein „‚Hello World“‚-Projekt an.


<?xml version="1.0"?>
<makefile>

  <include file="presets/simple.bkl"/>

  <exe id="hello" template="simple">
    <sources>hello.c</sources>
  </exe>
</makefile>

Wie jedem sofort auffallen sollte benutzt Bakefile XML. Das Wurzelelement ist (IMMER!) „makefile“.
Per Include-Direktive werden sogenannte „Presets“ eingebunden. Das sind quasi die Templates die ich meinte, für eine ausführbare Datei usw.
Dabei werden ein paar Optionen automatisch eingebunden. In diesem Beispiel wird das Preset „simple“ geladen, welche für den Endbenutzer eine
Debug/Release-Option hinzufügt. Innerhalb des Wurzelelements können wir nun beliebige Build-Targes angeben
(hier per <exe id="hello" template="simple"> </exe>) Dabei können wir das jeweilige Build-Target innerhalb des Bakefiles mit dessen Id ansprechen (wird benötigt für Abhängigkeiten). Was das Template hier wohl zu suchen hat sollte klar sein.
über „<sources>“ können nun beliebig viele Dateien eingebunden werden, die zum Compilieren des Projekts benötigt werden. Natürlich bietet euch Bakefile noch eine vielzahl weiterer Möglichkeiten.

Targets

Wie schon beim Aufbaue einer Bakefile erwähnt, gibt es sogenannte Targets. Damit wird ein Bakefile eine ausführbare Datei oder eine Bibliothek bezeichnet, welche durch das generierte Makefile erstellt wird. Zuvor habe ich schon das Target „exe“ exemplarisch Vorgestellt. Das Target „exe“ hatte einen sogenannten Tag. Über diesen Tag kann das Target weiter spezifiziert werden (Abhängigkeiten, Compiler-Einstellungen, Suchpfade für Header-,Source-Dateien oder Bibliotheken).
Im obigen Beispiel hatten wir dem Target hello mit dem Tag sources eine Quelltext-Datei hinzugefügt, die beim ausführen der Makefile übersetzt wird.

Alles in allem sind wohl folgende Targets für euch wichtig:

Target Beschreibung
exe Erstellt ein ausführbares Programm
lib Erstellt eine Bibliothek, welche zum statischen Linken benutzt werden kann
dll Erstellt eine dynamische Bibliothek, welche beim Programmstart in den virtuellen Speicher geladen wird
subproject Weist make an, die Makefile vom Unterprojekt ebenfalls zu erstellen

Tags

Bei der Sektion „Targets“ habe ich von Tags gesprochen. Dabei hatten wir den Tag „sources“ (da eine Bakefile im XML-Format vorliegt, wird ein Tag wie ein Element benutzt), mit dessen Hilfe wir dem Projekt eine Quelldatei hinzugefügt haben. Wenn wir nun Abhängigkeiten und ähnliches deklarieren möchten, nutzen wir dafür genau diese Tags. Manche Tags sind sehr spezifisch und sind nur innerhalb eines Targets gültig, andere haben allgemeine Gültigkeit und können mit allen Targets benutzt werden.
Beginnen wir nun mit den Targets, dessen Gültigkeit abhängig von den Targets ist.
Da wohl das wichtigste Target eine ausführbare Datei ist, sollten wir wohl mit dieser anfangen und arbeiten uns weiter vor.

Tags für exe
Tag Beschreibung
app-type Werte sind nur „gui“ oder „app“. Legt fest, ob das Target eine Konsolenanwendung ist oder eine grafische Oberfläche bestitz. (nur wichtig für Windows, wegen des unterschiedlichen linkens)
exename Legt den Namen der Datei fest. Standardmäßig ist dieser Wert gleich der ID und muss somit nicht umgeändert werden
Tags für lib
Tag Beschreibung
libname Ähnlich wie exename bei dem Target exe
Tags für dll
Tag Beschreibung
dllname er Name einer DLL-Datei (libname bei lib)
libname Siehe dllname, nur für dynamische Libs oder symlink unter Unix
so_version Nur im Autoconf-Format (wird nicht behandelt)
mac_version Nur im Autoconf-Format
Tags für subproject
Tag Beschreibung
dir Ordner in dem die Makefile liegt (Achtung es wird als Trennzeichen für Ordner / bentutz!)
target Optional, kann das Target der Makefile noch weiter sppezifizieren
installable Werte sind „yes“ oder „no“. Falls make mit dem Argument install aufgerufen wurde und installable auf yes gesetzt wird, so wird das Makefile ebenfalls mit make install aufgerufen

Werfen wir nun ein Blick in Richtung allgemein gültiger Tags. Das schöne an diesen ganzen Tags und Targets ist, dass ihr quasi einen großen Haufen von Legosteinen bekommt und diese frei zusammensetzen könnt. Somit habt ihr sehr schnell und unkompliziert eine Bakefile mit sehr vielen Targets, Abhängigkeiten und Regeln und generiert euch eine noch komplexere Makefile (Ich werde hier nur die gängisten Tags vorstellen!).

Allgemein gültige Tags
Tag Beschreibung
depends Das Target ist abhängig von einem anderen Target. Hier kann eine Liste angegeben werden mit den Target-IDs
headers Ähnlich wie sources, nur eben für Header-Dateien
dirname Setzt den Erstellpfad, wenn keiner angegeben wurde, wird „BUILDDIR“ verwendet
sources Ähnlich wie headers, Beispiel weiter oben
include fügt den übergebenden Ordner als Suchordner für Header-Dateien hinzu (vergleichbar mit dem „-I“ Schalter bei einem Compiler)
define Definiert ein Präprozessor-Macrosys-libLinkt das Target gegen eine System-Bibliothek, wobei der Tag immer nur eine Datei annimmt(Bibliothek wird NICHT von der Makefile erstellt)
lib-path das gleiche wie Include, nur für SystembibliothekenlibraryTarget wird gegen die Bibliothek gelinkt, wobei diese von der Makefile erstellt wird. Der Parameter ist eine ID eines erstellen library-targets
threading Werte sind „multi“ und „single“. Definiert ob das Target Multithreading unterstüzt werden soll (Wird bei Linux anders gelinkt als unter Windows)
cxx-exceptions Aktiviert oder Deaktiviert das Exception-Handling von C++. Werte sind dabei „on“ und „off“
cflags Setzt die clfags für den Compiler
cxxflags Setzt die cxxflags für den Compiler
cppflags Setzt die cppflags für den Compiler
ldflags Setzt die Linker-Flags
win32-res Setzt die Ressource-Datei für das Target (nur für Windows)
clean-files Setzt die Liste für Dateien, die bei dem Aufruf von „make clean“ gelöscht werden
install-to Pfad an dem die Dateien kopiert/installiert werden, wenn „make install“ ausgeführt wird
install-headers-to Das gleiche wie „install-to“, nur für Header-Dateien

Weitere Möglichkeiten

Es gibt sogenannte Conditionen, Variablen und Funktionen die man innerhalb von Bakefile setzen und ausführen lassen kann. Um den Umfang des Artikels nicht zu sprengen gehe ich hier nicht weiter darauf ein. Diejenigen, die dennoch interessiert sind, können sich gerne die Dokumentation von Bakefile angucken.

Ein kleines Beispiel

In diesem Beispiel haben wir ein einfaches Hello-World Programm.
Dieses ist von einer Bibliothek abhängig, die ebenfalls von der Makefile generiert wird.
In dieser Bibliothek deklarieren wir uns die Klasse „MeineLibKlasse“, welche lediglich beim Konstruktor und Destruktor einen kleinen Text auf die Konsole ausgibt.
Ich denke, dass dieses kleine Beispiel ausreichen sollte.
Fangen wir nun mit unserer Hello-World-Executable an.


#ifndef MEINELIBKLASSE_H
#define MEINELIBKLASSE_H

#include <iostream>

class MeineLibKlasse
{
    public:
        MeineLibKlasse();
        virtual ~MeineLibKlasse();
};

#endif // MEINELIBKLASSE_H

#include "MeineLibKlasse.h"

MeineLibKlasse::MeineLibKlasse()
{
    std::cout << "objekt von MeineLibKlasse wurder erstellt!" << std::endl;
}

MeineLibKlasse::~MeineLibKlasse()
{
    std::cout << "Objekt von MeineLibKlasse wurde gelöscht!" << std::endl;
}

#include <iostream>
#include "MeineLibKlasse.h"

using namespace std;

int main()
{
    MeineLibKlasse Eintest;

    std::cout << "Das hier sollte zwischen dem Konstruktor und dem Destruktor stehen" << std::endl;
    return 0;
}

Da wir nun die nötigen Header und Source-Dateien haben können wir uns nun der Bakefile zuwenden.


<?xml version="1.0"?>
<makefile>

  <include file="presets/simple.bkl"/>

  <lib id="MeineLib">
	<headers>MeineLibKlasse.h</headers>
	<sources>MeineLibKlasse.cpp</sources>
  </lib> 	

  <exe id="hello" template="simple">
	<headers>MeineLibKlasse.h</headers>    
	<sources>main.cpp</sources>
	<library>MeineLib</library>
  </exe>

</makefile>

Das Generieren der Makefile

Das ist wohl der Abschnitt, den ich bisher am meisten vernachlässigt habe und auf den ihr lange gewartet habt.
Bakefile benutzt Python (Achtung nicht Version >= 3.0!, falls ihr diese Python-Version installiert habt, solltet ihr euch eine Verknüpfung anlegen (cd /usr/bin/ ; ln python2 python) )
Um aus der Bakefile eine Makefile zu machen müssen wir diese mit:
bakefile Bakefile.bkl
aufrufen. Hierbei habt ihr die Möglichkeit die verschiedenen Compiler anzugeben.
Da ich meine Programme mit gcc übersetze gebe hierfür:
bakefile Bakefile.bkl -f gnu ein.
Anschließend könnt ihr einfach mit dem Befehl make die automatisch generierte makefile ausführen und euer Programm wird erstellt

Fazit

Bakefile ist echt ein schönes Programm. Es ist einfach zu bedienen und bietet für die meisten C++-Projekte genug Auswahl. Gerade durch den Tag „subproject“ kann man Bakefiles/Makefiles miteinander verschachteln.
Wer nun den Frust mit makefiles leid ist, oder einfach keine Lust hat, sich damit zu beschäftigen, für den ist Bakefile eine sehr gute alternative. Ich selber werde es für die SecSuite-Plugins verwenden.
Falls noch irgendwelche Fragen bestehen sollte -> Mail an mich.

mit freundlichen Grüßen
euer TgZero

Das ganze gibt es natürlich auch als PDF-Version.
Der Dank geht an Ghost, der sich als einziger über das schlechte Layout beim Java-Tutorial beschwert hat.