Yabu ("yet another build utility") ist ein Make-ähnliches Werkzeug zum automatisierten Kompilieren von Softwareprojekten.
Yabu ist ein Ersatz für Make, also in erster Linie ein Werkzeug für den Softwareentwickler. Hauptanwendungsgebiet von Yabu ist das Kompilieren von Programmen: nach einer Änderung der Quellen führt Yabu automatisch die erforderlichen Kommandos aus, um alle betroffenen Programmbestandteile zu aktualisieren. Allgemeiner formuliert lassen sich mit Yabu beliebige Prozesse automatisieren, bei denen Dateien ohne Benutzerinteraktion ineinander umgewandelt werden.
Für seine Arbeit benötigt Yabu eine spezielle Datei namens "Buildfile". Sie enthält erstens eine Beschreibung der Abhängigkeiten zwischen Dateien – zum Beispiel zwischen Quelltext und ausführbarem Programm – und zweitens die auszuführenden Kommandos. Yabu benutzt diese Informationen zusammen mit den Änderungszeiten der Dateien, um nach Änderung einer Quelldatei alle daraus abgeleiteten Dateien (und nur diese!) zu aktualisieren.
Vorbild von Yabu ist das weit verbreitete Programm Make. Insbesondere ist Yabus Dateiformat dem von Make sehr ähnlich, wenn auch nicht vollständig kompatibel. Im folgenden sind einige der Unterschiede zwischen Yabu und Make kurz beschrieben.
Schnell kompiliert und installiert
Yabu ist unter vielen UNIX-artigen Betriebssystemen – unter anderem FreeBSD, NetBSD, OpenBSD und Linux – mit nur drei Kommandos kompiliert und einsatzbereit:
gunzip <yabu-X.XX.tar.gz | tar xf - cd yabu-X.XX.tar.gz g++ -o yabu ya*.cc
Yabu benötigt keine weiteren Bibliotheken oder Hilfsmittel außer den im Betriebssystem enthaltenen, und (natürlich) braucht man auch kein Build-Utility, um Yabu zu kompilieren.
Aus Makefile wird Buildfile
Da es nicht kompatibel zu Make ist, benutzt Yabu einen anderen Namen für die Beschreibungsdatei, und zwar "Buildfile". Ihr Format ist dem eines Makefiles sehr ähnlich, doch es gibt einige wichtige Unterschiede.
Vereinfachte Einrückungn
Wie Make unterscheidet auch Yabu eingerückte und nicht eingerückte Zeilen. Während in einem Makefile die Einrückung stets durch ein Tabulaturzeichen erfolgen muß, sind im Buildfile beliebige Kombinationen von Tabs und Leerzeichen erlaubt.
Standard-Ziel "all"
Während bei Make das zuerst definierte Ziel zum Standard wird, ist dies bei Yabu immer das Ziel "all", unabhängig von seiner Position im Buildfile.
Mehrzeilige Kommandos
Yabu erlaubt mehrzeilige Kommandoskripte, die durch eine einzige Shell ausgeführt werden, ohne daß man jede Zeile mit "\" beenden muß. Ein Beispiel:
backup:: { DATE=`date +%Y%m%d` tar snapshot-$DATE.tar *.c }
Eingeschränkte Variablen
Bei der Verwendung von Variablen ist Yabu strikter als Make. Insbesondere zählen Referenzen auf undefinierte Variablen als Fehler (und nicht als leerer String), und bereits definierte Variablen lassen sich nicht versehentlich überschreiben.
Flexiblere %-Platzhalter
Der %-Platzhalter in Regeln kann nicht nur in den Quellen, sondern auch an beliebiger Stelle im Skript benutzt werden. Mit Yabu ist zum Beispiel Folgendes möglich:
%.o: %.pp other.h preproc %.pp %.c cc -DSRCFILE=%.c -o %.o %.c
Regeln können viele %-Platzhalter enthalten. Damit sind komplexe Konstruktionen möglich:
%/%.o: %2.c cc $(CFLAGS_%1) -o %1/%2.o %2.c
Eine mögliche Realisierung dieser Prototypregel wäre zum Beispiel:
debug/app.o: app.c cc $(CFLAGS_debug) -o debug/app.o app.c
Erweiterte Transformationsregeln
Wie Make bietet Yabu die Möglichkeit, Variablenwerte durch eine Transformationsregel zu verändern. Allerdings benutzt Yabu hierfür einen anderen Platzhalter ("*") und kann deshalb Variablentransformationen mit '%'-Platzhaltern kombinieren. Auch hierfür ein Beispiel:
OBJS=eins zwei drei all: app_debug app_release app_%: $(OBJS:*=%/*.o) $(CC) -o $(0) $(*)
Alle Ausgaben über stdout
Yabu gibt alle Meldungen – auch Fehlermeldungen des Compilers o.ä. – über die Standardausgabe aus. Mit dem einfachen Kommando
yabu | more
lassen sich die Meldungen somit bequem seitenweise betrachten. Wer das schon einmal mit Make versucht hat, weiß wahrscheinlich, warum dieses Verhalten sinnvoll ist.
Makros und Schleifen
Yabu enthält einen Präprozessor, der Schleifenkonstruktionen und Makros ermöglicht. Siehe Präprozessor: .foreach und Präprozessor: Makros.
Konfigurationen
Konfigurationen bieten einen Mechanismus, den gesamten Buildvorgang zu parametrisieren und mehrere unabhängige Varianten der gleichen Software zu erzeugen. Beispiele: Varianten für unterschiedliche Plattformen oder oder separate Debug- und Releaseversionen.
Verteilte Ausführung
Yabu kann in einem Rechnerverbund ("Build Cluster") Kommandos parallel auf verschiedenen Rechnern ausführen. Damit lassen sich Kompilierzeiten verkürzen und Programme gleichzeitg für verschiedene Plattformen kompilieren. Siehe Parallel und verteilt: Yabu im Netzwerk.
Unterprojekte
Yabu kann unabhängige Projekte in einem einzigen Hauptprojekt zusammenfassen. Unterprojekte sind Yabus Alternative zu dem oft benutzte Verfahren, innerhalb eines Makefiles wiederum make für Unterproekte aufzurufen. Letzteres geht auch mit Yabu, Unterprojekte haben aber gegebenüber dem "rekursiven make" den Vorteil, daß man logische Abhängigkeiten über Projektgrenzen hinweg einfach ausdrücken kann. Siehe Unterprojekte: die !project-Anweisung.
Das Dateiformat von Yabu ist (noch) komplexer als das von Make. Einige von Make übernommene Mechanismen, zum Beispiel Variablen und die '%'-Syntax, sind in Yabu deutlich leistungsfähiger und damit auch schwerer zu beherrschen. Unbedacht eingesetzt können sie leicht zu undurchsichtigen und schwer zu pflegenden Buildfiles führen. Tatsächlich ist Yabu aus der Idee entstanden, die Makefile-Syntax so weit wie möglich zu erweitern, ohne daß sie vollständig undurchschaubar wird. Ob das gelungen ist, sollte jeder für sich entscheiden, bevor er Yabu ernsthaft einsetzt.
Yabus Syntax enthält eine Reihe von recht willkürlichen Einschränkungen, zum Beispiel bei der Syntax von Bezeichnern, beim Gebrauch von Anführungszeichen und bei der Behandlung von Listen. Yabu hat diesbezüglich ähnliche Fähigkeiten und Einschränkungen wie Make oder die UNIX-Shell. Wer mit Make nicht zufrieden ist, weil Leer- und Anführungszeichen in Dateinamen immer wieder zu Fehlern führen, wird auch mit Yabu nicht glücklich werden.
Das Dateiformat von Yabu ist für menschliche Leser konzipiert und nicht für maschinelle Verarbeitung optimiert. Wer etwa ein XML-basiertes Dateiformat bevorzugt, sollte statt Yabu eine andere Make-Alternative suchen.
Yabu hat keinen nennenswerten Anwenderkreis und es gibt keine größeren Referenzprojekte, welche Yabu verwenden. Es könnte durchaus sein, daß in einer der nächsten Versionen noch grundlegende konzeptionelle Änderungen stattfinden.
Die folgenden Abschnitte stellen einzelne Möglichkeiten von Yabu an Beispielen vor. Dabei wird vorausgesetzt, daß der Leser mit Makefiles vertraut ist. Eine detaillierte Beschreibung der hier vorgestellten Mechanismen findet sich im Referenzteil (Referenz).
Yabu ist in C++ geschrieben und benötigt zur Übersetzung einen standardkonformen Compiler. Alle GCC-Versionen ab 2.95 können Yabu kompilieren. Im Quellverzeichnis gibt man dazu folgendes Kommando ein:
g++ -o yabu ya*.cc
Zur Installation kopiert man das Programm (yabu) und optional die Manual-Page (yabu.1) an die geeigneten Stellen.
Das Tar-Archiv yabu-x.y enthält neben dem Quellcode eine Reihe von Funktionstests im Unterverzeichnis "tests". Um die Tests auszuführen, sind im Buildfile der Name des Compilers und Compileroptionen zu konfigurieren. Die vordefinierten Werte gelten für den GNU-Compiler (g++). Nach Anpassung der Konfiguration startet man die Tests mit
./yabu tests
Yabu kompiliert sich dann erneut selbst und führt die mitgelieferten Selbsttests aus. Dabei sollten keine Fehler auftreten.
Das folgende Listing zeigt ein einfaches Buildfile, in dem bereits die wichtigsten Mechanismen von Yabu benutzt werden. Es beschreibt zwei Programme – prog1 und prog2 – die aus verschiedenen Quellen kompiliert werden.
CC=gcc -->(a) CFLAGS=-g -Wall COMMON=eins.o zwei.o drei.o all: prog1 prog2 -->(b) prog1: $(COMMON) x1.o y1.o -->(c) $(CC) -o $(0) $(*) prog2: $(COMMON) x2.o y2.o $(CC) -o $(0) $(*) %.o: %.c -->(d) $(CC) $(CFLAGS) -c -o $(0) $(1)
Bemerkungen:
Das Buildfile beginnt mit der Definition einiger Variablen, hier der Compiler, Compileroptionen sowie die Liste der gemeinsamen Module.
Zeilen der Form Ziel: Quelle(n) beschreiben eine Abhängigkeit.
Das Ziel all
hat eine spezielle Bedeutung: es legt fest, was passiert,
wenn man Yabu ohne weitere Argumente aufruft.
In diesem Beispiel würde Yabu die beiden Programme prog1
und prog2
kompilieren.
Die Reihenfolge der Regeln spielt keine Rolle.
Man könnte die "all"-Regel genauso gut an das Ende des Buildfile schreiben.
Eine weitere Regel definiert, daß prog1
aus den den Objektdateien
eins.o
, zwei.o
, drei.o
, x1.o
und y1.o
erzeugt wird.
Die ersten drei Quellen sind in Form der Variable $(COMMON) angegeben,
die unter (a) definiert ist.
Regel (c) enthält ein Skript, das hier aus einem einzigen Kommando besteht.
Im Skript sieht man neben der Variablen CC zwei weitere, spezielle Variablen:
$(0) ist immer der Name des Ziels, hier also prog1
.
$(*) ist die Liste aller Quellen. Ausgeschrieben würde die
Zeile also lauten:
gcc -o prog1 eins.o zwei.o drei.o x1.o y1.o
Die letzte Regel ist eine sogenannte Prototyp-Regel. Hierunter versteht man eine Regel, die Platzhalter (%) enthält. Das "%" steht für eine beliebige Zeichenfolge. Prototyp-Regeln helfen, Wiederholungen zu vermeiden. Ohne Platzhalter müßte man für jede Objektdatei eine eigene Regel schreiben:
eins.o: eins.c $(CC) $(CFLAGS) -c -o $(0) $(1) zwei.o: zwei.c $(CC) $(CFLAGS) -c -o $(0) $(1) ...
Inhalt » Einführung und Überblick » Yabu in Beispielen
Im folgenden Beispiel werden zwei Erweiterungen von Yabu gegenüber Make vorgestellt: Prototyp-Regeln können mehrere %-Platzhalter enthalten, und die Platzhalter können auch im Skript benutzt werden.
Im Beispiel soll das Programm prog
aus den drei C-Quelldateien eins.c
,
zwei.o
und drei.o
kompiliert werden.
Außerdem werden drei Varianten des Programms benötigt: eine normal kompilierte,
eine zur Fehlersuche und schließlich eine statisch gelinkte Variante.
Die drei Varianten unterschieden sich nur durch unterschiedliche Optionen beim
Kompilieren und Linken.
Zu jeder Variante existiert ein Unterverzeichnis ("release", "debug" bzw. "static"),
in dem temporäre Dateien und das fertige Programm abgelegt werden.
Das Buildfile für dieses Szenario könnte dann so aussehen:
CC=gcc # Einstellungen für release-Konfiguration CFLAGS_release=-O -Wall LFLAGS_release= # Einstellungen für debug-Konfiguration CFLAGS_debug=-g -D_DEBUG -Wall LFLAGS_debug= # Einstellungen für static-Konfiguration CFLAGS_static=-O LFLAGS_static=-static CONFIGS=release debug static OBJS=eins zwei drei all: $(CONFIGS) $(CONFIGS): $(0)/prog ---> (a) %/prog: $(OBJS:*=%/*.o) ---> (b) $(CC) $(LFLAGS_%) -o $(0) $(*) %/%.o: %2.c ---> (c) $(CC) $(CFLAGS_%1) -o $(0) -c $(1) clean:: ---> (d) rm -f $(CONFIGS:*=*/**)
Ein Aufruf von Yabu ohne Argumente würde dann folgende Kommandos ausführen (vorausgesetzt, die betreffenden .o-Dateien existieren noch nicht oder sind veraltet):
gcc -O -Wall -o release/eins.o -c eins.c gcc -O -Wall -o release/zwei.o -c zwei.c gcc -O -Wall -o release/drei.o -c drei.c gcc -o release/prog release/eins.o release/zwei.o release/drei.o gcc -g -D_DEBUG -Wall -o debug/eins.o -c eins.c gcc -g -D_DEBUG -Wall -o debug/zwei.o -c zwei.c gcc -g -D_DEBUG -Wall -o debug/drei.o -c drei.c gcc -o debug/prog debug/eins.o debug/zwei.o debug/drei.o gcc -O -o static/eins.o -c eins.c gcc -O -o static/zwei.o -c zwei.c gcc -O -o static/drei.o -c drei.c gcc -static -o static/prog static/eins.o static/zwei.o static/drei.o
Bemerkungen:
Dies ist eine Regel mit drei Zielen, nämlich "release", "static" und "debug". Das $(0) auf der rechten Seite steht immer für das eine(!) Ziel, auf das die Regel angewendet wird. Man könnte diese Zeile also auch als drei separate Regeln schreiben:
release: release/prog static: static/prog debug: debug/prog
In der Regel zum Linken der drei Programmvarianten sind zwei Details bemerkenswert:
Die Referenz auf OBJS enthält eine Transformationsregel. Sie funktioniert wie bei Make, benutzt als Platzhalter aber "*". Das hat den Vorteil, daß man "%" auch innerhalb der Transformationsregel benutzen kann.
Selbst in Variablennamen sind %-Platzhalter zulässig, wie das Beispiel
$(LFLAGS_%)
zeigt.
Angewendet auf das Ziel "debug/prog" würde die Regel somit
debug/prog: debug/eins.o debug/zwei.o debug/drei.o $(CC) $(LFLAGS_debug) -o $(0) $(*)
lauten.
Zum Kompilieren der C-Quellen reicht wieder eine einzige Prototyp-Regel, allerdings mit zwei Platzhaltern. Der erste steht für das Unterverzeichnis, der zweite für den Dateinamen. Referenzen auf die Platzhalter schreibt man als %1, %2, usw. Angewendet auf "debug/eins.o" und nach Ersetzung aller Variablen würde die Regel so aussehen:
debug/eins.o: eins.c gcc -g -Wall -o debug/eins.o -c eins.c
Das Ziel "clean" dient per Konvention dazu, alle temporären Dateien zu löschen. Man beachte den rechten Teil der Transformationsregel: "**" steht hier für das Zeichen "*" (ohne Platzhalterfunktion). Ausgeschrieben lautet das Kommando also
rm -f release/* static/* debug/*
Die Schreibweise "clean::" an Stelle von "clean:" zeigt an, daß "clean" keine Datei sondern ein sogenannter Alias ist. In einem Makefile würde man dies als "phony Target" bezeichnen.
Yabu verfügt über einen Präprozessor, der unter anderem eine einfache Schleifenkonstruktion ermöglicht. Das folgende Buildfile erzeugt zwei Programme "client" und "server" in drei verschiedenen Varianten (debug, release und static). Die Regel zur Erzeugung der Programme unterscheidet sich nur in den Quellen (spezifisch für jedes Programm) und den Compileroptionen (spezifisch für jede Variante). Sie läßt sich mit Hilfe einer verschachtelten Schleife sehr kompakt schreiben:
SRCS_server=srv1.c srv2.c common.c SRCS_client=cli1.c cli2.c common.c CFLAGS_debug=-g -Wall CFLAGS_release=-O CFLAGS_static=-static .foreach cfg debug release static -----> (a) .foreach prog server client -----> (b) $(.prog).$(.cfg): $(SRCS_$(.prog)) -----> (c) cc $(CFLAGS_$(.cfg)) -o $(0) $(*) all:: $(.prog).$(.cfg) -----> (d) .endforeach .endforeach
Bemerkungen:
Die Variable $(.cfg) durchläuft die Werte debug, release, static.
Die Variable $(.prog) durchläuft die Werte server, client
Regel zur Erzeugung des Programms.
Dieser Regel macht das Standardziel "all" von dem Programm abhängig. Ein Aufruf von Yabu ohne Argumente erzeugt somit alle Programme in allen Varianten.
Für ein Ziel können beliebig viele Regeln definiert sein, solange nur maximal eine Regel ein Skript enthält.
Ohne die .foreach
-Anweisungen müßte man die 12 Regeln einzeln schreiben:
server.debug: $(SRCS_server) cc $(CFLAGS_debug) -o $(0) $(*) all:: server.debug client.debug: $(SRCS_client) cc $(CFLAGS_debug) -o $(0) $(*) all:: client.debug server.release: $(SRCS_server) cc $(CFLAGS_release) -o $(0) $(*) all:: server.release client.release: $(SRCS_client) cc $(CFLAGS_release) -o $(0) $(*) all:: client.release server.static: $(SRCS_server) cc $(CFLAGS_static) -o $(0) $(*) all:: server.static client.static: $(SRCS_client) cc $(CFLAGS_static) -o $(0) $(*) all:: client.static
Statt mit einer Schleifenkonstruktion läßt sich das Buildfile aus dem vorigen Beispiel auch mit Hilfe eines Makros realisieren:
CFLAGS_debug=-g -Wall CFLAGS_release=-O CFLAGS_static=-static .define app name cfg objs... -----> (a) $(.name).$(.cfg): $(.objs) cc $(CFLAGS_$(.cfg)) -o $(0) $(*) all:: $(.name).$(.cfg) .enddefine SRCS_server=srv1.c srv2.c common.c .app server debug $(SRCS_server) ------> (b) .app server release $(SRCS_server) .app server static $(SRCS_server) SRCS_client=cli1.c cli2.c common.c .app client debug $(SRCS_client) .app client release $(SRCS_client) .app client static $(SRCS_client)
Bemerkungen:
Zunächt wird das Makro ".app" definiert. Es hat drei Argumente (name, cfg, objs) und definiert zwei Regeln. Die Makroargumente werden mit der gleichen Syntax wie Schleifenvariablen referenziert, zum Beispiel $(.name).
Aufruf des Makros mit den Argumenten name="server", cfg="debug" und objs="srv1.c srv2.c common.c". Der Aufruf erzeugt folgende Regeln:
server.debug: srv1.c srv2.c common.c cc $(CFLAGS_debug) -o $(0) $(*) all:: server.debug
In diesem Beispiel wurde bewußt nur eine Schleifenebene als Makro realisiert. Das Buildfile ließe sich noch weiter verkürzen, wenn man ein weiteres Makro einführt:
.define app3 name objs .app $(.name) debug $(.objs) .app $(.name) release $(.objs) .app $(.name) static $(.objs) .enddefine
Das eigentliche Buildfile reduziert sch dann zu
.app3 server srv1.c srv2.c common.c .app3 client cli1.c cli2.c common.c
Im folgenden betrachten wir noch einmal den Anwendungsfall aus , also ein einzelnes Programm (prog), das aus mehreren C-Quellen kompiliert und gelinkt wird. Das Programm soll wieder in den drei Varianten "release", "debug" und "static" erzeugt werden.
Bei der oben vorgestellten, auf %-Platzhaltern basierenden Realisierung wurde bereits deutlich, daß sich der Unterschied zwischen den Varianten auf spezifische Werte der Variablen CFLAGS und FLAGS reduzieren läßt. Hierfür bietet Yabu einen sehr leistungsfähigen Mechanismus, die sogenannten Konfigurationen. Hier ist das zugehörige Buildfile:
!options cfg(release static debug) -->(a) CFLAGS= LFLAGS= STRIP= !configuration [release] -->(b) CFLAGS=-O -Wall LFLAGS= STRIP=strip !configuration [static] CFLAGS=-O -Wall LFLAGS=-static STRIP=strip !configuration [debug] CFLAGS=-g -Wall LFLAGS=-g STRIP=true CC=gcc OBJS=eins zwei drei CONFIGS=release static debug all: $(CONFIGS) $(CONFIGS): $(0)/prog %/prog: [%1] $(OBJS:*=%/*.o) -->(c) $(CC) $(LFLAGS) -o $(0) $(*) $(STRIP) $(0) %/%.o: [%1] %2.c -->(d) $(CC) $(CFLAGS) -o $(0) -c $(1)
Bemerkungen:
Hier wird eine Gruppe von drei Optionen – "release", "static" und "debug" – definiert, die den drei Programmvarianten entsprechen.
Für jede der drei Konfigurationen wird ein Satz von Werten für die Variablen CFLAGS, LFLAGS und STRIP definiert. (Bemerkung: "Konfiguration" ist hier gleichbedeutend mit "Option", das es nur ein einzige Optionsgruppe gibt)
Das erste Wort rechts vom ":" ist keine Quelle, sondern eine sogenannte Konfigurationsauswahl, erkenntlich an den eckigen Klammern. Die mit [...] ausgewählte Konfiguration gilt bei der Ausführung des Skriptes. Wie man sieht, kann man auch hier den %-Platzhalter verwenden. Für das Ziel "debug/prog" würde die Regel also folgendermaßen lauten:
debug/prog: [debug] $(OBJS:*=debug/*.o) gcc -g -o $(0) $(*) true $(0)
Jetzt dürfte auch klar sein, warum STRIP in der Konfiguration debug den Wert "true" hat und nicht etwa leer gelassen wurde.
Auch die Regel zum Kompilieren der C-Quellen enthält eine Konfigurationsauswahl. Zum Beispiel würde debug/eins.o immer in der Konfiguration "debug" kompiliert, also mit "CFLAGS=-g -Wall"