Yabu - Eine Alternative zu Make
Yabu - Eine Alternative zu Make
Version: 1.21 (20.07.2009)

Yabu ("yet another build utility") ist ein Make-ähnliches Werkzeug zum automatisierten Kompilieren von Softwareprojekten.

Inhaltsverzeichnis

1 Einführung und Überblick

» » »

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.

.1 Yabu und Make

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.

.2 Was spricht gegen Yabu?

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.

.3 Yabu in Beispielen

» » » » » »

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).

..1 Yabu kompilieren und installieren

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.

..2 Ein einfaches Beispiel

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:

(a)

Das Buildfile beginnt mit der Definition einiger Variablen, hier der Compiler, Compileroptionen sowie die Liste der gemeinsamen Module.

(b)

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.

(c)

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
(d)

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)
...

..3 Prototypregeln mit mehreren Platzhaltern

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:

(a)

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
(b)

In der Regel zum Linken der drei Programmvarianten sind zwei Details bemerkenswert:

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.

(c)

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
(d)

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.

..4 Präprozessor: .foreach

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:

(a)

Die Variable $(.cfg) durchläuft die Werte debug, release, static.

(b)

Die Variable $(.prog) durchläuft die Werte server, client

(c)

Regel zur Erzeugung des Programms.

(d)

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

..5 Präprozessor: Makros

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:

(a)

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).

(b)

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

..6 Konfigurationen

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:

(a)

Hier wird eine Gruppe von drei Optionen – "release", "static" und "debug" – definiert, die den drei Programmvarianten entsprechen.

(b)

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)

(c)

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.

(d)

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"