Referenz

2 Referenz

» » » » » » » » » » » » » » » » » » » » » » »

.1 Quellen, Ziele und Regeln

Hauptanwendung von Yabu ist das automatische Erzeugen ("Build") von Programmen. Dabei sind eine Reihe von Einzelschritten – zum Beispiel Compiler- und Linkeraufrufe – in der richtigen Reihenfolge auszuführen. Jeder Einzelschritt wird durch eine Regel beschrieben; sie enthält die erforderlichen Kommandos, um aus einer Reihe von Eingabedateien (den Quellen) eine Ausgabedatei (Ziel) zu erstellen. Zum Beispiel produziert ein C-Compiler aus dem Quelltext eine Objektdatei mit Maschinencode. Es kann auch vorkommen, daß in einem Schritt zwei oder mehr Ausgabedateien erzeugt werden; die betreffende Regel hat dann mehrere Ziele. Eine Datei kann Quelle und Ziel zugleich sein, nämlich dann, wenn die Ausgabe eines Einzelschrittes als Eingabe für den folgenden Schritt dient. Zum Beispiel könnte die vom C-Compiler erzeugte Objektdatei im nächsten Schritt beim Linken des Programms als Quelle auftreten.

Wurde ein Kommando von Yabu erfolgreich ausgeführt, dann gilt das zugehörige Ziel als "erreicht". Beim Aufruf von Yabu gibt der Benutzer einen Satz von Zielen vor. Yabu endet, wenn alle vorgegebenen Ziele erreicht sind.

Eine Regel enthält also zwei Informationen: erstens die Beziehung zwischen Quellen und Ziel und zweitens die Kommandos, um das Ziel aus den Quellen zu erzeugen. Yabu benötigt beide Angaben, jedoch zu unterschiedlichen Zeiten; siehe dazu Wie Yabu arbeitet. Einen Sonderfall bilden Regeln, die kein Kommando enthalten, sondern nur aus Quellen und Ziel bestehen. Man benutzt sie, um zusätzliche Abhängigkeiten zu beschreiben, die aus praktischen Gründen nicht in einer Regel enthalten sind.

Eine spezielle Form von Zielen sind die sogenannten Aliase. Ein Alias ist keine Datei, sondern ein frei definierter Name, der zum Beispiel für eine Gruppe von Dateien steht, oder auch für eine bestimmte Aktion. Siehe Alias-Ziele.

.2 Wie Yabu arbeitet

Um Yabus Arbeitsweise zu verstehen und Buildfiles richtig zu lesen, muß man wissen, daß jeder Programmlauf aus zwei Phasen besteht. In der ersten Phase liest Yabu das Buildfile und erzeugt daraus ein Regelwerk, welches die verfügbaren Ziele, ihre Abhängigkeiten sowie die auszuführenden Kommandos beschreibt. Phase 1 gliedert sich in folgende Teilabschnitte:

In der zweiten Phase wendet Yabu das Regelwerk auf die vom Benutzer vorgegebenen Ziele an und führt die notwendigen Kommandos aus. Das ist ein iterativer Prozeß: ein Ziel hat in der Regel eine oder mehrere Quellen, die vor dem Ziel erreicht werden müssen, die Quellen haben ihrerseits weitere Quellen und so weiter. Details hierzu finden sich in . Yabu führt Kommandos nur aus, wenn das Ziel in Bezug auf seine Quellen veraltet ist; als Kriterium dienen normalerweise die Änderungszeiten der betroffenen Dateien. Siehe dazu .

Zur Phase 2 gehört auch die Verarbeitung von Variablen (siehe Variablen). Man beachte, daß Yabu zwei Arten von Variablen kennt: die oben genannten Präprozessor-Variablen und "gewöhnliche" Phase-2-Variablen. Erstere werden in Phase 1 ersetzt; ihr Wert ist somit in Phase 2 konstant. Gewöhnliche Variablen wertet Yabu dagegen so spät wie möglich aus und berücksichtigt dabei die gerade gültige Konfiguration (siehe Konfigurationen). Der Wert einer Variablen hängt deshalb nicht von der Stelle im Buildfile sondern vom gerade betrachteten Ziel und von der gewählten Konfiguration ab.

.3 Dateiformat und Vorverarbeitung

Dateiformat

Bevor Yabu ein Buildfile interpretiert, durchläuft der Inhalt eine Vorverarbeitung, die aus drei Schritten besteht:

  1. Leerzeichen und Tabs am Ende einer Zeile werden abgeschnitten.

  2. Endet eine Zeile mit einem Backslash ("\"), dann zählt die folgende Zeile als Fortsetzung: das "\" wird entfernt, und die Folgezeile an die bestehende Zeile angehängt. Dieser Schritt kann sich beliebig oft wiederholen

  3. Beginnt die Zeile mit dem Zeichen "#" (in der ersten Spalte!) oder ist sie leer, dann wird sie verworfen.

Man beachte die Reihenfolge der einzelnen Schritte, insbesondere daß Fortsetzungszeilen vor der Eliminierung von Kommentaren und Leerzeilen zusammengesetzt werden. Dazu ein Beispiel:

# Dies ist ein Kommentar\
Auch diese Zeile ist ein Kommentar

Umgekehrt verliert das führende "#" seine Bedeutung, wenn es in einer Fortsetzungszeile steht:

Kein Kommentar\
#Auch diese Zeile ist KEIN Kommentar

Beachtenswert ist außerdem, daß Leerzeichen am Zeilenende bereits vor der Zusammenführung von Zeilen abgeschnitten werden. Hinter dem "\" können also beliebig viele Leerzeichen und Tabs stehen, sie werden von Yabu ignoriert. Leerzeichen am Anfang der nächsten Zeile bleiben dagegen bei der Zusammenführung erhalten, und Yabu fügt auch keine zusätzlichen Leerzeichen ein. Zum Beispiel ist

Zeile1\
Zeile2\
 Zeile3

äquivalent zu

Zeile1Zeile2 Zeile3

Die Einrückung

Während Leerzeichen am Zeilenende immer abgeschnitten werden, bleiben führende Leerzeichen und Tabs erhalten, denn sie sind ein syntaktisches Element: wie Make unterscheidet Yabu eingerückte von nicht eingerückten Zeilen. Wie groß die Einrückung ist, und ob sie mittels Leerzeichen, Tabs oder einer Kombination beider erreicht wird, ist nicht relevant.

.4 Die .include- und .include_output-Anweisung

Kurz und knapp

Beschreibung

Mit .include fügt man eine andere Datei in das Buildfile ein. Im einzelnen bedeutet das, die angegebene Datei durchläuft die oben beschriebene Vorverarbeitung und der daraus entstandene Text ersetzt die Zeile mit der .include-Anweisung. Der .include-Mechanismus hilft bei der Organisation von Buildfiles: man häufig benutzte Definitionen und Regeln schreibt man in eine zentrale Datei, die man in den Buildfiles per .include einbindet. So erspart man sich unnötige Wiederholungen und vereinfacht die Pflege der Buildfiles.

Es gibt verschiedene Formen der Anweisung. Sie unterscheiden sich darin, wie Yabu den Dateinamen interpretiert:

.include /yabu/include/Buildfile.inc
.include Buildfile.inc
.include <Buildfile.inc>

Beginnt der Dateiname mit '/', dann handelt es sich um einen absoluten Pfad, und Yabu fügt die angegebene Datei ein. Andernfalls (zweite Zeile) gilt der Dateiname relativ zu der Datei mit der .include. Ein Beispiel: angenommen, das Buildfile befindet sich im aktuellen Verzeichnis und enthält die Zeile

.include ../common/Buildfile.glob

und Buildfile.glob enthält wiederum die Zeile

.include arch/Buildfile.i386

Beim Aufruf von Yabu ohne Argumente würden dann folgende Dateien gelesen:

Die dritte Form mit "<...>" bewirkt, daß Yabu die Datei in CFGDIR/include sucht. Dabei ist CFGDIR das globale Konfigurationsverzeichnis, das man beim Aufruf von Yabu mit der Option -g (oder über die Umgebungsvariable YABU_CFG_DIR) setzt. In diesem Falle spielt es keine Rolle, ob der Dateiname mit '/' beginnt oder nicht.

Der Dateiname kann Variablen (siehe Präprozessor-Variablen) enthalten. Zum Beispiel könnte man in einer .foreach-Schleife (siehe unten) mehrere Dateien nacheinander einfügen. Auch die von Yabu vordefinierten Variablen lassen sich verwenden. Ein Beispiel: die Anweisung

.include /yabu/lib/$(.SYSTEM)/osdefs

fügt die Datei "osdefs" aus einem plattformspezifischen Verzeichnis ein.

Regenerieren fehlender Include-Dateien

Die .include-Anweisung kann bei Bedarf um ein sogenanntes Regenerierungsskript erweitert werden. Man schreibt dazu das Regenerierungsskript eingerückt direkt hinter die .include-Anweisung:

.include DATEINAME
    Script
    ...

Yabu führt das Regenerierungsskript immer dann aus, wenn die angegebene Datei nicht existiert. Nach Beendigung des Skriptes erwartet Yabu, daß die Datei existiert (und bricht andernfalls mit einer Fehlermeldung ab). Variablen (siehe Präprozessor-Variablen) im Skript werden vor der Ausführung ersetzt; zusätzlich steht $(._) für den Dateinamen.

Eine mögliche Anwendung für Regenerierungsskripte ist, Dateien bei Bedarf aus einem Versionskontrollsystem zu extrahieren. Zum Beispiel:

.rules_version=1.23
.include Buildfile.rules
    cvs update -r $(.rules_version) $(._)

Diese Konstruktion hat allerdings einen Fehler: wenn die Datei bereits existiert, prüft Yabu nicht, ob es sich wirklich um die angegebene Version (1.23) handelt. Das heißt, nach einer Änderung von rules_version muß der Benutzer die vorhandene Datei selbst aktualisieren oder löschen. In manchen Fällen mag das praktikabel sein, es geht aber auch vollautomatisch, indem man den Dateinamen aus der Version ableitet:

.rules_version=1.23
.include Buildfile.rules.$(.rules_version)
    cvs update -r $(.rules_version) $(._)

Schutz gegen mehrfaches Include mit .once

In größeren Projekten mit vielen Buildfiles kann es leicht passieren, daß man eine zentrale Datei mit allgemeinen Definitionen mehrfach einbindet. Zum Beispiel (">" steht für eine .include-Beziehung): Buildfile > Buildfile1 > Buildfile.common und Buildfile > Buildfile2 > Buildfile.common Manchmal ist das nicht schlimm und sogar gewollt. In vielen Fällen führt ein mehrfaches .include einer Datei aber zu Fehlern wie zum Beispiel die mehrfache Definition einer Variablen.

In solchen Fällen kann man einen Bereich von Zeilen – der auch die ganze Datei umfassen kann – gegen mehrfache Auswertung schützen, indem man ihn in einen sogenannten .once-Block schreibt:

.once
#Definitionen
...
.endonce

Die Zeilen zwischen .once und .endonce werden nur beim ersten Lesen der Datei ausgewertet und bei allen folgenden Malen ignoriert. Das funktioniert unabhängig vom Dateinamen, und insbesondere behandelt Yabu alle (symbolischen und echten) Links auf die gleiche Datei als identisch.

Ein Beispiel: die Datei Buildfile.ver enthalte die Zeilen

.once
VERSION=1.0
.endonce

Damit könnte man im Buildfile, ohne einen Fehler zu provozieren, folgendes schreiben:

.include Buildfile.ver
.include ./Buildfile.ver
.include subdir/../Buildfile.ver

Die zweite und dritte Anweisung sind hier wirkungslos, da der gesamte Inhalt von Buildfile.ver mit .once geschützt ist.

Dynamisch erzeugte Buildfiles mit .include_output

Eine .include_output-Anweisung arbeitet ähnlich wie .include, an Stelle einer Datei fügt sie aber die Ausgabe eines Shell-Skriptes in das Buildfile ein. Ein einfaches Beispiel: die Anweisung

.include_output
  {
     for d in * ; do
         [ -f $d/Buildfile ] && echo "all: all-$d"
     done
  }

erzeugt für jedes Unterverzeichnis, welches ein Buildfile enthält, eine Regel der Form "all:: all-VERZEICHNIS". Die beiden Zeilen "{" und "}" bewirken, daß die drei dazwischen liegenden Zeilen als ein einziges Skript ausgeführt werden. Details hierzu finden sich unter Skripte.

Yabu benutzt nur die Standardausgabe des Skripts. Fehlermeldungen erscheinen – wie alle Ausgaben von Yabu – auf der Standardausgabe, also normalerweise auf dem Terminal. Das Skript muß einen Exit-Code von 0 zurückliefern, ansonsten bricht Yabu mit einem Fehler ab.

.5 Die .foreach-Anweisung

Kurz und knapp

Beschreibung

Die .foreach-Anweisung wiederholt einen Textblock mehrfach, wobei eine spezielle Variable eine vorgegebene Werteliste durchläuft.

.foreach Variable Wert1 Wert2 ...
  ...
.endforeach

Innerhalb des .foreach-Blockes wird die Laufvariable mit $(.Variable) referenziert. Ein einfaches Beispiel:

.foreach p XX YY ZZ
$(.p): $($(.p)_objs)
    $(LINK) -o $(.p) $($(.p)_objs)
.endforeach

Dies ist äquivalent zu

XX: $(XX_objs)
    $(LINK) -o XX $(XX_objs)
YY: $(YY_objs)
    $(LINK) -o YY $(YY_objs)
ZZ: $(ZZ_objs)
    $(LINK) -o XX $(ZZ_objs)

Dieses Beispiel zeigt außerdem den Unterschied zwischen Präprozessor-Variablen (hier $(.p)) und gewöhnlichen Variablen: $(.p) wird bereits beim Einlesen der Datei ersetzt, der Wert von $(XX_objs) ist dagegen erst viel später bekannt. Beachtet man diesen Unterschied nicht, dann kommt es zu Fehlern wie im folgenden Beispiel: der Abschnitt

PROGS=XX YY ZZ
.foreach p $(PROGS)
$(.p): $($(.p)_objs)
    $(LINK) -o $(.p) $($(.p)_objs)
.endforeach

wird in Phase 1 zu

PROGS=XX YY ZZ
$(PROGS): $($(PROGS)_objs)
    $(LINK) -o $(PROGS) $($(PROGS)_objs)

umgewandelt und sicherlich nicht das tun was der Author beabsichtigt hatte.

Verschachtelte .foreach-Schleifen

.foreach-Anweisungen lassen sich ineinander verschachteln. Alle .foreach- und .endforeach-Anweisungen müssen jedoch in Spalte 1 beginnen. Einrückungen sind allerdings nicht erlaubt, statt

.foreach prog  A B C
   .foreach p X Y Z     <--- FALSCH
   .endforeach          <--- FALSCH
.endforeach

muß es

.foreach prog  A B C
.foreach p X Y Z        <--- richtig
.endforeach             <--- richtig
.endforeach

heißen.

Verschachtete Schleifen müssen verschiedene Laufvariablen verwenden.

.6 Präprozessor-Variablen

Kurz und knapp

Beschreibung

Außer in einer .foreach-Anweisung kann man Variablen der Form "$(.name)" auch direkt definieren. Die Anweisung

.srcdir=/usr/local/src/yabu

bewirkt zum Beispiel, daß in allen folgenden Zeilen $(.srcdir) durch "/usr/local/src/yabu" ersetzt wird. Namen vom Variablen dürfen Ziffern, Buchstaben, den Unterstrich ("_") sowie beliebige nicht-ASCII-Zeichen (Bytes mit einem Wert größer oder gleich 128) enthalten. Einige Beispiele:

.Größe=128

.822_header=From:      <----- FEHLER: erstes Zeichen ist eine Ziffer
.from-header=From:     <----- FEHLER: '-' nicht erlaubt

Man beachte, daß die Bedeutung von nicht-ASCII-Zeichen von der verwendeten Zeichenkodierung abhängt. Yabu selbst behandelt Variablennamen als Binärdaten und ist unabhängig von der Kodierung. Das gilt aber nur mit Einschränkungen für die Ausgabe auf dem Terminal – zum Beispiel innerhalb einer Fehlermeldung – und erst recht nicht beim Bearbeiten des Buildfiles mit einem Editor. Probleme mit unterschiedlichen Kodierungen vermeidet man am einfachsten, indem man für Variablennamen nur ASCII-Zeichen benutzt. Geht das nicht, dann sollte man durchgängig UTF-8 als Kodierung benutzen.

Präprozessor-Variablen dürfen nicht mit gewöhnlichen Variablen (siehe Variablen) verwechselt werden. Ihre Syntax ist zwar ähnlich, aber zwischen den beiden Variablenarten gibt es einige wichtige Unterschiede, die im folgenden erläutert werden. Wegen dieser Unterschiede kann es zur Verwirrung führen, wenn man in einem Buildfile beide Variablenarten mischt. Präprozessor-Variablen sollt man deshalb sparsam einsetzen; nach Möglichkeit nur als Makroargument (siehe unten) oder in .foreach-Schleifen. Eine explizite Definition nach dem obigen Muster sollte nur in Ausnahmefällen vorkommen.

PP-Variablen werden sofort ersetzt

Yabu ersetzt PP-Variablen bereits in Phase 1. Das bedeutet unter anderem, daß eine Variable vor ihrer ersten Verwendung definiert sein muß. Konstruktionen wie

.cflags=$(.cflags_common) $(.cflags_local)    <---- FEHLER
...
.cflags_common=-g
.cflags_local=-I../../include

sind nicht erlaubt. Ein willkommener Nebeneffekt dieses Verfahrens ist, daß man nicht unbeabsichtigt eine Schleife erzeugen kann, in der eine Variable von sich selbst abhängt. PP-Variablen können aber verschachtelt sein, d.h. auf der rehten Seite einer Variablendefinition kann eine Variable vorkommen. Auch der Name einer Variable darf aus Variablen gebildet werden, solange alle in einer Zeile vorkommenden Variablen an dieser Stelle bereits definiert sind. Beispiel:

.os=linux
.cflags_linux=-DLINUX
...
    ... $(.cflags_$(.os)) ...

Ähnliches gilt für Transformationsregeln (siehe unten). PP-Variablen sind sind außerdem niemals konfigurationsabhängig (siehe Konfigurationen).

Die GNU-Variante des Programms "make" hat einen ähnlichen Mechanismus. Dort schreibt man "Variable := Wert", um die sofortige Ausführung der Zuweisung zu erzwingen.

Gültigkeit von PP-Variablen

PP-Variablen können nur innerhalb ihres Gültigkeitsbereiches verwendet werden. Dieser beginnt mit der Definition und endet mit dem nächsten .enddefine, .endforeach, mit dem Ende der aktuellen Datei. Insbesondere können in einer .include-Datei definierte PP-Variablen nur innerhalb dieser Datei benutzt werden.

PP-Variablen sind unveränderlich

PP-Variablen können nach ihrer Definition nicht mehr verändert werden; die Operatoren "+=" und "?=" sind nicht verfügbar.

In einer verschachtelten .foreach-Schleifen oder .include-Datei kann man an eine bereits definierte Variable erneut einen Wert zuweisen. Dadurch erzeugt man eine neue Variable, die bis zum Ende des Gültigkeitsbereiches die vorhandene Variable "uberdeckt.

Vordefinierte Variablen

Die unter Systemvariablen beschriebenen vordefinierten Variablen lassen sich – bis auf $(_CONFIGURATION) – auch im Präprozessor verwenden. Dazu ersetzt man den Unterstrich durch einen '.', also zum Beispiel $(.SYSTEM) usw.

Transformationsregeln

Der Werte einer PP-Variablen kann mit einer sogenannten Transformationsregel verändert werden. Das funktioniert wie bei gewöhnlichen Variablen, allerdings ist nur ein Platzhalterzeichen (*) erlaubt. Beispiel:

.srcs=xxx.c yyy.c zzz.c
prog: $(.srcs:*.c=*.o)

ist äquivalent zu

prog: xxx.o yyy.o zzz.o

Die Transformationsregel kann Variablen enthalten, auch diese müssen aber bereits definiert sein.

.7 $(.glob): Verzeichnislisten

Kurz und knapp

Beschreibung

Die Variable $(.glob) hat eine spezielle Bedeutung. Mit ihr läßt sich der Inhalt eines Verzeichnisses ermitteln und in gewissen Grenzen umformen. $(.glob) muß immer mit einer Transformationsregel benutzt werden. Die linke Seite der Regel wird als Dateiname oder Pfad interpretiert und enthält normalerweise einen Platzhalter. Ein einfaches Beispiel:

$(.glob:*.c=*)

liefert die Namen aller C-Dateien im aktuellen Verzeichnis ohne die Endung .c. Im Unterschied zu Transformationsregeln bei gewöhnlichen Variablen werden Dateinamen, die nicht zur linken Seite passen, stillschweigend unterdrückt und erzeugen keinen Fehler.

Ist die linke Seite ein Pfad, dann darf der Platzhalter nur im Dateinamen, nicht aber im Verzeichnis benutzt werden. Zum Beispiel ergibt

$(.glob:../src/*.c=*.c)

die Liste aller C-Dateien im Verzeichnis ../src, jeweils ohne den Verzeichnispräfix. Dagegen würde

$(.glob:*/Buildfile=*)     <-- FEHLER

nicht wie beabsichtigt funktionieren. Mehrere Platzhalter innerhalb des Dateinamens sind jedoch erlaubt und können auf der rechten Seite der Transformation beliebig, auch als Vezeichnisnamen benutzt werden. Zum Beispiel würde

$(.glob:src/*.*=*2/*1.*2)

für den Dateinamen "src/test.c" den Wert "c/test.c" liefern.

Die von der Shell bekannten Platzhalter "?" und "[...]" werden nicht unterstützt. Das Ergebnis – nach Anwendung der Transformation – ist immer alphabetisch sortiert. Als Vereinfachung darf man die rechte Seite der Transformationsregel weglassen, wenn sie die linke Seite lediglich reproduziert. Das heißt, statt

$(.glob:src/*.c=src/*.c)

kann man kurz

$(.glob:src/*.c)

schreiben.

Wichtig ist, daß Yabu $(.glob) in Phase 1 ausführt. Das Ergebnis spiegelt immer den Zustand beim Start von Yabu wieder. Wenn im Laufe der Phase 2 Dateien erzeugt und gelöscht werden, wirkt sich das nicht mehr auf $(.glob) aus.

.8 $(.find) und $(.dirfind): Verzeichnisse durchsuchen

Kurz und knapp

Beschreibung

Die "Variable" $(.find) funktioniert ähnlich wie $(.glob), ist aber aber leistungsfähiger. Mit $(.find) kann man Verzeichnisse rekursiv durchsuchen und Dateien über frei definierbare Ein- und Ausschlußregeln auswählen. Wie der Name bereits suggeriert, ist $(.find) für Fälle gedacht, in denen man sonst das UNIX-Kommandos "find" verwenden würde. Ein Beispiel: das Ziel "all-headers" soll von allen Dateien im Verzeichnis include abhängen, deren Name mit .h endet. Das könnte man mit Hilfe einer .include_output-Anweisung erreichen:

.include_output
     find include -type f -name '*.h' | xargs echo "all-headers::"

Einfacher und intuitiver bewirkt man das Gleiche mit

all-headers:: $(.find(include/)(**.h))

Diese Lösung hat außerdem den Vorteil, daß sie nicht von einem externen Programm abhängt.

Das obige Beispiel zeigt bereits die allgemeine Form von $(.find): auf den Variablennamen folgen zwei in (...) eingeschlossene Listen, die den den beiden Grupen von Argumenten beim UNIX-find entsprechen, nämlich den zu durchsuchenden Verzeichnissen und dem Filterausdruck. Im allgemeinen hat $(.find) folgende Form:

$(find([?]dir1[/] [?]dir2[/] ...))
$(find([?]dir1[/] [?]dir2[/] ...)([!]pat1[=repl1] [!]pat2[=repl2] ...))

Der "/" am Ende eines Verzeichnisses bedeutet, daß auch alle Unterverzeichnisse dursucht werden sollen. Endet der Name nicht mit einem "/", dann liefert $(.find) nur Dateien in dem angegebenen Verzeichnis. Ein vorangestelltes "?" unterdrückt die Fehlermeldung in dem Fall, daß das Verzeichnis nicht existiert. Andere Fehler wie zum Beispiel unzureichende Zugriffsrechte oder die Angabe einer gewöhnlichen Dateien als Verzeichnis lassen sich nicht abschalten und führen immer zu einer Fehlermeldung. Beispiel:

$(.find(include srcs/ ?contrib/))

sucht Dateien in "include" (ohne Unterverzeichnisse), "src" (einschließlich allen Unterverzeichnissen) und, falls vorhanden, "contrib".

Das Ergebnis der Suche ist eine durch Leerzeichen getrennte Liste aller gefundenen regulären Dateien. Die Liste kann leer sein, was nicht als Fehler zählt. Verzeichnisse, symbolische Links und spezielle Dateien wie Pipes, Sockets, Blockgeräte usw. sind nicht im Ergebnis enthalten. Bei einer rekursiven Suche folgt $(.find) niemals symbolischen Links, sondern verzweigt stets nur in echte Unterverzeichnisse. Einzige Ausnahme: das beim Aufruf angegebene Startverzeichnis kann ein symbolischer Link auf ein Verzeichnis sein.

Sind nicht alle sondern nur bestimmte Dateien gesucht, dann gibt man in einer zweiten Klammer eine Liste von Ein- und Ausschlußregeln an. Einschlußregeln fügen Dateien zum Ergebnis hinzu. Im einfachsten Fall besteht die Regel nur aus einem Muster, welches bis zu 9 Platzhalterzeichen (*) enthalten darf. Das Muster beschreibt immer den vollständgen Pfad, ausgehend von dem angegebenen Startverzeichnis der Suche. Zum Beispiel liefert

$(.find(doc/)(**.tex))

als Dateien unterhalb von doc/, deren Name mit .tex endet. Man beachte, daß $(.find) den Platzhalter anders interpretiert als die Shell oder das find-Kommando: ein einzelnes "*" steht für eine Folge beliebiger Zeichen mit Ausnahme von "/" und ".", "**" steht für eine beliebige Zeichenfolge, die auch "/" und "." enthalten kann. Der Grund hierfür ist, daß Yabu Platzhalter überall gleich behandelt; die beschriebene Verhalten gilt auch für Transformationsregeln bei Variablen (siehe Transformationsregeln) und für Prototyp-Regeln (siehe Prototyp-Regeln (%-Platzhalter)).

Eine Regel mit Ersetzungsvorschrift (MUSTER=ERSETZUNG) arbeitet ebenfalls wie bei Variablen. Paßt ein gefundener Dateiname zu MUSTER, dann wird an Stelle der Dateinamens ERSETZUNG in das Ergebnis aufgenommen. Auf der rechten Seite steht "*" für den Teil des Namens, den der Platzhalter einnimmt. Bei mehreren Platzhaltern schreibt man "*1", "*2", usw. Zum Beispiel sucht

$(.find(doc/)(**.tex=*.dvi))

alle Dateien, deren Name mit .tex endet und ersetzt dann die Endung durch .dvi. Statt "doc/ref/intro.tex" würde also im Ergebnis "doc/ref/intro.dvi" erscheinen. Dabei spielt es keine Rolle, ob es eine Datei dieses Namens gibt oder nicht. Ein zweites Beispiel:

$(.find(.)(./**.=*))

ergibt eine Liste aller Dateien im aktuellen Verzeichnis. Die Ersetzungsvorschrift entfernt das vorangestellte "./".

Ein vorangestelltes "!" kehrt den Effekt einer Regel um, das heißt, die ausgewählten Dateien werden aus dem Ergebnis entfernt. Solche Ausschlußregeln sind nur in Verbindung mit mindestens einer vorangehenden Einschlußregel sinnvoll. Mit ihnen lassen sich komplexere Auswahlbedingungen formulieren. Zum Beispiel liefert

$(.find(doc/)(** !**.dvi))

alle Dateien unterhalb von doc, deren Erweiterung nicht .dvi ist.

Auch Ausschlußregeln können eine Ersetzungsvorschrift enthalten. Damit lassen sich Bedingungen an die Existenz mehrerer Dateien formulieren. Das ist manchmal nützlich, um zwischen echten Quelldateien und automatisch generierten Dateien zu unterscheiden. Auch hierzu ein Beispiel:

$(.find(doc/)(**.tex !**.dox=*.tex))

Dies liefert alle Dateien mit der Endung .tex mit Ausnahme derer, für die eine gleichnamige Datei mit der Endung .dox existiert.

Wie aus dem vorigen Absatz schon herausklingt, wendet Yabu die Regeln in der angegebenen Reihenfolge an. Die Reihenfolge ist deshalb meist nicht beliebig. Ohne Bedeutung ist dagegen die Reihenfolge, in der Yabu die einzelnen Dateien beim Durchlaufen der Verzeichnisse findet. Tatsächlich durchläuft Yabu zunächst alle Verzeichnisse vollständig und wendet erst danach die Ein- und Ausschlußregeln an. Um bei dem obigen Beispiel zu bleiben, betrachten wir die beiden Dateien "doc/intro.dox" und "doc/intro.tex". Ob Yabu die Dateien in dieser Reihenfolge findet oder in der umgekehrten, spielt keine Rolle. In jedem Fall würde zuerst "doc/intro.tex" zum Ergebnis hinzugefügt und danach (als Resultat von doc/intro.dox) wieder entfernt.

$(.find) liefert das Ergebnis immer als alphabetisch sortierte Liste ohne Duplikate zurück. In

$(.find(src/ include/ include/extra/)

ist deshalb das dritte Argument überflüssig, und außerdem steht im Ergebnis der Inhalt von "include" immer vor dem Inhalt von "src". Man könnte also genauso gut

$(.find(include/ src/)

schreiben.

Ein- und Ausschlußregeln wirken auf die sortierte Liste aller Dateien, nachdem alle angegebenen Verzeichnisse durchsucht wurden. Man kann deshalb ohne weiteres Dateien aus einem Verzeichnis basierend auf einem zweiten Verzeichnis hinzufügen oder entfernen. Beispiel:

$(.find(include model)(include/** !model/*.mod=include/*.y)

Dies liefert alle Dateien aus "include", mit Ausnahme der Dateien include/XXX.h, für die es eine zugehörige Datei model/XXX.y gibt.

Suche nach Verzeichnissen: $(.dirfind)

Um nach Verzeichnissen zu suchen, benutzt man $(.dirfind). Die Syntax ist wie bei $(.find), das Ergebnis enthält jedoch nur Verzeichnisse. Das Startverzeichnis, welches man beim Aufruf von $(.find) angibt, wird jedoch nicht zurückgeliefert. Zum Beispiel ergibt

$(.dirfind(.))

alle Unterverzeichnisse des aktuellen Verzeichnisses, nicht aber "." selbst.

.9 Makros

Kurz und knapp

Beschreibung

Ein Makro ist eine Folge von Textzeilen, die, einmal definiert, beliebig oft im Buildfile wiederverwendet werden kann. Makros können in vielen Fällen das Buildfile kürzer und leichter lesbar machen, da der "Aufruf" eines Makros nur eine einzelne Zeile benötigt. Vor allem aber erleichtert es die Pflege des Buildfiles, wenn man häufig benutzte Konstruktionen nicht immer wieder kopiert und modifiziert, sondern zentral an einer Stelle definiert.

Die wichtigste Anwendung von Makros ist die automatisierte Erzeugung von Regeln. Dem gleichen Zweck dienen Prototyp-Regeln mit '%'-Platzhaltern (siehe Prototyp-Regeln (%-Platzhalter)), zwischen den beiden Mechanismen gibt es jedoch einige wichtige Unterschiede:

Die beiden Mechanismen können auch in Kombination auftreten, wenn nämlich innerhalb eines Makros eine Prototypregel erzeugt wird.

Zur Definition eines Makros benutzt man die folgende Syntax:

.define NAME ARG1 ARG2 ...
  ...
.enddefine

Dabei ist NAME der Makroname, unter dem das Makro später aufgerufen wird. Für Makro-Namen gelten die gleichen Regeln wie für Variablennamen (siehe Präprozessor-Variablen); zusätzlich gilt, daß Namen von Yabu-Anweisungen (include, define, enddefine, foreach, endforeach) nicht als Makro-Namen erlaubt sind. Makronamen müssen außerdem eindeutig sein, und zwar über das gesamte Buildfile – einschließlich aller per .include eingefügten Dateien.

Der Rest der .define-Anweisung ist eine Liste der Makroargumente. Die Anzahl der Argumente ist beliebig und kann auch Null sein. Für die Namen der Argumente gelten die gleichen Regeln wie für Variablennamen. Innerhalb des Makros referenziert man die Argumente mit der Syntax "$(.arg)". Das letzte Makroargument kann den Suffix "..." tragen, wenn es für eine variable Anzahl von Argumenten stehen soll (siehe unten).

Ein einmal definiertes Makro wird mit der Syntax

.NAME WERT1 WERT2 ...

aufgerufen. Der Aufruf muß für jedes Argument einen Wert enthalten. Das letzte Argument spielt dabei eine Sonderrolle: Ist es mit dem Suffix "..." definiert, und sind beim Aufruf mehr Argumentwerte als in der Makrodefinition vorhanden, dann nimmt das letzte Argument alle restlichen Werte auf. Zum Beispiel hat in

.define mymacro arg1 arg2...
  ...
.enddefine

.mymacro A B C D E

das Argument $(.arg1) den Wert "A" und $(.arg2) den Wert "B C D E" . Als Spezialfall kann ein variables Argument auch ohne Werte sein, $(.arg1) ist dann ein leerer String. Zum Beispiel wäre

.mymacro A

ein gültiger Aufruf des obigen Makros.

Makroargumente kann man durch eine Transformationsregel umformen, und zwar mit der gleichen Sytax wie bei Variablen (siehe Transformationsregeln). Das oben definierte Makro .mymacro könnte zum Beispiel eine Referenz der Form

$(.arg2:*=obj/*.o)

enthalten, die beim Aufruf .mymacro A B C D E durch

obj/B.o obj/C.o obj/D.o obj/E.o

ersetzt würde.

Makros lassen sich verschachteln, d.h. innerhalb eines Makros kann man ein anderes Makro aufrufen.

Ein einfaches Beispiel

Das Makro

.define link app objs
$(.app:*=bin/*): $(.srcs:*=tmp/*.o)
   $(CC) -o $(0) $(*)
   strip $(0)
.enddefine

erzeugt eine Regel zum Linken und anschließenden Entfernen der Symbolinformationen. Makroargumente sind der Name des Programms und die zu linkenden Objektdateien (ohne die .o-Erweiterung). Der Aufruf

.link: prog obj1 obj2 obj3

erzeugt somit die Regel

bin/prog: tmp/obj1.o tmp/obj2.o tmp/obj3.o
   $(CC) -o $(0) $(*)
   strip $(0)

Verwendung von %-Platzhaltern in Makros

Das Zeichen "%" hat für Makros keine spezielle Bedeutung. Tatsächlich kann man beide Mechanismen kombinieren; sie stören sich nicht, da der Zeitpunkt der Anwendung verschieden ist: Makros werden in Phase 1 bei der Analyse des Buildfiles ausgewertet, die Ersetzung von %-Platzhaltern erfolgt dagegen erst bei Bedarf in Phase 2. Zum Beispiel wird aus

.define link
((0:*=bin/*.%)) : [%] (1-:*=tmp/%/*.o)
    $(CC) -o $(0) $(*)
.enddefine
.link: app obj1 obj2

die Regel

bin/%/app: [%] tmp/%/obj1.o tmp/%/obj2.o
    $(CC) -o $(0) $(*)

Benannte Makroargumente

Bei der oben beschriebenen Aufrufsyntax werden die Argumente durch ihre Position den formalen Parameters zugeordnet. Dies ermöglicht eine kompakte Schreibweise, hat aber auch Nachteile: Makroaufrufe mit vielen Argumenten werden leicht unübersichtlich und – wichtiger – Makroargumente können keine Leerzeichen enthalten. Um diese Beschränkungen zu umgehen, gibt es eine zweite Aufrufsyntax für Makros, bei denen die Argumente zeilenweise aufgeführt und Namen versehen werden. Hierzu schreibt man die Argumente in der Form "name=Wert" in eingerückten Zeilen unmittelbar nach dem Makroaufruf. Ein Beispiel:

.define xbuild prog objs libs flags
$(.prog): $(.objs)
    $(LINK) $(.flags) -o $(0) $(.objs) $(libs)
.enddefine
  ...
.xbuild
   prog=app
   objs=obj1.o obj2.o
   libs=lib1.a lib2.a
   flags=-L../lib

Beide Formen lassen sich auch kombinieren, soweit die Zuordnung der Argumente über ihre Reihenfolge eindeutig möglich ist:

.xbuild app
   flags=-L../lib
   objs=obj1.o obj2.o
   libs=lib1.a lib2.a

Im letzen Beispiel sieht man zugleich, daß die Reihenfolge der benannten Argumente keine Rolle spielt. Natürlich dürfen keine Argumente undefiniert bleiben, und jedes Argument darf nur einmal aufgeführt werden.

Standardwerte für Makroargumente

In einer Makrodefinition kann man für jedes Argument einen Standardwert vorgeben. Beim Aufruf des Makros darf man dann das betreffende Argument weglassen, und es erhält automatisch den Standardwert:

.define compile name variant(std)
$(.name).o: $(.name).c
   $(CC) $(CFLAGS) -DVARIANT=$(.variant) -c $(1)
.enddefine

.compile a                <----- -DVARIANT=std
.compile a fast           <----- -DVARIANT=fast

Einen Standardwert schreibt man unmittelbar hinter den Argumentnamen, und zwar in einer der drei Formate

argument(standartwert)
argument[standartwert]
argument{standartwert}

Die drei Formen sind gleichberechtigt, unterscheiden sich aber in einem Detail: bei der Verwendung von "(...)" darf der Standardwert nicht das Zeichen ")" enthalten, bei "{...}" und "[...]" ist "]" bzw. "}" verboten. Ein Standardwert, der alle drei Zeichen ")", "]" und "}" enthält, ist nicht möglich. Im folgenden Beispiel enthält der Standardwert das Zeichen ")" und wird deshalb mit der '{...}'-Syntax angegeben:

.define compile name compiler{$(CC)}
$(.name).o: $(.name).c
   $(.compiler) -c $(1)
.enddefine

.compile a                <----- a.c mit $(CC) kompilieren
.compile a g++            <----- b.c mit g++ kompilieren

Makros und Variablen

Makros und Variablen (siehe Präprozessor-Variablen) sind unabhängig voneinander. Das heißt, ein Makro kann den gleichen Namen wie eine Variable haben. Eine mögliche Unklarheit entsteht, wenn man ein solches Makro aufruft und das erste Argument mit einem '=' beginnt. Yabu interpretiert dies immer als Makroaufruf. Ist dagegen eine Zuweisung gemeint, dann darf zwischen Variablenname und '=' kein Leerzeichen stehen. Beispiel:

.define macro arg1
  ...
.enddefine

.macro=1234      <----- Variablenzuweisung an $(.macro)
.macro =1234     <----- Aufruf des Makros .macro

Ohne die vorangehende Makrodefinition würde dagegen auch die erste Zeile als Wertzuweisung interpretiert.

Lokale Makros

Durch ein vorangestelltes "local:" in der Definition macht man ein Makro lokal. Ein lokales Makro ist nur innerhalb der Datei wirksam, in der es definiert wurde, und es gibt keine Konflikte mit gleichnamigen Makros in anderen Dateien. Beispiel:

.define local:build_app app srcs
$(.app): $(.srcs)
   ...
.enddefine

Ist ein (globales) Makro "build_app" bereits definiert, dann überdeckt die lokale Definition die globale, und zwar vom Punkt der Definition bis zum Dateiende.

Anders als Variablen werden lokale Makros nicht an include-Dateien "vererbt". Sie gelten ausscließlich in der Datei, in der sie definiert sind. Das folgende Beispiel macht den Unterschied deutlich:

.SYSTEM=linux
.define local:mymacro
   ...
.enddefine
.include otherfile

In der Datei otherfile ist die Variable "$(.SYSTEM)" definiert (und hat den Wert "linux"), nicht aber das Makro ".mymacro".

.10 Variablen

» » » »

Kurz und knapp

Beschreibung

Variablen funktionieren in Yabu ähnlich wie in Make: nach einer Zuweisung der Form "VARIABLE=WERT" kann man statt WERT auch $(VARIABLE) schreiben. Variablen dienen vor allem dazu, Wiederholungen zu vermeiden. Statt

prog: aaa.o bbb.o ccc.o ddd.o eee.o
  cc -o prog aaa.o bbb.o ccc.o ddd.o eee.o

schreibt man besser

OBJS=aaa.o bbb.o ccc.o ddd.o eee.o
prog: $(OBJS)
  cc -o prog $(OBJS)

und vermeidet damit die fehleranfällige Wiederholung der Dateiliste.

Daneben spielen Variablen in Yabu eine zentrale Rolle bei der Beschreibung verschiedener Varianten der gleichen Software. Siehe hierzu Konfigurationen.

..1 Variablen definieren

Beispiel für eine Variablenzuweisung:

CC=/opt/gcc-4.0.2/bin/gcc

Für den Variablennamen gelten die gleichen Regeln wie für Präprozessor-Variablen (siehe Präprozessor-Variablen). Die Groß- und Kleinschreibung ist relevant: PATH, Path und path bezeichnen drei verschiedene Variablen.

In einer Zuweisung können links und rechts vom "=" beliebig viele Leerzeichen und Tabs stehen. Sie werden ignoriert. Alle folgenden Zuweisungen sind also äquivalent zu der obigen:

CC  =/opt/gcc-4.0.2/bin/gcc
CC=  /opt/gcc-4.0.2/bin/gcc
CC = /opt/gcc-4.0.2/bin/gcc

Außerdem gilt – siehe Dateiformat und Vorverarbeitung – daß Yabu Leerzeichen und Tabs am Ende einer Zeile grundsätzlich abschneidet.

Soll der Variablenwert mit Leerzeichen beginnen oder enden, dann muß man ihn in (doppelte) Anführungszeichen setzen. In diesem Fall muß das letzte Zeichen der Zeile ein Anführungszeichen sein. Es ist also nicht möglich, hinter die Zuweisug einen Kommentar zu setzen

TEXT = " Fehler! "
TEXT = " Fehler! " # Meldungstext    <---- FEHLER

Abgesehen vom Abschneiden der beiden Anführungszeichen läßt Yabu den Variablenwert unverändert. Insbesondere können innerhalb der Wertes Anführungszeichen vorkommen und haben dort keine spezielle Bedeutung. Zum Beispiel hat nach

CFLAGS = "-g -DVERSION="1.2.3""

die Variable CFLAGS den Wert

-g -DVERSION="1.2.3"

In diesem Beispiel könnte man übrigens auf die umschließenden Anführungszeichen auch verzichten, da der Wert weder mit einem Leerzeichen beginnt noch mit einem endet:

CFLAGS = -g -DVERSION="1.2.3"

Innerhalb von Skripten gelten natürlich die Syntaxregeln der Shell, die das Skript ausführt. Zum Beispiel würde Yabu mit

CFLAGS=-DTITLE="Name des Programms"
prog.o: prog.c
   cc $(CFLAGS) -o prog.o prog.c

das Kommando

cc -DTITLE="Name des Programms" -o prog.o prog.c

ausführen. Wahrscheinlich ist das nicht, was der Autor beabsichtigt hatte; richtig wäre wohl

CFLAGS=-DTITLE=\"Name des Programms\"

gewesen.

Ändern von Variablenwerten

Mit der obigen Syntax kann eine Variable nur einmal definiert werden. Den Versuch, einer existierenden Variablen einen neuen Wert zuzuweisen, ahndet Yabu mit einer Fehlermeldung:

VAR=eins
VAR=zwei     <--- FEHLER!

Diese – absichtliche – Einschränkung schützt den Anwender davor, daß er eine Variable versehentlich mehrfach für verschiedene Zwecke benutzt. Das ist eine realistische Fehlerquelle, insbesondere in komplexen Projekten mit verschachtelten Buildfiles, denn in Yabu sind alle Variablen global.

Die einzige Möglichkeit einen Variablenwert zu ändern, besteht darin, weiteren Text anzuhängen. Dazu benutzt man statt "=" den Operator "+=" oder "?=". Ein Beispiel: nach

SRCS=eins.c zwei.c
SRCS+=drei.c

hat die Variable SRCS den Wert "eins.c zwei.c drei.c". Yabu fügt automatisch ein Leerzeichen zwischen den alten Wert und dem neu hinzugekommenen Teil ein. "?=" funktioniert wie "+=", fügt aber den Text hinzu, wenn er nicht bereits im Variablenwert enthalten ist. Auch hierzu ein Beispiel:

CFLAGS=-g -I/usr/local/include
CFLAGS?=-I/usr/local/include2      <----- wie "+="
CFLAGS?=-I/usr/local/include       <----- wirkungslos

Sowohl "+=" and auch "?=" können nur auf bereits definierte Variablen angewendet werden. Auch das ist ein Schutzmechanismus, und zwar gegen versehentlich falsch geschriebene Variablennamen:

CFLAGS=-g
...
CPLAGS+=-O       <----- FEHLER!

..2 Systemvariablen

Eine Reihe von Variablen sind bereits durch Yabu vordefiniert. Diese Systemvariablen, deren Namen mit "_" beginnen, können im Buildfile nicht verändert werden. Ihr Wert ist mit Ausnahme von $(_CONFIGURATION) während eines Programmlaufs konstant.

Die folgende Tabelle enthält alle Systemvariablen:

_CWD

Aktuelles Verzeichnis, in dem Yabu gestartet wurde.

_CONFIGURATION

Die aktuelle Konfiguration (siehe Konfigurationen).

_LOCAL_CFG

Die lokale Konfiguration (siehe Auswahl des Servers per Konfiguration).

_HOSTNAME

Hostname (ohne Domain), auf dem Yabu gestartet wurde.

_MACHINE

Name der Plattform (uname -m), auf der Yabu gestartet wurde.

_PID

Die Prozeß-Id des Yabu-Prozesses als Dezimalzahl.

_RELEASE

Betriebsystemversion (uname -r), unter der Yabu gestartet wurde.

_SYSTEM

Betriebssystem (uname -s), unter dem Yabu gestartet wurde.

_TIMESTAMP

Die Startzeit von Yabu (GMT) im Format YYYY-MM-TT-HH-MM-SS.

Bei der Ausführung eines Skriptes exportiert Yabu die Systemvariablen in das Environment, wobei den Variablennamen der Präfix YABU vorangestellt wird. Der Wert von $(_TIMESTAMP) wird zum Beispiel als YABU_TIMESTAMP exportiert.

..3 Verwenden von Variablen

Eine Variable wird im einfachsten Fall mit der Syntax "$(NAME)" referenziert. Beispiel:

SOURCES=src1.c src2.c
prog: $(SOURCES)
    gcc -o prog $(SOURCES)

Im obigen Beispiel erfolgt die Wertzuweisung vor der Verwendung der Variablen. Tatsächlich spielt die Reihenfolge aber keine Rolle! Das liegt daran, daß Wertzuweisungen in Phase 1 ausgeführt werden, die Ersetzung von Referenzen aber erst in Phase 2. Das heißt, bevor es zu der ersten Ersetzung von "$(NAME)" kommt, hat Yabu alle Zuweisungen im Buildfile bereits ausgeführt. Das obige Beispiel könnte man also auch in der Form

prog: $(SOURCES)
    gcc -o prog $(SOURCES)
SOURCES=src1.c src2.c

oder

SOURCES=src1.c
prog: $(SOURCES)
    gcc -o prog $(SOURCES)
SOURCES+=src1.c

schreiben – das Ergebnis wäre immer das gleiche. Eine Variable hat also überall im Buildfile den gleichen Wert. Das gilt allerdings nur solange keine konfigurationsabhängigen Variablen (siehe Dynamische Variablen) verwendet werden. Bei letzteren kann sogar das Ergebnis derselben Variablenreferenz je nach Kontext, in dem sie ausgewertet wird, verschieden ausfallen.

Rekursive Verwendung von Variablen

Ein Variablenwert kann selbst wieder eine Referenz auf andere Variablen enthalten. Eine solche rekursive Verwendung ist in beliebiger Tiefe erlaubt, solange dadurch keine Schleifen entstehen. Auch hier gilt die obige Regel, daß alle Zuweisungen bereits ausgeführt sind, bevor Referenzen ersetzt werden. Beispielsweise hat nach

VAR1=eins $(VAR2)
VAR2=zwei $(VAR3)
VAR3=drei

die Variable VAR1 den Wert "eins zwei drei".

Sogar der Name einer Variablen kann Referenzen auf Variablen enthalten. Auf diese Weise kann man Variablen "indirekt" referenzieren. Beispiel

OPTS_Linux=-DLINUX
OPTS_FreeBSD=-DBSD
CFLAGS=$(OPTS_$(_SYSTEM))

Hier wird die vordefinierte Variable _SYSTEM benutzt, um einen Variablennamen zu konstruieren. Auf einem Linux-System würde die obigen Zuweisungen CFLAGS=-DLINUX ergeben.

Verboten ist dagegen die Verwendung von Variablen auf der linken Seite einer Zuweisung:

VARNAME=CC
$(VARNAME)=gcc    <----- FEHLER

Undefinierte Variablen

Für jede verwendete Variable muß es mindestens eine Zuweisung geben. Eine undefinierte Variable behandelt Yabu nicht als leeren Text, sondern bricht mit einer Fehlermeldung ab. Allerdings darf die Zuweisung im Buildfile an beliebiger Stelle stehen, auch erst nach der Verwendung der Variablen. Da Yabu zunächst die gesamte Datei liest und alle Zuweisungen ausführt, spielt die Reihenfolge keine Rolle.

Daß Yabu undefinierte Variablen als Fehler betrachtet, hat einen einfachen Grund: eine leere Zuweisung ist bei Bedarf schnell hingeschrieben, aber es ist nicht so einfach festzustellen, ob eine Variable versehentlich undefiniert ist. Das kann durch einen einfachen Schreibfehler passieren:

VERSIONTAG=date_compiled[]
ALL_SRC=date.c main.c utils.c ...
vtags: $(ALL_SRC)
        grep '$(VERSION_TAG)' $(ALL_SRC) >vtags

Das Ziel war hier offenbar, Versionsinformationen aus allen Quelldateien zu extrahieren. Wegen des falsch geschriebenen Variablennamens würde allerdings das Kommando

grep  date.c main.c utils.c ...  > vtags

ausgeführt. Dummerweise ist "date.c" als regulärer Ausdruck so ähnlich zu dem eigentlich gedachten "date_compiled[]", daß der Fehler womöglich lange Zeit unbemerkt bleibt.

..4 Transformationsregeln

Mit einer Transformationsregel läßt sich der Wert einer Variablen umformen. Transformationsregeln werden oft benutzt, um Dateinamen umzuschreiben. Zum Beispiel wird in

OBJS=eins.o zwei.o drei.o
SRCS=($OBJS:*.o=*.c)        <---- eins.c zwei.c drei.c

die Endung ".o" durch ".c" ersetzt. Wie aus dem Beispiel ersichtlich ist, wird die Transformation, wenn die Variable eine Liste von Elementen enthält, auf jedes Listenelement einzeln angewendet. Das Platzhalterzeichen "*" steht für eine (möglicherweise leere) Folge beliebiger Zeichen mit Ausnahme von "/", "." und Leerzeichen.

Paßt der Wert der Variablen nicht zur linken Seite der Transformation, dann tritt ein Fehler auf. Das würde zum Beispiel in

FILES=aaa bbb ccc.x
OBJS=$(FILES:*=*.o)      <---- Fehler

passieren, denn "ccc.x" paßt nach dem oben Gesagten nicht zum Muster "*". Um in solchen Fällen einen Fehler zu vermeiden, benutzt man eine der beiden folgenden Varianten:

FILES=aaa bbb ccc.x
OBJS=$(FILES?:*=*.o)     <---- aaa.o bbb.o ccc.x
OBJS=$(FILES!:*=*.o)     <---- aaa.o bbb.o

Der Unterschied zwischen den beiden Formen besteht in der Behandlung von Werten, die nicht zum Muster passen: bei "?:" bleiben sie unverändert erhalten, bei "!:" werden sie unterdrückt.

Ein Spezialfall der "!:"-Form liegt vor, wenn die linke und rechte Seite der Transformation identisch sind. In diesem Fall darf man die rechte Seite (einschließlich des "="!) weglassen. Die Transformation arbeitet dann als Filter, der nur die passenden Werte aus einer Liste durchläßt. Ein Beispiel: statt

SRCS=eins.c zwei.c drei.asm vier.c
C_SRCS=$(SRCS!:*.c=*.c)

kann man kurz

SRCS=eins.c zwei.c drei.asm vier.c
C_SRCS=$(SRCS!:*.c)

schreiben.

Umgekehrt kann man mit einer Transformation auch gezielt Elemente aus der Liste entfernen, indem man sie durch ein leeres Element ersetzt. Das ist natürlich nur in Verbindung mit der '?'-Variante sinnvoll. Das obige Beispiel könnte man also auch so schreiben:

SRCS=eins.c zwei.c drei.asm vier.c
C_SRCS=$(SRCS?:*.asm=)

Die Zeichenfolge "**" auf der rechten Seite wird in ein einfaches "*" ohne Platzhalterfunktionen übersetzt. Zum Beispiel ergibt

FILES=aaa bbb ccc
OBJS=$(FILES:*=*.**)

für OBJS den Wert "aaa.* bbb.* ccc.*". Stehen mehr als zwei "*" hintereinander, dann faßt Yabu zunächst so viele Paare wie möglich zu einen "*" zusammen, und bei einer ungeraden Anzahl zählt das letzte "*" als Platzhalter. Beispiel:

FILES=aaa bbb ccc
OBJS=$(FILES:*=***)

weist OBJS den Wert "*aaa *bbb *ccc" zu. Diese Einschränkungen lassen sich umgehen, wenn man ein alternatives Platzhalterzeichen benutzt (siehe unten).

Transformationen mit mehreren Platzhaltern

Komplexere Transformationsregeln lassen sich schreiben, wenn man mehrere Platzhalter benutzt:

FILES=dir1/A.c dir1/B.c dir2/C.c
NAMES=$(FILES:*/*.c=*2)

Danach hat NAMES den Wert "A B C". Im linken Teil der Transformation können bis zu 9 Platzhalter vorkommen. Sie werden der Reihe nach durchnumeriert und auf der rechten Seite mit '*1', '*2', ... '*9' referenziert. Gibt es mehr als einen Plazuhalter, dann ist die Verwendung eines einzelnen "*" rechts vom "=" nicht erlaubt und führt zu einem Syntaxfehler. Statt

OBJS=111.o 222.o 333.a
$(OBJS:*.*=*)    <----- FEHLER

muß man also

OBJS=111.o 222.o 333.a
$(OBJS:*.*=*1)

schreiben.

Ein Platzhalter kann sogar auf der linken Seite der Transformation referenziert werden, um sich wiederholende Teilstrings zu beschreiben. Beispiel:

FILES=dbg/aaa_dbg.o dbg/bbb_dbg.o rel/aaa_rel.o
NAMES=$(FILES:*/*_*1.c=*2)

Das Ergebnis wäre in diesem Fall "NAMES=aaa bbb aaa".

Enthält ein Muster mehr als einen Platzhalter, dann kann in manchen Fällen die Zuordnung von Teilstrings zu Platzhaltern mehrdeutig sein. In diesen Fällen gilt die Regel, daß von links nach rechts jedem Platzhalter der längstmögliche Teilstring zugeordnet wird, gegebenenfalls auf Kosten der weiter rechts stehenden Patzhalter. Im folgenden Beispiel

INPUT=aaa-bbb-ccc
OUTPUT=$(INPUT:*-*=*1/*2)

würde also der erste Platzhalter durch aaa-bbb ersetzt, so daß das Ergebnis

OUTPUT=aaa-bbb/ccc

ist und nicht etwa aaa/bbb-ccc.

"*" und "**"

Wie oben bereits erwähnt, steht ein *-Platzhalter für eine beliebige Zeichenfolge mit Ausnahme von "." und "/". Diese Einschränkung ist in vielen Fällen sinnvoll, denn typischerweise steht ein Platzhalter für den Hauptteil eines Dateinamens (d.h. den Teil ohne Pfad und ohne Erweiterung). Sie läßt sich umgehen, indem man statt "*" den Platzhalter '**' benutzt. Letzterer steht für eine beliebige Zeichenfolge, die auch "/" und "." enthalten kann:

A=file.tmp.c
B=$(A:*.*=*.x)       FEHLER
C=$(A:**.*=*.x)      C=file.tmp.x

Verschachtelte Referenzen und Transformationsregeln

Die rechte Seite einer Transformationsregel darf wiederum Variablen enthalten. Allerdings sind dabei keine Transformationsregeln mehr erlaubt. Die Ersetzung von '*' findet nämlich nur einmal statt und erfolgt ohne Berücksichtigung der Verschachtelungstiefe (siehe aber "Alternative Platzhalterzeichen" weiter unten!).

Das folgende Beispiel zeigt das Prinzip:

DIR=/usr/local
FILES=a b c
install: $(FILES:*=$(DIR)/*)

Bei solchen einfachen Referenzen erfolgt die Ersetzung von innen nach außen. Die letzte Zeile wird also schließlich zu

install: /usr/local/a /usr/local/b /usr/local/c

Ein komplexerer Fall liegt vor, wenn der Name einer "inneren" Variable einen '*'-Platzhalter enthält. Hierbei gilt, daß zunächst die '*'-Ersetzung stattfindet, und danach werden die Variablen von innen nach außen rekursiv ersetzt. Auch hierzu ein Beispiel:

LD_curses=-lcurses -ltermcap
LD_sql=-lsqlite3
LIBS=sql curses
app: app.c
    cc -o app app.c $(LIBS:*=$(LD_*))

Wie oben erläutert, wird in der letzten Zeile zuerst die '*'-Ersetzung ausgeführt. Das liefert

    cc -o app app.c $(LD_sql) $(LD_curses)

und schließlich

    cc -o app app.c -lsqlite3 -lcurses -ltermcap

Alternative Platzhalterzeichen

Statt des "*" kann man auch eines der folgenden 6 Platzhalterzeichen verwenden:

 @ & ^ ~ + #

Die Syntax hierfür ist: $(NAMEx:LS=RS), wobei x das Platzhalterzeichen und LS=RS die Transformationsregel ist. Die Wahl eines alternativen Platzhalters kann sinnvoll sein, wenn auf der rechten Seite der Transformationsregel ein '*' vorkommt. Beispiel:

FILES=aaa bbb ccc
OBJS=$(FILES&:&=&*)   <--- OBJS=aaa* bbb* ccc*

Ein anderer Anwendungsfall sind verschachtelte Referenzen mit Transformationsregeln. Beispiel:

PROGS=a b
CFGS=debug release
all:: $(PROGS:*=$(CFGS:&=*.&))

Dies ist äquivalent zu

all:: a.debug a.release b.debug b.release

Diese Reihenfolge entspricht der Vorstellung, daß die innere Variable der äußeren untergeordet ist. Um die umgekehrte Reihenfolge zu erzielen, müßte man

all:: $(CFGS:*=$(PROGS&=&.*))

schreiben und das Ergebnis wäre

all:: a.debug b.debug a.release b.release

Statt des ":" darf man in allen Fällen auch die oben beschriebenen alternativen Formen "?=" oder "!:" verwenden. Dabei ist die Reihenfolge der beiden Zeichen links vom ":" nicht relevant. Zum Beispiel sind die beiden Zeilen

OBJS=$(FILES&?:&=&*)
OBJS=$(FILES?&:&=&*)

äquivalent.

.11 Konfigurationen

» » » »

Kuz und knapp

Beschreibung

In der Praxis kommt es häufig vor, daß der gleiche Quellcode auf verschiedene Weise kompiliert wird. Ein Beispiel dafür ist eine separate Debug-Variante, die mit anderen Compilereinstellungen als die Standardvariante übersetzt wird. Oder ein Programm soll für unterschiedliche Rechnerarchitekturen oder Betriebssysteme erzeugt werden. Ein weiteres Beispiel sind optionale Programmteile, die je nach Anwendungsfall ein- oder ausgeblendet werden sollen.

Yabu enthält für solche Fälle einen speziellen Mechanismus, dessen zentrale Elemente die sogenannten Konfigurationen sind. Sie ermöglichen die einfache Beschreibung von Varianten ohne überflüssige Wiederholungen. Im einzelnen funktioniert der Konfigurationsmechanismus wie folgt:

Im Buildfile werden Konfigurationen teilweise explizit festgelegt. Zum Beispiel könnte eine Vorschrift lauten, Dateien im Verzeichnis "obj/debug" immer in der Debug-Konfiguration zu kompilieren. Des weiteren können Regeln (siehe Regeln) so definiert sein, daß sie nur in einer bestimmten Konfiguration anwendbar sind. Schließlich kann der Benutzer beim Aufruf von Yabu explizit eine Konfiguration vorgeben (zum Beispiel "nur Debug-Version erzeugen").

..1 Optionen

Optionen sind gewissermaßen das "Atom", also der kleinste konfigurierbare Aspekt des Projektes. Eine Option hat drei mögliche Zustände: undefiniert, eingeschaltet (+), oder ausgeschaltet (-). Alle Optionen sind anfänglich undefiniert.

Darüber hinaus kann man mehrere Optionen zu einer Gruppe zusammenfassen. Für die Optionen in einer Gruppe gilt eine zusätzliche Einschränkung: höchstens eine Option darf eingeschaltet sein, und wenn das der Fall ist, müssen alle anderen Optionen ausgeschaltet sein. Mit anderen Worten, für eine Optionsgruppe sind zwei Zustände denkbar:

Jede Optionen kann höchstens einer Gruppe angehören.

Für die Namen von Optionen gelten die gleichen Regeln wie für Präprozessor-Variablen (siehe Präprozessor-Variablen). Alle verwendeten Optionen müssen im Buildfile explizit definiert sein; es gibt keine implizite "Definition durch Zuweisung" wie bei Variablen. Die Definition erfolgt durch die !options-Anweisung, auf die eine beliebig lange Liste von (eingerückten) Optionsdefinitionen folgt:

!options
   Definition
   Definition
   ...

Jede Definition besteht aus einzelnen, durch Leerzeichen getrennten Optionsnamen sowie Gruppen von Optionen on der Form "GRUPPE(OPT1 OPT2 ...)". Für den Gruppennamen gelten die gleichen Einschränkungen wie für Optionsnamen. Das folgende Beispiel definiert zwei einzelne sowie eine Gruppe von 5 Optionen:

!options
    debug
    with_ssl
    platform(linux win32 netbsd solaris aix)

Man beachte: in diesem Beispiel wird keine Option namens "platform" definiert; dies ist lediglich der Name der Gruppe.

Jede Option darf man einmal definieren, und der Namen einer Optionsgruppe kann nicht zugleich als Optionnsname benutzt werden:

!options
   debug
   debug                       <---- FEHLER: debug bereits definiert
   platform(standard debug)    <---- FEHLER: debug bereits definiert
   os(linux os)                <---- FEHLER: os mehrfach benutzt
   debug(none std extra)       <---- FEHLER: debug bereits definiert

Um Platz zu sparen, darf man auch mehrere Optionen in einer Zeile definieren:

!options
    debug with_ssl platform(linux win32 netbsd solaris aix)

Eine Gruppe von Optionen muß immer vollständig in einer Zeile stehen. Bei Bedarf kann man Fortsetzungszeilen verwenden, um diese Einschränkung zu umgehen:

!options
    platform(linux win32\
             netbsd solaris aix)

..2 Konfigurationen

Eine Konfiguration in Yabu ist ein Satz von Werten für alle Optionen. Man schreibt sie als Liste von Optionen mit vorangestelltem "+" bzw. "-", die undefinierten Optionen läßt man weg. Zum Beispiel bedeutet

+linux +sqlite -ldap

daß die Optionen "linux" und "sqlite" eingeschaltet, und "ldap" ausgeschaltet ist. Das "+" kann man auch weglassen, d.h.

linux sqlite -ldap

wäre äquivalent zur obigen Konfiguration.

Im Falle von Optionsgruppen gilt implizit die Regel aus dem vorigen Abschnitt: ist ein Mitglied einer Gruppe eingeschaltet, dann sind alle übrigen Mitglieder automatisch ausgeschaltet, auch wenn sie nicht explizit mit vorangestelltem "-" aufgeführt werden. Betrachten wir erneut das Beispiel von oben mit den Optionen

!options
    platform(linux win32 netbsd solaris aix)

dann sind also alle folgenden Konfigurationen identisch:

linux
linux-win32-netbsd-solaris-aix
linux-aix-win32

Darüber hinaus gilt natürlich, daß eine Option nicht zugleich ein- und ausgeschaltet sein darf, und daß maximal eine Option innerhalb einer Gruppe eingeschaltet sein kann:

+linux-linux     <--- FEHLER!
+linux+aix       <--- FEHLER!

Die Reihenfolge der Optionen in einer Konfiguration ist nicht relevant.

Ordnung von Konfigurationen

Man sagt, die "Konfiguration A ist in der Konfiguration B enthalten" (kurz: A⊆B), wenn alle in A ein- oder ausgeschalteten Option in B den gleichen Wert haben. A⊆B bedeutet also, daß man A in B überführen kann, indem man undefinierte Optionen ein- oder ausschaltet. Wie die Schreibweise bereits suggeriert, ist dadurch eine Ordnung definiert, d.h. es gilt

  1. Aus A⊆B und B⊆A folgt A=B

  2. A⊆B und B⊆C impliziert A⊆C

  3. Für jede Konfiguration A gilt A⊆A

Kompatibilität von Konfigurationen

Zwei Konfigurationen A und B heißen kompatibel, wenn sie keine widersprüchlichen Optionswerte enthalten. Mit anderen Worten: jede Option, die in A ein- oder ausgeschaltet ist, hat in B entweder den gleichen Wert oder sie ist undefiniert (Wie man sich leicht überlegt, ist diese Bedingung symmetrisch in A und B).

Von zwei kompatiblen Konfigurationen ist im allgemeinen keine in der anderen enthalten. Kompatible Konfigurationen lassen sich aber zusammenführen. Das Ergebnis ist eine neue Konfiguration, die beide Ausgangskonfigurationen enthält (genauer gesagt, es ist die kleinste Konfiguration mit dieser Eigenschaft). Hierzu einige Beispiele. Folgende Optionen seien definiert:

!options
    debug with_ssl
    platform(linux win32 netbsd openbsd aix)

Die folgende Tabelle enthält einige Paare von kompatiblen und nicht kompatiblen Konfigurationen sowie (falls möglich) das Ergebnis der Zusammenführung:

A

B

Ergebnis

+linux

+linux

+linux

-debug

linux-debug

+linux+debug

+debug

+linux+debug

-debug

+linux

+linux-debug

+linux

-aix

+linux (impliziert -aix)

+linux

+aix

nicht kompatibel (+aix würde -linux implizieren)

+linux+debug

+openssl-debug

nicht kompatibel (debug ist bereits eingeschaltet und soll ausgeschaltet werden)

netbsd-debug

openssl+debug

nicht kompatibel (debug ist bereits ausgeschaltet und soll eingeschaltet werden).

..3 Dynamische Variablen

Wie bereits oben beschrieben, können Variablen konfigurationsabhängig sein. Das heißt, $(NAME) hat je nach aktueller Konfiguration einen anderen Wert. Solche Variablen werden im folgenden dynamische Variablen genannt, im Unterschied zu den nicht konfigurationsabhängigen statischen Variablen.

Um eine Variable dynamisch zu machen, benutzt man eine sogenannte bedingte Zuweisung. Sie hat eine der folgenden Formen:

NAME[KONFIG]=WERT
NAME[KONFIG]+=WERT
NAME[KONFIG]?=WERT

Zwischen dem Variablennamen und dem '[' sind keine Leerzeichen erlaubt. Eine bedingte Zuweisung arbeitet wie eine gewöhnliche Zuweisung, ist aber nur wirksam, wenn die in KONFIG angegebenen Optionswerte vorliegen (oder, mit anderen Worten, wenn KONFIG in der aktuellen Konfiguration enthalten ist). Ein Beispiel: die Zuweisung

CFLAGS[bsd-threads]=-DSINGLE

ist immer dann wirksam, wenn die Option "bsd" eingeschaltet und "threads" ausgeschaltet ist. Weitere Beispiele siehe unten.

Vor der ersten bedingten Zuweisung muß man der Variable einen Standardwert zuweisen. Der Standardwert gilt immer dann, wenn in der aktuellen Konfiguration keine der bedingten Zuweisungen wirksam ist. Vollständig müßte also das obige Beispiel lauten:

CFLAGS=-g
CFLAGS[bsd-threads]+=-DSINGLE

Der Standardwert kann leer sein, er muß aber immer vor der ersten bedingten Zuweisung stehen. Das hat den (beabsichtigten) Nebeneffekt, daß Yabu versehentlich falsch geschriebene Variablennamen erkennt und als Fehler meldet.

Eine zweite Form bedingter Zuweisungen besteht in einem !configuration-Abschnitt. Sie ist immer dann vorteilhaft, wenn mehrere bedingte Zuweisungen für die gleiche Konfiguration aufeinanderfolgen. Die allgemeine Form ist

!configuration [KONFIG]
    ZUWEISUNG
    ...

Alle auf die !configuration-Zeile folgenden eingerückten Zuweisungen gelten implizit für die angegebene Konfiguration. Zum Beispiel ist

!configuration [linux]
   CC=/usr/bin/gcc
   CFLAGS=-DOS_LINUX

äquivalent zu

CC[linux]=/usr/bin/gcc
CFLAGS[linux]=-DOS_LINUX

Auch hierbei gilt, daß alle dynamischen Variablen zuvor mit normalen Zuweisung "deklariert" werden müssen.

In komplexeren Fällen kann man sogar beide Methoden kombinieren. Yabu hängt die Konfiguration aus der !configuration-Zeile und die explizit angegebene Konfiguration einfach aneinander. Das Ergebnis muß natürlich wieder eine gültige Konfiguration darstellen. Beispiel:

!options
    system(linux openbsd netbsd) debug
!configuration [linux]
   CC=/usr/bin/gcc
   LFLAGS[debug]=-ldbmalloc
   CFLAGS[openbsd+debug]=-O     <---- FEHLER: +linux+openbsd nicht erlaubt

die zweite Zuweisung in diesem Beispiel ist äquivalent zu

LFLAGS[linux+debug]=-ldbmalloc

Je nach aktueller Konfiguration können für eine Variable keine, eine oder mehrere bedingte Zuweisungen wirksam sein. Darunter darf jedoch höchstens eine =-Zuweisung sein, alle anderen müssen vom Typ += oder ?= sein. Gibt es eine bedingte =-Zuweisung, dann ersetzt diese den Standardwert der Variablen. Bedingte +=- und ?=-Zuweisungen werden danach ausgeführt, und zwar in der Reihenfolge, in der sie im Buildfile auftreten.

Ein kurzes Beispiel:

!options
    os(linux bsd win32)
    old c99 debug

CFLAGS=-ansi
CFLAGS[old]=-traditional
CFLAGS[c99]=-std=c99
CFLAGS[debug]+=-g
CFLAGS[-win32]+=-O
CFLAGS+=-Wall
CFLAGS+=-ansi

Die folgende Tabelle enthält einige mögliche Konfigurationen und die zugehörigen Werte von CFLAGS

Konfiguration

$(CFLAGS)

Bemerkungen

[]

-ansi -Wall

[debug]

-Wall -ansi -g

[linux+debug]

-Wall -ansi -g -O

+linux impliziert -win32

[old]

-traditional

Die bedingte Zuweisung mit "=" überschreibt alle unbedingten Zuweisungen, also auch alle folgenden Zuweisung mit "+=".

[old+c99]

FEHLER

2 Zuweisungen mit "="

Spezielle Variablen

Yabu definiert im Zusammenhang mit Konfigurationen automatisch gewisse Variablen. Sie sind Spezialfälle der oben beschriebenen konfigurationsabhängigen Variablen, können aber nicht verändert werden.

Die Systemvariable $(_CONFIGURATION) liefert immer die gerade aktive Konfiguration. Der Wert enthält alle festgelegten Option in der Reihenfolge ihres Auftretens im !options-Abschnitt mit vorangestelltem "+" bzw. "-" und ohne weitere Trennzeichen. Ist ein Mitglied einer Optionsgruppe eingeschaltet, dann wird nur dieses als "+" aufgeführt. Andernfalls werden alle ausgeschalteten Mitglieder mit "-" aufgeführt. Ein möglicher Wert wäre zum Beispiel "+linux+with_ssl-gui"

Zu jeder Option gibt es eine zugehörige Systemvariable, deren Name aus dem Optionsnamen und einem führenden "_" gebildet wird. Der Wert dieser Variablen ist, je nach Wert der Option, gleich "+", "-" oder "". Zum Beispiel läßt sich der Wert der Option "debug" als $(_debug) ermitteln. Die Variable $(_opt) ist für alle Optionen definiert, also für einzelne Optionen und für Optionen, die einer Gruppe angehören. Zusätzlich gibt es zu geder Optionsgruppe eine zugehöige Variable, deren Wert den Namen der eingeschalteten Option oder "" ist. Beispiel:

!options
   os(windows unix)

Hier hätte die Variable $(_os) in der Konfiguration [windows] den Wert "windows", in der Konfiguration [unix] den Wert "unix", und in allen anderen Konfigurationen ([], [-windows], [-unix], [-windows-unix]) wäre der Wert leer.

..4 Auswahl der Konfiguration

Die aktive Konfiguration setzt sich aus zwei Teilen zusammen: eine Startkonfiguration, die während des gesamten Programmlaufs konstant bleibt, und ein dynamischer Anteil, der für jedes Ziel unterschiedlich sein kann. Allerdings kann der dynamische Anteil die Startkonfiguration nicht modifizieren: Optionen, die in der Startkonfiguration festgelegt sind, können nicht für einzelne Ziele einen anderen Wert bekommen oder undefiniert sein. Ist zum Beispiel die Startkonfiguration "+linux+debug", dann kann man für ein Ziel die Konfiguration "+linux+debug+mysql-gui" festlegen, nicht aber "+linux-debug".

Ein Beispiel für die Verwendung der Startkonfiguration ist die Auswahl des Betriebssystems und der Hardwarearchitektur. Es gibt mehrere Wege, die Startkonfiguration auszuwählen; sie sind im Folgenden beschrieben. Wie man für einzelne Ziele eine indivduelle Konfiguration auswählt, ist in Konfigurationsauswahl in Regeln und Konfigurationsauswahl mit !configure erläutert. Bemerkungen zu Konfigurationsauswahl in einem Rechnerverbund finden sich in Auswahl des Servers per Konfiguration.

Auswahl über die Kommandozeile

Mit Hilfe der Option -c kann man beim Aufruf von Yabu eine Konfiguration vorgeben. Beispiel:

yabu -c debug+mt-ssl all

Enthält das Buildfile !configure-Anweisungen (siehe unten), dann faßt Yabu beide Konfigurationen zusammen. Geht das nicht, weil sie für eine Option widersprüchliche Werte enthalten, dann bricht Yabu mit einer Fehlermeldung ab.

Auswahl mit einer !configure-Anweisung

Um im Buildfile eine Startkonfiguration explizit festzulegen, benutzt man eine !configure mit folgender Syntax:

!configure  Selektor
    Muster1: Konfiguration1
    Muster2: Konfiguration2
    ...

Vor Beginn der Phase 2 ersetzt Yabu alle in Selektor auftretenden Variablen und vergleicht das Ergebnis der Reihe nach mit den aufgeführten Mustern. In Muster sind keine Variablen erlaubt, man kann aber %-Platzhalter verwenden, die wie in Regeln funktionieren. Die erste passende Zeile bestimmt die Startkonfiguration; paßt keines der Muster, bricht Yabu mit einer Fehlermeldung ab. Beispiel:

!options
    platform(linux bsd solaris)
!configure $(_SYSTEM)
    Linux: linux
    %BSD: bsd
    SunOS: solaris

Ein Buildfile kann beliebig viele !configure-Anweisungen enthalten. Sie werden der Reihe nach ausgeführt und die dabei ausgewählten Konfigurationen zusammengeführt. Geht das nicht, weil zwei Anweisungen widersprüchliche Optionswerte liefern, bricht Yabu mit einer Fehlermeldung ab.

Autokonfigurationsskript

Eine zweite Form der !configure-Anweisung erlaubt die Ausführung beliebiger Shell-Kommandos, um die Startkonfiguration zu ermitteln. Steht kein Argument hinter !configure, dann bilden Folgezeilen ein Skript. Zu Beginn von Phase 2 ersetzt Yabu alle Variablen im Skript, führt es aus und interpretiert die Standardausgabe als Startkonfiguration. Die Ausgabe des Skriptes muß also eine Folge von Optionen mit optionalem vorangestellten "+" oder "-" sein. Optionen können durch beliebig viele Leerzeichen, TABs oder Zeilenwechsel (LF, CR) getrennt sein. Endet das Skript mit einen Exit-Code ungleich 0, dann bricht Yabu die Verarbeitung mit einer Fehlermeldung ab.

Ein einfaches Beispiel:

!configure
   case `uname -s` in
      Linux) echo +linux+gcc
             pgrep -x udevd >/dev/null && echo +udev
             ;;
      SunOS) echo +solaris ;;
   esac

Dieses Skript würde je nach Umgebung, in der es läuft, eine der folgenden Konfigurationen auswählen:

(leer)
+linux+gcc
+linux+gcc+udev
+solaris

Auch hier gilt, daß ein Buildfile beliebig viele !configure-Anweisungen enthalten kann.

.12 Regeln

» » » » » » » »

Kurz und knapp

Beschreibung

Regeln sind die zentralen Elemente in jedem Buildfile. Sie beschreiben die Abhängigkeit der Zeiel von ihren Quellen und enthalten die Vorschriften (Skripte) zur Erreichung der Ziele. Eine Regel besteht aus drei Hauptteilen, von denen aber nur der erste zwingend erforderlich ist:

..1 Regeln mit mehreren Zielen

Yabu behandelt eine Regel mit 2 oder mehr Zielen, was die Beziehungen zwischen Quellen und Zielen betrifft, wie eine Abkürzung für mehrere gleichartige Regeln, die sich nur im Ziel unterscheiden. Zum Beispiel wirkt

prog1 prog2: prog
    cp $(1) $(0)

wie

prog1: prog
    cp $(1) $(0)
prog2: prog
    cp $(1) $(0)

In beiden Fällen sind "prog1" und "prog2" abhängig von "prog" und werden durch Kopieren aktualisiert.

Ein Unterschied ist allerdings, daß prog1 und prog2 im oberen Beispiel eine sogenannte Gruppe von Zielen bilden. Das bedeutet unter anderem, daß Yabu niemals versucht, prog1 und prog2 gleichzeitig zu erreichen. Details hierzu finden sich in Gruppierung und Reihenfolge von Zielen.

..2 Mehrere Regeln für ein Ziel: Regelauswahl

Für eingegebenes Ziel enthält das Buildfile unter Umständen mehrere passende Regeln. "Passend" bedeutet, das Ziel stimmt – unter Berücksichtigung von %-Platzhaltern (siehe Prototyp-Regeln (%-Platzhalter)) – mit der linken Seite der Regel überein, und, falls die Regel eine Konfigurationsauswahl (siehe Konfigurationsauswahl in Regeln) enthält, die vorgeschriebene Konfiguration ist kompatibel zur aktuellen Konfiguration.

Für jedes zu erreichende Ziel durchläuft Yabu sämtliche Regeln in der Reihenfolge ihrer Definition im Buildfile und sucht die passenden heraus. "Passend" bedeutet, das Ziel steht – unter Berücksichtigung von %-Platzhaltern (siehe Prototyp-Regeln (%-Platzhalter)) – auf der linken Seite der Regel und, falls die Regel eine Konfigurationsauswahl (siehe Konfigurationsauswahl in Regeln) enthält, die vorgeschriebene Konfiguration ist kompatibel zur aktuellen Konfiguration. Das Ergebnis dieses Durchlaufs ist eine (oder keine) "ausgewählte" Regel sowie eine Liste von Quellen. Sie werden wie folgt bestimmt:

Die Ermittlung der Quellen sei an einem weiteren Beispiel erläutert.

%.o: %.c common.h                              <----- (1)
    $(CC) -o $(0) $(1)
db_init.o: db_init.c ../database/dbinit.h      <----- (2)
    $(CC) -DINIT -o $(0) $(1)
db_%.o: ../database/db.h                       <----- (3)

In der folgenden Tablle ist des Ergebnis der Regelanalyse für einige Ziele dargestellt.

Ziel

Quellen

Ausgewählte Regel

main.o

main.c common.h

(1)

db_config.o

dbconfig.c common.h ../database/db.h

(1)

db_init.o

db_init.c ../database/dbinit.h ../database/db.h

(2)

Verhinderung von Schleifen

Bei dem oben beschriebenen Prozeß gilt eine Einschränkung: ein Ziel darf niemals von sich selbst abhängen. Tritt eine solche Abhängigkeitsschleife auf, dann bricht Yabu die Programmausführung mit einer Fehlermeldung ab. Schleifen können sich über mehrere Regeln erstrecken und sind dann nur schwer erkennbar. Beispiel:

a: a1.o a2.o b3.o
   ...
%.o: %.c
   ...
b3.c: a
   ...

Eine zweite Einschränkung besteht darin, daß eine Regel niemals für die in ihr aufgeführten Quellen in Betracht gezogen wird. Damit verhindert Yabu einen einfachen Fall von endloser Rekursion:

%.c: %.hll template.c
   gen_impl -o $(0) $(*)

Wenn Yabu diese Regel anwendet – zum Beispiel für "abc.c" – wird "template.c" als Quelle ausgewählt. Bei der Suche nach einer passenden Regel für "template.c" wird dann diese Regel übergangen, denn sie würde zu einer endlosen Schleife führen. Unter der Annahme daß "template.c" bereits existiert, funktioniert das obige Beispiel also so, wie es der Entwickler offensichtlich beabsichtigt hat. (Wenn "template.c" an anderer Stelle als Quelle auftritt oder als Ziel auf der Kommandozeile als Ziel vorgegeben wird, würde allerdings ein Fehler auftreten).

..3 Alias-Ziele

Aliase sind Ziele, die keiner Datei entsprechen. Sie werden meist benutzt, um eine Gruppe von Zielen unter einem einzigen Namen zusammenzufassen. Zum Beispiel ermöglicht die Regel

progs: aaa bbb ccc ddd eee ffff

die Ziele "aaa",...,"fff" durch einem einfachen Aufruf, nämlich "yabu progs" zu aktualisieren. Findet Yabu für ein Ziel nur Regeln ohne Skript, dann gilt das Ziel als Alias. Die speziellen Ziele all, !INIT und !ALWAYS zählen ebenfalls immer als Alias.

Der wichtigste Unterschied zwischen Aliasen und gewöhnlichen Zielen ist, daß Yabu bei einem normalen Ziel prüft, ob eine gleichname Datei erzeugt oder aktualisiert wurde. Bei einem Alias unterbleibt diese Prüfung. Insbesondere gibt Yabu keine Fehlermeldung aus, wenn die Datei nicht existiert.

Existiert eine Regel mit Skript, dann betrachtet Yabu das Ziel als gewöhnliches Ziel (Ausnahme sind die oben genannten speziellen Ziele). Ergänzt man die obigen Regel um ein Kommando, zum Beispiel

progs: aaa bbb ccc ddd eee ffff     <----- FEHLER: kein Alias
   echo "Fertig"

dann ist progs kein Alias mehr. Yabu wird nun nach Ausführung des Kommandos prüfen, ob die Datei progs existiert und wahrscheinlich – weil das nicht der Fall ist – mit einer Fehlermeldung abbrechen. Deshalb muß man bei Regeln mit Skript explizit festlegen, daß es sich um einen Alias handelt. Das geschieht mit der "::"-Syntax, also

progs:: aaa bbb ccc ddd eee ffff
   echo "Fertig"

Ein zweiter Unterschied zwischen Aliasen und gewöhnlichen Zielen ist, daß Aliase immer als "veraltet" gelten. Bei gewöhnlichen Zielen trifft Yabu diese Entscheidung auf der Grundlage von Änderungszeiten oder Prüfsumme. Ein Alias ist aber per Definition keine Datei und wird deshalb immer als veraltet behandelt. Das bedeutet: wird ein Alias in Phase 2 ausgewählt (siehe ) und gibt es dafür eine Regel mit Skript, dann führt Yabu das Skript immer aus. Ein Beispiel:

all:: file1 file2
    echo "Alles fertig"
file%:  file%.c
    cc -o $(0) $(1)

Hierbei würde Yabu, falls nötig, die Programme file1 und file2 kompilieren und danach – selbst wenn beide Programm bereits aktuell waren – die Meldung "Alles fertig" ausgeben.

Nachdem Yabu ein Alias-Ziel erreicht hat, erhält es als "Änderungszeit" die aktuelle Zeit. Das gilt auch, wenn keine Regel mit Skript vorhanden ist. Alle von dem Alias abhängigen Ziele sind also immer veraltet.

Schließlich ist noch zu bemerken, daß Yabu Alias-Ziele nicht in der Statusdatei (siehe Die Statusdatei (Buildfile.state) speichert. Aliase gehen auch nicht in die Statistik ein, die Yabu am Ende ausgibt.

..4 Überschreiben verhindern mit ':?'

Nomalerweise wird bei der Ausführung einer Regel das Ziel neu erzeugt oder mit einer neuen Datei überschrieben. Manchmal ist das aber nicht erwünscht: die Regel soll die Zieldatei erzeugen, falls sie nicht existiert, aber eine bestehende Datei soll nicht überschrieben werden. Das folgende Beispiel zeigt einen solchen Fall:

VERSION=1.18
...
export:: released/yabu-$(VERSION).tar

released/yabu-$(VERSION).tar: $(DIST_FILES)
   tar cf $(0) $(*)

Hier soll das Ziel "export" eine zur Freigabe bestimmte tar-Datei erzeugen. Vergißt man jedoch, nach dem Exportieren die Versionsnummer hochzuzählen, dann würde mit dem nächsten "yabu export" die bestehende tar-Datei überschrieben, was sicherlich nicht gewollt ist.

Um das Überschreiben zu verhindern, schreibt man die Regel mit ":?", also

released/yabu-$(VERSION).tar:? $(DIST_FILES)
   tar cf $(0) $(*)

Yabu prüft nun vor der Ausführung des Skriptes, ob die tar-Datei bereits vorhanden ist. Falls ja, gibt Yabu eine Fehlermeldung aus, und das Ziel gilt als nicht erreicht. Dieser Mechanismus kommt natürlich nur dann zur Anwendung, wenn das Skript tatsächlich ausgeführt würde, d.h. wenn das Ziel veraltet ist. Ist das Ziel jüngeren Datums als die Quellen, dann verhält sich die ":?"-Regel wie eine normale Regel: das Skript wird nicht ausgeführt, und das Ziel gilt als erreicht.

Bei Alias-Regeln (siehe Alias-Ziele) und bei Regeln ohne Skript (siehe Regeln ohne Skript: sekundäre Quellen) kann die ':?'-Syntax nicht verwendet werden.

..5 Regeln ohne Skript: sekundäre Quellen

Oben wurden bereits Regeln ohne Skript im Zusammenhang mit Aliasen diskutiert. Ein zweiter Anwendungsfall, für Regeln ohne Skript sind indirekte Abhängigkeiten, die aus verschiedenen Gründen in einer Regel nicht explizit aufgeführt werden können. Ein typisches Beispiel hierfür sind Header-Dateien, die in einer Quelle mittels #include eingebunden werden. Normalerweise ist es nicht sinnvoll, solche versteckten oder sekundären Quellen explizit in der Regel aufzuführen. Das würde nämlich dazu führen, daß die sekundären Quellen in $(*) enthalten sind:

eins.o: eins.c incl1.h incl2.h
    cc $(CFLAGS) -o $(*) -c $(*)             <-- POTENTIELLER FEHLER

Je nach Inhalt der Include-Dateien und Compileroptionen würde das Skript in der obigen Regel eine Warnung oder einen Fehler produzieren, oder es funktioniert fehlerfrei.

Eine mögliche Lösung ist, die sekundären Quellen in einer separaten Regel ohne Skript zu notieren:

eins.o: eins.c
    cc $(CFLAGS) -o $(*) -c $(*)
eins.o: incl1.h incl2.h

Noch eleganter ist die Verwendung von auto-depend (siehe Autodepend-Skripte). Damit wird der Prozeß komplett automatisiert, und man muß sich nicht um die Pflege der sekundären Abhängigkeiten kümmern.

..6 Konfigurationsauswahl in Regeln

In einer Regel läßt sich eine Konfiguration auswählen; man schreibt sie in eckigen Klammern unmittelbar hinter dem ":" und vor der ersten Quelle. Eine solche Regel verhält sich etwas anders als eine Regel ohne Konfigurationsauswahl:

Ein Beispiel: die Regel

app: [with_ssl] app.o
   $(CC) -o $(0) $(*) $(LIBS)

fordert, die Option "with_ssl" einzuschalten. Wäre die letztere nun beispielsweise mit

yabu -c -with_ssl

explizit ausgeschaltet, dann stünde diese Regel gar nicht zur Verfügung. Sofern nicht eine weitere passende Regel existiert, würde Yabu beim Versuch, "app" zu erreichen, eine Fehlermeldung ausgeben.

Ist dagegen "with_ssl" undefiniert, dann kann Yabu die Regel benutzen. Dabei würde folgendes passieren:

  1. Yabu wendet den Inhalt der [...] auf die aktuelle Konfiguration an. Mit anderen Worten, die Option "with_ssl" wird eingeschaltet.

  2. Etwaige Variablen in der Quellenliste (kommt im Beispiel nicht vor) werden ersetzt. Dabei gilt die ausgewählte Konfiguration.

  3. Yabu versucht, die Quelle app.o zu erreichen. Hierbei gilt nicht die ausgewählte Konfiguration, d.h. "with_ssl" ist wieder undefiniert! Natürlich könnte die Regel für app.o die Option ebenfalls aktivieren.

  4. Yabu führt das Skript aus. Hierbei ist die ausgewählte Konfiguration aktiv. Das könnte zum Beispiel den Wert von $(LIBS) beeinflussen.

  5. Nach Anwendung der Regel stellt Yabu die alte Konfiguration wieder her. Hier würde also "with_ssl" von "eingeschaltet" auf "undefiniert" zurückgesetzt.

Konfigurationsauswahl und "%"

Bei einer Prototyp-Regel kann man die "%"-Platzhalter auch in der Konfigurationsauswahl verwenden. Auf diese Weise lassen sich ganz einfach Konfigurationen mit Unterverzeichnissen verknüpfen. Zum Beispiel würde mit

%/%.o: [%1] source/%2.c
   cc $(CFLAGS) -o $(0) -c $(1)

für das Ziel "debug/init.o" die Konfiguration "+debug" aktiviert, für "release/init.o" dagegen die Konfiguration "release".

Variablen in der Quellenliste

Wie oben erwähnt, gilt Konfigurationauswahl in einer Regel nicht nur für das Skript, sondern bereits wenn Yabu die Liste der Quellen auswertet. Hierzu ein Beispiel:

SOURCES=common.c
SOURCES[server] += server.c
SOURCES[client] += client.c

app.%: [%] $(SOURCES)
   cc $(CFLAGS) -o $(0) $(*)

all: app.server app.client app.other

Bei der Ersetzung von $(SOURCES) gilt bereits die ausgewählte Konfiguration, d.h. die erste Regel hätte für die drei Ziele die Form

app.server: [server] common.c server.c
app.client: [server] common.c client.c
app.oter:   [other]  common.c

Variablen in der Zielliste

Auch die linke Seite einer Regel kann Variablen enthalten. Bei deren Ersetzung gilt die Startkonfiguration und, falls zutreffend, die per !configure ausgewählte zielspezifische Konfiguration (siehe [ref.rules.cfgpertgt]), nicht aber die in der Regel selbst ausgewählte Konfiguration.

..7 Konfigurationsauswahl mit !configure

Eine weitere Möglichkeit, die Konfiguration für einzene Ziele festzulegen, ist die folgende, dritte Form der !configure-Anweisung.

!configure [//KONFIG//] //Muster// ...

Damit Yabu diese Form erkennt, muß das erste Argument mit '[' beginnen. Andernfalls interpretiert Yabu die Anweisung wie in Auswahl der Konfiguration beschrieben. Das Buildfile kann beliebig viele !configure-Anweisungen enthalten. Yabu vergleicht jedes neu auftretende Ziel der Reihe nach mit den aufgeführten Mustern, wobei %-Platzhalter wie üblich behandelt werden. Die erste passende !configure-Anweisung bestimmt zusammen mit der Startkonfiguration die Konfiguration für dieses Ziel. Anders als bei Regeln muß KONFIG mit der Startkonfiguration kompatibel sein; falls nicht, bricht Yabu mit einer Fehlermeldung ab.

Ein Beispiel: mit

!configure [+rt] build/sem%.o build/msg%.o

würde für "build/sem_base.o" oder "build/msg_read.o" die "+rt" eingeschaltet, für "build/other.o" dagegen nicht.

..8 Phase 2: Wie Yabu Ziele erreicht

» »

Durch den oben beschriebenen Prozeß der Regelauswahl ist noch nicht festgelegt, wie Yabu die ausgewählten Ziele erreicht. Auch das ist ein rekursiver Vorgang, der aus der wiederholten Ausführung eines einzelnen Schrittes besteht. Ein solcher Schritt erfolgt immer dann, wenn alle Quellen für ein Ziel in einem definierten Zustand (erreicht oder nicht erreichbar) sind. Er besteht aus 3 Teilschritten:

  1. Ist eine Quelle oder mehrere Quellen unerreichbar, wird das Ziel ebenfalls als unerreichbar markiert und fällt damit heraus.

  2. Sind alle Quellen erreicht (was auch dann zutrifft, wenn das Ziel gar keine Quellen hat), dann entscheidet Yabu, ob das Ziel veraltet ist. Dafür muß mindestens eins der folgenden Kriterien erfüllt sein:

    Ist das Ziel veraltet, dann führt Yabu das zugehörige Skript aus, sofern eins vorhanden ist. War das erfolgreich, gilt das Ziel als erreicht. Meldet das Skript einen Fehler (Exit-Code ungleich 0), dann markiert Yabu das Ziel als unerreichbar.

    Ist das Ziel nicht veraltet, gilt es sofort als erreicht.

  3. Wurde ein Ziel ereicht, dann führt Yabu das Autodepend-Skript aus, sofern die Regel eines enthält. Die Ausgabe des Autodepend-Skriptes hat keinen Einfluß auf den weiteren Ablauf. Sie geht jedoch beim nächsten Programmlauf in die Liste der Quellen ein (siehe oben).

...1 Vergleichsalgorithmen

Um Festzustellen, ob ein Ziel veraltet in Bezug auf seine Quellen ist, kann Yabu drei verschiedene Verfahren einsetzen.

Als Standard verwendet Yabu das erste, Make-kompatible Verfahren. Die beiden anderen können bei Bedarf mit der Option "-y" ausgewählt werden, siehe Kommandozeile und Environment. Yabu speichert den ausgewählten Algorithmus in der Statusdatei (siehe Die Statusdatei (Buildfile.state)) und benutzt ihn dann bei folgenden Aufrufen als Standard, solange man nicht mit mit -y einn anderes Verfahren wählt oder die Statusdatei löscht

Die drei Verfahren lassen sich nicht kombinieren; während eines Programmlaufes ist immer ein Verfahren festgelegt. Ändert man den Algorithmus, dann wird die Zustandsdatei ungültig, was unter Umständen dazu führt, daß Yabu sämtliche Ziele als veraltet betrachtet und aktualisiert.

-y mt: Änderungszeiten

Dieses Verfahren arbeitet mit den Änderungszeiten der Dateien. Eine Datei wird als veraltet betrachtet, wenn ihre Änderungszeit kleiner als das Maximum der Änderungszeiten aller Quelldateien ist. Dieses Verfahren ist als einziges nicht auf eine Speicherung von Statusinformationen in Buildfile.state angewiesen, da die Änderungszeiten vom Betriebssystem verwaltet werden. Ein Verlust der Statusdatei führt deshalb nicht dazu, daß Yabu beim nächsten Aufruf alle Ziele erneut aktualisiert. Tatsächlich kann man die Verwendung der Statusdatei in diesem Modus unterbinden (Option -s).

Das Verfahren hat aber auch Nachteile. Änderungszeiten sind unter Umständen (je nach Betriebsystem und Dateisystem) nur sekundengenau. Das bedeutet, daß Yabu Änderungen der Quellen nicht bemerkt, wenn diese schnell aufeinander folgen. Bei manuell veränderten Dateien wie zum Beispiel C-Quelltexten ist das praktisch nicht von Bedeutung, bei automatisch generierten Dateien kann es dagegen wichtig werden.

Ein zweites Problem tritt auf, wenn sich Quellen und Ziel in verschiedenen Dateisystemen befinden. Ist zum Beispiel eines der Dateisysteme lokal und das andere ein ein NFS-Dateisystem, dann beziehen sich die Änderungszeiten auf verschiedene Uhren. Weichen die Uhren voneinander ab, dann arbeitet Yabu nicht mehr korrekt.

-y mtid: Änderungszeiten als Prüfsumme behandeln

Dieses Verfahren basiert ebenfalls auf Änderungszeiten, setzt aber nicht voraus, daß es sich um Zeiten handelt. Insbesondere werden niemals die Änderungszeiten zweier Dateien miteinander verglichen. Vielmehr behandelt Yabu die Änderungszeit als Identifikation des Dateiinhaltes – gewissermaßen wie eine Prüfsumme. Jedes Mal nach Ausführung einer Regel speichert Yabu die Änderungszeit aller Quellen sowie des Ziels in der Statusdatei (Buildfile.state). Bei folgenden Programmläufen prüft Yabu nur noch, ob sich die Änderungszeit einer der Quellen oder des Ziels verändert hat. Falls ja, gilt die Datei als veraltet und wird neu generiert.

Dieses Verfahren hat den Vorteil, daß es auf den ohnehin bekannten Änderungszeiten basiert und keine weiteren Informationen über die Dateien benötigt. Außerdem ist es nicht anfällig gegen Zeitdifferenzen zwischen verschiedenen Dateisystemen. Es setzt lediglich voraus, daß bei jeder Modifikation einer Datei eine neue Änderungszeit vergeben wird. Die oben erwähnten Beschränkungen durch die endliche Genauigkeit der Zeiten bleiben bestehen.

-y cksum: Prüfsummen

Bei diesem Verfahren ignoriert Yabu die Änderungszeiten komplett und berechnet stattdessen Prüfsummen des Dateiinhaltes. Yabu benutzt hierfür das gleiche Verfahren (CRC-32) wie das UNIX-Dienstprogramm "cksum". Ansonsten ist der Algorithmus der gleiche wie bei -y mtid: nach Anwendung einer Regel speichert Yabu die Prüfsummen aller Quellen und des Ziels. Hat beim nächsten Programmlauf eine der Quellen eine veränderte Prüfsumme, dann erzeugt Yabu die Zieldatei erneut.

Im Unterschied zu den anderen Verfahren muß Yabu hier nur dann ein Ziel neu generieren, wenn sich eine Quelldatei tatsächlich geändert hat. Deshalb ist die cksum-Variante besonders dann vorteilhaft, wenn Dateien zwar häufig automatisch generiert werden, der Inhalt aber meist gleich bleibt. Außerdem ist das Verfahren immun gegen die oben beschriebenen Probleme bei der Verwendung von Änderungszeiten.

Ein Nachteil des Prüfsummenverfahrens ist der höhere Rechenaufwand durch die Berechnung der Prüfsummen bei jedem Programmlauf.

...2 Ergebnisstatistik

Beim Programmende gibt Yabu eine Statistik über die ausgewählten und erreichten bzw. nicht erreichten Ziele aus. Genauer betrachtet unterscheidet Yabu für jedes Ziel vier mögliche Ergebnisse:

Unverändert

Das Ziel war bereits erreicht, d.h. es existierte bereits und war auch nicht veraltet in Bezug auf seine Quellen.

Erneuert

Das Ziel wurde erneut erreicht, das zugehörige Skript endete ohne Fehler.

Fehler

Das Ziel wurde nicht erreicht, weil bei der Skriptausführung ein Fehler auftrat. Dazu zählt auch der Fall, daß das Skript zwar fehlerfrei ausgeführt wurde, aber die erwartete Zieldatei nicht erzeugt wurde.

Ausgelassen

Das Ziel wurde nicht erreicht, und Yabu hat gar nicht erst versucht, das erforderliche Skript auszuführen. Das passiert beispielsweise, wenn eine der Quellen nicht erreicht wurde.

Im Erfolgsfall wurden alle Ziele erreicht oder waren bereits erreicht. Die Statistik sieht dann etwa so aus:

21 Ziele: 13 unverändert, 8 erneuert in 0s

Im Fehlerfalle kommen die Anzahlen der nicht erreichten Ziele hinzu. Damit die Ausgabe nicht zu unübersichtlich wird, entfällt die Angabe der unveränderten und Gesamtzahl der Ziele:

*** NICHT ERFOLGREICH: 0 erneuert, 7 Fehler, 191 ausgelassen

.13 Gruppierung und Reihenfolge von Zielen

» » »

..1 Implizite Zielgruppen

Manchmal kommt es vor, daß ein Skript mehrere Ziele in einem Schritt erzeugt. Zum Beispiel werden in

abc.h abc_xdr.c abc_svc.c abc_clnt.c: abc.x
    rpcgen abc.x

alle vier Ziele auf einmal generiert. Diese Eigenschaft wird in zwei Fällen wichtig, nämlich bei der parallelen Verarbeitung von Zielen und bei der Behandlung von Fehlern. Yabu faßt deshalb Ziele, die in einer Regel auftreten, automatisch zu einer Gruppe zusammen. Das bedeutet zum einen, daß Yabu das Skript niemals für zwei Ziele parallel ausführt, selbst wenn die aktuelle Konfiguration das zulassen würde (siehe Parallel und verteilt: Yabu im Netzwerk). Die zweite Konsequenz ist, daß ein Fehler bei einem der vier Ziele sich auf die übrigen Ziele überträgt. Wenn im obigen Beispiel rpcgen an der Erzeugung von abc.h scheitert, versucht Yabu nicht mehr, die übrigen Ziele abc_xdr.c, abc_svc.c und abc_clnt.c zu erreichen. Auch das ist sinnvoll, denn mit großer Wahrscheinlichkeit würde rpcgen jedesmal den gleichen Fehler liefern.

Die erfolgreiche Auführung eines Skriptes hat dagegen keine Auswirkung auf die anderen Ziele in der Gruppe, sie gelten nicht automatisch als erreicht. Insbesondere setzt Yabu nicht voraus, daß das Skript alle in der Regel genannten Ziele erzeugt. Zum Beispiel würde beim Aufruf

yabu abc.h abc_xdr.c

folgendes passieren (vorausgesetzt, abc.x ist jüngeren Datums als abc.h und abc_xdr.c):

  1. Yabu führt das Skript aus, weil abc.h bezüglich abc.x veraltet ist.

  2. Yabu vergleicht (erst jetzt!) die Änderungszeiten von abc_xdr.c und abc.x. Da im ersten Schritt auch eine neue Datei abc_xdr.c erzeugt wurde, führt Yabu das Skript nicht erneut aus.

Yabu versucht Ziele "intelligent" zu gruppieren, um einerseits unerwünschte Mehrfachausführung von Skripten zu vermeiden und andererseits die Parallelisierung der Skriptausführung nicht unnötig zu behindern. Das genaue Kriterium, nach dem Yabu Ziele zu Gruppen zusammenfaßt, ist deshalb ein wenig komplizierter als es das obige Beispiel vermuten läßt. Zwei Ziele gehören zur gleichen Gruppe, wenn die beiden folgenden Bedingungen erfüllt sind:

Die erste Bedingung besagt unter anderem, daß eine Regel ohne Skript niemals zu einer Gruppierung führt. Schreibt man das obige Beispiel also in der Form

abc.h: abc.x
    rpcgen abc.x
abc_xdr.c: abc.x
    rpcgen abc.x
abc_svc.c: abc.x
    rpcgen abc.x
abc_clnt.c: abc.x
    rpcgen abc.x
abc_xdr.c abc_svc.c abc_clnt.c: abc.x

dann führt die letzte Regel nicht zu einer Gruppierung. Yabu würde also alle vier Ziele als unabhängig voneinander behandeln und das rpcgen-Kommando unter Umständen parallel ausführen.

Die zweite Bedingung ist nur relevant, wenn %-Platzhalter im Spiel sind. In einem Projekt mit vielen RPC-basierten Modulen könnte man etwa folgende Regel finden:

%.h %_xdr.c %_svc.c %_clnt.c: %.x
    rpcgen %.x

Hierbei würden zum Beispiel 'abc.h' und 'abc_xdr.c' zu einer Gruppe gehören, 'xyz.h' und 'xyz_xdr.c' aber zu einer anderen Gruppe. Yabu würde also abc.x und xyz.x gegebenenfalls parallel verarbeiten. Ein noch einfacheres Beispiel: die Regel

%.o: %.c
    $(CC) -c $(CFLAGS) -o $(0) $(1)

bewirkt ebenfalls keine Gruppierung, da der Platzhaltertext für alle Ziele verschieden ist.

..2 Gruppierung mit !serialize

Mit Hilfe der !serialize-Anweisung kann man die oben beschriebene implizite Gruppierung von Zielen aufheben und selbst beliebige Gruppen definieren. Ziele in einer so definierten Gruppe bearbeitet Yabu immer nacheinander und niemals parallel. Anders als bei den oben beschriebenen impliziten Gruppen übertragen sich Fehler aber nicht auf die übrigen Mitglieder einer !serialize-Gruppe.

Die allgemeine Form der !serialize-Anweisung ist

!serialize Ziel Ziel ...
!serialize <Kennung> Ziel Ziel ...

wobei in Ziel auch %-Platzhalter erlaubt sind. Alle Ziele, die zu einem der aufgeführten Muster passen, werden Mitglied der Gruppe. Jede !serialize-Anweisung der ersten Form definiert eine eigene Gruppe. Bei der zweiten Form mit einer zusätzlichen, in "<...>" eingeschlossenen Kennung faßt Yabu alle !serialize-Anweisungen mit gleicher Kennung zu einer Gruppe zusammen. Das heißt,

!serialize <global> gen_%.o
...
!serialize <global> gen_%.c

ist gleichbedeutend mit

!serialize gen_%.o gen_%.c

Ein Ziel gehört niemals zu mehr als einer Gruppe. Gibt es für ein Ziel mehr als eine passende !serialize-Anweisungen, dann gilt immer die im Buildfile zuerst aufgeführte.

Die Reihenfolge der Ziele in einer !serialize-Anweisung hat keine Bedeutung. In welcher Reihenfolge Yabu versucht, Ziele zu erreichen, richtet sich primär nach der Reihenfolge, in der die Ziele im Buildfile bzw. auf der Kommandozeile auftreten und natürlich nach den Abhängigkeiten der Ziele untereinander. Führt Yabu Skripte parallel aus, dann hängt die Reihenfolge zusätzlich von der Laufzeit der Skripte ab und ist im allgemeinen nicht vorhersagbar. Siehe auch Die Reihenfolge von Quellen.

Ein Beispiel:

all:: all-single all-multi
all-%::
   (Kommandos)
!serialize all-%

Die beiden Ziele "all-single" und "all-multi" werden zwar mit derselben Regel erzeugt, gehören aber wegen des verschiedenen Wertes des %-Platzhalters trotzdem nicht zu einer Gruppe. Ohne die letzte Zeile würde Yabu deshalb beide Ziele parallel bearbeiten (sofern die Konfiguration das zuläßt). Erst die !serialize-Anweisung erzwingt, daß Yabu das Skript für "all-multi" nach dem Skript für "all-single" ausführt. Das ließe sich auch erreichen, indem man

all:: all-single all-multi
all-single all-multi::
   (Kommandos)

schreibt. Diese Variante ist aber weniger flexibel, denn man kann keine %-Platzhalter einsetzen.

Ein zweiter Unterschied tritt zutage, wenn das Skript für "all-single" einen Fehler verursacht. In der ersten Variante mit !serialize würde Yabu als nächstes versuchen, "all-multi" zu erreichen. In der zweiten Variante überträgt sich der Fehler auf alle Ziele in der Gruppe, d.h. Yabu würde das Skript für "all-multi" gar nicht mehr ausführen.

..3 Die Reihenfolge von Quellen

Yabu versucht normalerweise, die in einer Regel aufgeführten Quellen in der angegebenen Reihenfolge zu erreichen. Dafür gibt es allerdings keine Garantie, denn die Reihenfolge kann sich aus verschiedenen Gründen ändern. Einer davon ist, daß Yabu Ziele parallel bearbeitet (siehe Parallel und verteilt: Yabu im Netzwerk). Dann hängt es im wesentlichen von der Laufzeit der ausgeführten Skripte ab, in welcher Reihenfolge Yabu die Quellen erreicht. Ein weiterer Faktor, der die Reihenfolge der Quellen beeinflußt, sind explizit definierte Gruppen (siehe oben).

Normalerweise ist die Reihenfolge auch gar nicht wichtig, sondern es kommt nur darauf an, daß Yabu alle Abhängigkeiten zwischen den Zielen beachtet. Doch es gibt auch Ausnahmen:

run-all:: build-prog test-prog clean

Hier ist offensichtlich eine bestimmte Reihenfolge beabsichtigt. Es würde wahrscheinlich zu unerwarteten Ergebnis führen, wenn wenn Yabu das Ziel "clean" vor oder gleichzeitig mit test-prog" erreicht. Um in solchen Fällen eine bestimmte Reihenfolge der Ziele zu erzwingen, kann man zusätzliche Regeln schreiben:

run-all:: build-prog test-prog clean
test-prog:: build-prog
clean:: test-prog

Damit ist gewährleistet, daß Yabu die Ziele "build-prog", "test-prog" und "clean" in genau dieser Reihenfolge erreicht. Kürzer und eleganter ereicht man das Gleiche mit einer speziellen Syntax:

run-all:: build-prog , test-prog , clean

Das Komma zwischen zwei Quellen erzwingt die sequentielle Bearbeitung und verhindert, daß Yabu die Quellen parallel bearbeitet. Man beachte, daß links und rechts des Kommas ein Leerzeichen oder Tab stehen muß. Die Syntax ist sogar noch etwas allgemeiner, wie das folgende Beispiel zeigt:

all:: init1 init2 , run1 run2 run3 , cleanup

Das bedeutet, daß Yabu zuerst versucht, "init1" und "init2" zu erreichen, wobei die Reihenfolge nicht bestimmt und Parallelisierung erlaubt ist. Danach behandelt Yabu "run1", "run2" und "run3", wiederum in nicht definierter Reihenfolge und möglicherweise parallel. Schließlich versucht Yabu noch "cleanup" zu erreichen.

Wenn man den hier beschriebenen Mechanismus verwendet, sollte man immer die möglichen Auswirkungen im Fehlerfalle bedenken. Durch die (impliziten) Zusatzregeln übertragen sich nämlich Fehler von den links stehenden Quellen auf die Quellen rechts des Kommas. Tritt im obigen Beispiel ein Fehler bei "init1" auf, dann betrachtet Yabu "run1", "run2", "run3" und "cleanup" ebenfalls als unerreichbar und macht gar keinen Versuch mehr, diese Ziele zu erreichen.

.14 Prototyp-Regeln (%-Platzhalter)

Kurz und knapp

Beschreibung

In größeren Projekten gelten oft für viele Dateien die gleichen Abhängigkeiten und Kommandos zur Regenerierung. Beispielsweise ist in einem C-Projekt normalerweise von XXX.o von XXX.c abhängig, und das Kommando zur Regenerierung von XXX.o ist immer der gleiche Compileraufruf, nur mit verschiedenen Dateinamen.

Hierfür lassen sich sogenannte Prototyp-Regeln einsetzen, die in einer Regel Abhängigkeiten und Skript für eine ganze Gruppe von Dateien enthalten. Eine Prototyp-Regel ist per Definition eine Regel, deren Ziel das Platzhalterzeichen "%" enthält. Das "%" steht hierbei für eine beliebige Zeichenfolge mit Ausnahme von "." und "/". In Quellen und Skript kann man den Teilstring beliebig oft mit "%" referenzieren. Ein einfaches Beispiel:

%.o: %.c
  cc -o %.o %.c

definiert eine Regel für beliebige Ziele, die mit .o enden. Versucht Yabu beispielweise, das Ziel aaa.o zu erreichen, dann würde die obige Regel anwendbar, wobei der Platzhalter "%" durch 'aaa' ersetzt wird:

aaa.o: aaa.c
  cc -o aaa.o aaa.c

Regeln mit mehreren Platzhaltern

Noch allgemeinere Prototyp-Regeln als oben lassen sich formulieren, wenn man mehr als einen Platzhalter benutzt. Hierbei gelten die gleichen Konventionen wie bei Variablentransformationen. Innerhalb der Regel und der Liste der Quellen referenziert man die einzelnen Platzhalter mit %1, %2, usw.

Ein Beispiel: angenommen in einem Projekt gibt es drei Unterverzeichnisse "rot", "blau" und "gelb", in denen drei Fassungen der gleichen Programme kompiliert werden sollen. Der einzige Unterschied besteht in einer spezifischen Includedatei (rot.h, blau.h, gelb.h). Alle C-Dateien verwenden das Makro FARBE, um die passende Includedatei einzubinden. Das könnte zum Beispiel so aussehen:

#if FARBE==rot
#include "rot.h"
#elif FARBE=blau
...

Ferner nehmen wir an, daß sich alle C- und Includedateien im Verzeichnis "src" befinden. Für dieses Szenario läßt sich eine einzige Regel schreiben:

%/%.o: src/%2.c src/%1.h
  cc -o %1/%2.o -DFARBE=%1 -c ../src/%2.c

Zur Erreichung des Ziels "rot/main.o" würde Yabu dann folgende Regel verwenden:

rot/main.o: src/main.c src/rot.h
  cc -o rot/main.o -DFARBE=rot -c ../src/main.c

Ein Nebeffekt des hier beschriebenen Mechanismus ist, daß auf einen Platzhalter nicht unmittelbar eine Ziffer folgen kann. Im folgenden Beispiel soll eine Regel definiert werden, die xxx.o aus xxx8086.c erzeugt. Der naheliegende Ansatz

%.o: %8086.c        <---- FEHLER
   cc -o $(0) -c $(1)

funktioniert jedoch nicht, weil Yabu "%8" als Referenz auf den achten Platzhalter interpretiert. Die Lösung in diesem Fall ist, statt "%" "%1" zu benutzen:

%.o: %18086.c
   cc -o $(0) -c $(1)

% und %%

Die oben genannte Einschränkung bis zum nächsten "." bzw. "/" verhindert ungewollte Nebeneffekte. Beispiel:

%: %.c
    $(CC) -o $(0) $(1)
%.o: %.c
    $(CC) -c -o $(0) $(1)

all:: prog.o

Ohne die besagte Einschränkung gäbe es zwei passende Regeln für "prog.o",

$(CC) -o prog.o prog.o.c

und

$(CC) -c -o prog.o prog.c

Yabu würde deshalb einen Fehler melden und die Verarbeitung abbrechen. Daß eine Datei namens prog.o.c gar nicht existiert, nutzt übrigens nichts. Yabu legt bei der Auswahl der passenden Regeln nämlich ausschließlich das Ziel (und die aktive Konfiguration) zugrunde.

Benutzt man '%%' als Platzhalter, dann fällt die obige Einschänkung weg. '%%' steht für eine beliebige Zeichenfolge, die auch "." und "/" enthalten darf. Zum Beispiel beschreibt

%%.o: %.c
   cc -o %.o -c %.c

die Erzeugung von Objektdateien in beliebig tiefen Unterverzeichnissen. Eine mögliche Anwendung dieser Regel wäre

libs/libbuffer/buffer.o: libs/libbuffer/buffer.c
  cc -o libs/libbuffer/buffer.o -c libs/libbuffer/buffer.c

Prototyp-Regeln und Variablen

Ein %-Platzhalter kann an fast beliebiger Stelle – in der Quellenliste oder im Skript – benutzt werden. Er darf beispielsweise in einer Transformationsvorschrift stehen, wie im folgenden Beispiel:

OBJS=aaa bbb ccc
%/prog: $(OBJS:*=tmp/*_%.o)
   ...

Angewendet auf das Ziel "debug/prog" würde diese Regel wie folgt aussehen:

debug/prog: tmp/aaa_debug.o tmp/bbb_debug.o tmp/ccc_debug.o
   ...

Ein % kann sogar in einem Variablennamen stehen. Das könnte man zum Beispiel nutzen, um Compileroptionen verzeichnisabhängig festzulegen:

CFLAGS_debug=-g -DDEBUG
CFLAGS_release=-O
%/%.o: src/%2.c
   $(CC) $(CFLAGS_%1) -o %1/%2.o -c src/%2.c

Mögliche Ausprägungen dieser Regel wären

debug/xxx.o: src/xxx.c
   $(CC) -g -DDEBUG -o debug/xxx.o -c src/xxx.c
release/xxx.o: src/xxx.c
   $(CC) -O -o release/xxx.o -c src/xxx.c

Bei größeren Projekten ist es aber einfacher, solche Fallunterscheidungen über Optionen zu behandeln Details siehe Konfigurationen. Das obige Beispiel würde dann so aussehen:

!configuration [debug]
   CFLAGS=-g
!configuration [release]
   CFLAGS=-O

%/%.o: [%1] src/%2.c
   $(CC) $(CFLAGS) -o %1/%2.o -c src/%2.c

'%' ohne Platzhalterfunktion

In seltenen Fällen kommt in einer Prototypregel ein Prozentzeichen als normales Zeichen ohne Platzhalterfunktion vor. In der Quellenliste und im Skript schreibt man dann "%%". Zum Bespiel würde

timestamp-%:
    date +%%H%%M%%S >$(0)

für das Ziel "timestamp-begin" das Komando

    date +%H%M%S >timestamp-begin

ausführen. Man beachte, daß diese spezielle Bedeutung des doppelten Prozentzeichens nur in Prototypregeln wirksam ist. In einer normalen Regel würde man

timestamp-begin:
    date +%H%M%S >$(0)

schreiben.

Soll dagegen das Ziel ein Prozentzeichen enthalten, schreibt man "%%%". Zum Beispiel ist

local%%%:
   touch $(0)

keine Protoypregel sondern eine gewöhnliche Regel für das Ziel "local%".

.15 Spezielle Variablen für Ziel und Quellen: $(n) und $(*)

Kurz und Knapp

Beschreibung

Innerhalb eines Skriptes sind einige zusätzliche Variablen definiert, mit denen man den Namen des Ziels und der Quellen einfügen kann. Ein Beispiel: statt

prog: aaa.o bbb.o ccc.o
  cc -o prog aaa.o bbb.o ccc.o

schreibt man

prog: aaa.o bbb.o ccc.o
  cc -o $(0) $(*)

und vermeidet die fehlerträchtige Wiederholung der Dateinamen im Skript.

Man beachte, daß die Auswertung von $(n) nach der Ersetzung von Variablen und %-Platzhaltern in Ziel und Quellen stattfindet. Das obige Beispiel könnte man also auch so schreiben:

OBJS=aaa.o bbb.o ccc.o
prog: $(OBJS)
  cc -o $(0) $(1) $(2) $(3)

Hier hat $(1) den Wert "aaa.o" und nicht etwa "aaa.o bbb.o ccc.o".

Die Variable $(0) hat eine Sonderrolle, sie läßt sich nämlich nicht nur im Skript, sondern bereits in der Liste der Quellen verwenden. Hier ist ein Beispiel:

start-% stop-% check-%: $(0).c
    cc -o $(0) $(1)

Diese Regel würde zum Beispiel "start-server" aus "start-server.c" kompilieren, und genauso "stop-client" aus "stop-client.c".

Bis auf die Tatsache, daß sie nur in Skripten definiert und niemals konfigurationsabhängig sind, verhalten sich $(0), $(1), ... und $(*) wie gewöhnliche Variablen. Insbesondere kann man ihren Wert durch eine Transformationsregel (siehe Transformationsregeln) modifizieren. Damit läßt sich in manchen Fällen vermeiden, eine zusätzliche Variable einzuführen. Zum Beispiel könnte man statt

TAR=../releases/archive/latest.tar
$(TAR).gz: $(FILES)
  tar cf $(TAR) $(*)
  gzip $(TAR)

auch (einfacher?)

../releases/archive/latest.tar.gz: $(FILES)
  tar cf $(0:**.gz=**) $(*)
  gzip $(0:**.gz=**)

schreiben.

.16 Skripte

»

Kurz und knapp

Details

Die in einer Regel enthaltenen Kommandos – das sogenannte Skript – führt Yabu aus, nachdem alle Quellen erreicht wurden und wenn das Ziel in Bezug auf die Quellen veraltet ist (Details hierzu siehe ). Das Skript besteht im Normalfall aus einem oder mehreren Shell-Kommandos; zur Ausführung verwendet Yabu die Stamdard-Shell des Betrebssystems (/bin/sh), falls nicht per Programmeinstellung oder im Buildfile ein anderer Interpreter vorgeschrieben wird (siehe unten).

Einrückung

Alle auf die Kopfzeile einer Regel folgenden, eingerückten Zeilen bilden das Skript. Leerzeilen und Kommentare entfernt Yabu bereits beim Lesen des Buildfiles; sie werden nicht Bestandteil des Skriptes. Man beachte aber, daß Yabu-Kommentare immer mit einem "#" in Spalte 1 beginnen. Eingerückte Kommentarzeilen behandelt Yabu als Teil des Skriptes. Ein Beispiel: bei Ausführung der Regel

program: obj1.o obj2.o

# Skript beginnt hier
     # Programm linken:

     gcc -o $(0) $(*)
# Skript-Ende

würde Yabu das folgende Skript an die Shell übergeben:

# Programm linken:
gcc -o program obj1.o obj2.o

Wie man sieht, entfernt Yabu die Einrückung aus dem Buildfile. Yabu versucht dabei, "intelligent" vorzugehen und relative Einrückungen der Skriptzeilen untereinander zu erhalten. Dabei dient die erste Skriptzeile aus Referenz; die nachfolgenden Zeilen behalten soweit möglich ihre Einrückung relativ zur ersten. Zum Beispiel enthält die Regel

prog: prog1.c prog2.c
    cc -c prog1.c
      cc -c prog2.c
  cc -o prog prog1.o prog2.o

das folgende dreizeilige Skript:

cc -c prog1.c
  cc -c prog2.c
cc -o prog prog1.o prog2.o

Für die Einrückung können Leerzeichen und Tabulatoren beliebig gemischt werden. Bei der Berechnung der Einrückungstiefe rechnet Yabu mit einer Tabulator-Schrittweise von 8.

Mehrzeilige Skripte und Verwendung von "{"..."}"

Vor der Ausführung des Skriptes ersetzt Yabu zunächst alle Variablen und %-Platzhalter. Den dabei entstandenen Text führt Yabu zeilenweise aus, das heißt, für jede Zeile startet Yabu eine eigene Shell (im Normalfall /bin/sh). Dieses Verhalten ist kompatibel zu Make und hat den Vorteil, daß Yabu bei jeder Zeile prüft, ob das Kommando erfolgreich war und im Fehlerfalle das Skript abbricht.

Bei längeren Skripten ist allerdings die zeilenweise Ausführung oft unpraktisch. Sie verhindert zum Beispiel den Gebrauch von Shellvariablen:

ziel: quelle1 quelle2
   TGT="$(0)"
   echo $TGT             <--- $TGT="" !

Eine mögliche Lösung dieses Problems ist, das gesamte Skript mit Hilfe von Fortsetzungszeichen (\) in eine einzige logische Zeile zu schreiben. Darüber hinaus bietet Yabu die Möglichkeit, ein mehrzeiliges Skript als zusammenhängend zu definieren, indem man es mit "{" beginnt und mit "}" abschließt. Beide Klammern müssen jeweils am Ende einer Zeile stehen. Beispiel:

backup::
  {
     TIMESTAMP=`date +%%y%%m-%%d%%M%%H%%S`
     mkdir backup/$TIMESTAMP || exit 1
     foreach f in tmp/*.o ; do
        cp $x backup/$TIMESTAMP || exit 1
     done
  }

Man beachte, daß auch bei dieser Syntax alle Skriptzeilen --- einschließlich der '{' und '}' --- eingerückt sein müssen.

Bei der Verwendung von mehrzeiligen Skripten muß man auf eine korrekte Fehlerbehandlung achten. Der Exitcode der Shell spiegelt nämlich nur das Ergebnis des zuletzt ausgeführten Kommandos wieder, und Fehler innerhalb des Skripts bleiben unter Umständen unbemerkt. Das folgende Beispiel veranschaulicht das Problem:

copy-temp::
  {
    [ -d temp ] || mkdir temp    <-- Fehler bleibt unbemerkt
    cp app.c temp
  }

Wenn eine Datei namens "temp" existiert, würde zwar das mkdir scheitern (und eine entsprechende Meldung ausgeben), das folgende cp-Kommando würde aber dennoch die Datei mit app.c überschreiben, und das Skript erfolgreich abschließen. Yabu hätte keine Möglichkeit, den Fehler zu erkennen. Um das zu vermeiden, könnte man das Skript wie folgt erweitern:

copy-temp::
  {
    [ -d temp ] || mkdir temp || exit 1
    cp app.c temp
  }

Noch besser wäre in diesem Fall allerdings, auf die "{"..."}" ganz zu verzichten, da es keinen triftigen Grund dafür gibt. Yabu würde dann bei einem Fehler in der ersten Zeile das Skript abbrechen.

Verschachtelte '{...}'-Konstruktionen sind erlaubt, sofern alle Klammern jeweils am Ende einer Zeile stehen. Für Yabu haben die inneren Klammerebenen keine Bedeutung sondern werden Bestandteil des Skriptes. Auf diese Weise kann man zum Beispiel Shell-Funktionen in Skripten definieren:

copy-templates::
  {
    Copy() {
        cp -r $1 $2
    }
    cp src/templates $(INSTALLDIR)
  }

Mehrzeilige Skripte mit "|"

Enthält ein Skript viele geschweifte Klammern, dann ist die oben beschriebene Syntax mit "{" und "}" umständlich und fehleranfällig. Für diese Fälle bietet Yabu eine alternative Schreibweise: man beginnt jede Skriptzeile mit einem "|". Yabu faßt aufeinanderfolgenden Zeilen dieser Form zu einem Kommando zusammen und entfernt das führende "|". Das vorige Beispiel könnte man also auch so schreiben:

copy-templates::
 |Copy() {
 |    cp -r $1 $2
 |}
 |cp src/templates $(INSTALLDIR)

Ausgaben von Skripten

Ein Skript hat zwei Ausgabekanäle: die Standardausgabe (stdout) und die Fehlerausgabe (stderr). Yabu leitet bei Skripten die Fehlerausgabe auf die Standardausgabe um. Das heißt, alle Fehlermeldungen aus dem Skript erscheinen zusammen und in der richtigen Reihenfolge mit den normalen Meldungen von Yabu in der Standardausgabe.

Der Sinn dieser Umleitung wird klar, wenn ein Skript sehr viele Ausgaben erzeugt, zum Beispiel Fehlermeldungen eines Compilers. In solchen Fällen möchte man vielleicht die Meldungen mit "more" seitenweise anzeigen oder in eine Datei schreiben. Beides geht sehr einfach, da Yabu Meldungen ausschließlich über stdout ausgibt:

yabu | more
yabu >LOGFILE

Das oben gesagte gibt nur für normale Skripte in Regeln. Bei Autodepend-Skripten (siehe unten) und Autokonfigurations-Skripten (siehe Auswahl der Konfiguration) bleibt stderr und stdout getrennt. In diesen Fällen wertet Yabu die Standardausgabe selbst aus, und Fehlermeldungen des Skriptes erscheinen in der Standardausgabe.

Ändern der Default-Shell

Ein Skript wird normalerweise durch die Shell /bin/sh ausgeführt. Yabu ruft die Shell mit zwei Argumenten auf, und zwar

/bin/sh -c Script

wobei Script das auszuführende Skript bzw. der auszuführende Teil des Skriptes ist. Über die Programmeinstellung shell (siehe Programmeinstellungen) oder über die Option -S kann man aber auch ein beliebiges anderes Programm zur Ausführung der Skripte vorgeben. Voraussetzung ist, daß die alternative Shell die obige Aufrufsyntax unterstützt.

In speziellen Fällen kann man auch für ein Skript die Shell individuell festlegen. Hierzu benutzt man die Syntax "#!SHELL" in der ersten Zeile des Skriptes, wobei SHELL der vollständige Pfad der zu verwendenden Shell ist. Beispiel:

ziel: quelle
    #!/usr/bin/csh
    ... (csh-Skript) ...

Bei dieser Variante gibt es zwei wichtige Unterschiede zum Normalfall:

..1 Das Environment von Skripten

Jedes von Yabu ausgeführt Skript – sei es lokal oder über einen Yabu-Server – erhält gewisse Umgebungsvariablen (Environment). Anders als Make gibt Yabu jedoch nicht die gesamte Umgebung des Benutzers an Skripte weiter, sondern nur explizit ausgewählte sowie einige spezielle Yabu-interne Variablen. Das hat einen einfachen Grund: wenn verschiedene Benutzer das gleiche Buildfile verwenden, sollte das Ergebnis reproduzierbar sein und nicht von persönlichen Einstellungen der Benutzer abhängen.

Noch wichtiger wird die Trennung von Benutzer- und Skript-Umgebung, wenn Yabu Skripte in einem Rechnerverbund ausführt (siehe Parallel und verteilt: Yabu im Netzwerk). Skripte laufen dann unter Umständen auf einer anderen Hardwarearchitektur oder einem anderen Betriebssystem als die Shell des Benutzers, und ein Übernehmen der Benutzerumgebung könnte die Skriptausführung sogar unmöglich machen.

Vordefiniertes Environment

Yabu definiert eine Reihe von Umgebungsvariablen automatisch:

Exportieren weiterer Variablen mit !export

Mit Hilfe der !export-Anweisung kann ein Buildfile beliebige weitere Variablen in das Skript-Environment einfügen. Sie hat folgendes allgemeine Format

!export NAME... $NAME ...
    NAME... $NAME ...
    ...
    NAME=WERT
    ...

Variablennamen ohne vorangestelltes '$' sind Yabu-Variablen. Diese müssen im Buildfile definiert sein, ansonsten tritt bei der Ausführung des Skriptes ein Fehler auf. Anders als Systemvariablen erhalten explizit exportierte Variablen keinen Präfix "YABU_". Außerdem können sie konfigurationsabhängig sein, wie das folgende Beispiel zeigt:

!export OSNAME
OSNAME[linux]=Linux
OSNAME[netbsd]=NetBSD
...
build/%/osname: [%]
   echo $OSNAME >$(0)

Die Reihenfolge von !export und Variablenzuweisungen ist nicht relevant.

Ein Name mit vorangestelltem '$' bezeichnet dagegen eine Variable in der Umgebung des Benutzers. Zum Beispiel würde man mit

!export $LC_TIME

die Variable LC_TIME des Benutzers an alle Skripte weitergeben. Ist die betreffende Variable beim Aufruf von Yabu nicht definiert, wird sie auch nicht exportiert, ohne daß Yabu eine Fehlermeldung ausgibt. Interaktive Programme benötigen oft benutzerspezifische Umgebungsvariablen und funktionieren deshalb nicht, wenn sie aus einem Yabu-Skript gestartet werden. In diesen Fällen muß man dem Skript die fehlenden Variablen entweder im Skript selbst setzen, oder man exportiert sie mit !export $XXXX. Ein Beispiel sind X-Windows-Clients: Programme wie xterm oder xmessage funktionieren in Yabu-Skripten erst nachdem man mit

!export $HOME $DISPLAY

die erforderlichen Variablen exportiert hat.

Die dritte Form (NAME=WERT) definiert eine Umgebungsvariable, ohne daß eine gleichnamige Yabu-Variable existieren müßte. Bei dieser Form muß man jede Definition in einer eigenen Zeile schreiben. Beispiel:

!export
   LC_ALL=C
   TZ=MET

Bei der Verwendung von !export $XXXX ist generell Vorsicht angebracht, denn damit kann man den Vorteil der kontrollierten Skriptumgebung (reproduzierbare Ergebnisse, unabhängig von Benutzereinstellungen) wieder zunichtemachen. Eine Anweisung wie

!export $PATH $LD_LIBRARY_PATH    <--- NICHT EMPFOHLEN

sollte man deshalb vermeiden.

.17 Autodepend-Skripte

Kurz und knapp

Beschreibung

In den meisten Softwareprojekten gibt es zwischen den Quelldateien "versteckte" Abhängigkeiten, die nicht durch eine Regel im Buildfile beschrieben werden. Ein typisches Beispiel dafür sind #include-Anweisungen in C-Programmen: das Kompilat ist nicht nur von der C-Quelldatei abhängig, sondern auch von allen darin benutzten Header-Dateien. Diese versteckten Abhängigkeiten manuell im Buildfile zu erfassen, wäre mühsam und fehleranfällig. Der Vorgang läßt sich jedoch automatisieren, und zwar mit Hilfe von Autodepend-Skripten.

Wie das funktioniert, sei hier am Beispiel von C-Programmen und dem GNU-Compiler beschrieben. Der Compiler besitzt eine Option (-M), mit der er sämtliche per #include eingebundenen Dateien eines C-Programms bestimmt und in make-kompatibler Syntax ausgibt. Zum Beispiel könnte der Befehl

gcc -M app.c

folgende Ausgabe liefern:

app.o: app.c utils.h parser.h

Damit Yabu von dieser Fähigkeit des Compilers Gebrauch macht, ergänzt man die Regel zum Kompilieren um ein Autodepend-Skript:

%.o: %.c
   gcc -o $(0) -c $(1)
   [auto-depend]
   gcc -M -c $(1)

Durch diese Ergänzung werden nun alle versteckten Abhängigkeiten automatisch berücksichtigt. Nach einer Änderung an "parser.h" würde also "app.c" neu kompiliert.

Syntax und Ausgabeformat

Ein Autodepend-Skript steht immer nach dem eigentlichen Skript (das im Folgenden mit Build-Skript bezeichnet wird) und wird durch die (eingerückte!) Zeile [auto-depend] eingeleitet. Für den Inhalt des Autodepend-Skriptes gelten die gleichen Regeln wie für das Build-Skript, insbesondere die Gruppierung mit "{" und "}" und die Verwendung von %-Platzhaltern sowie $(0), $(1) usw.

Die Ausgabe des Autodepend-Skriptes muß das oben beschriebene make-kompatible Format haben, also

Ziel : Quelle Quelle ...

der Teil links vom Doppelpunkt ist nicht relevant, Yabu ignoriert ihn vollständig. Rechts vom Doppelpunkt steht eine Liste von Dateinamen. Sie kann sich über mehrere Zeilen erstrecken, wobei alle Zeilen außer der letzten mit einem "\" enden.

Ausführung des Autodepend-Skripts

Yabu führt das Autodepend-Skript jedesmal aus, nachdem das Build-Skript erfolgreich abgeschlossen wurde. Dadurch bleiben die Informationen über versteckte Quellen immer aktuell. Tritt im Build-Skript ein Fehler auf oder war das Ziel bereits erreicht, dann führt Yabu auch das Autodepend-Skript nicht aus.

Die vom Skript ausgegebene Quellenliste vergleicht Yabu mit den schon bekannten Quellen. Ist eine Quelle bereits vorhanden, weil sie in einer Regel aufgeführt ist, dann passiert nichts. Andernfalls fügt Yabu die Quelle dem Ziel hinzu. Die Liste aller (regulärer und automatischer) Quellen schreibt Yabu in die Statusdatei. Sie steht somit beim nächsten Aufruf sofort zur Verfügung.

Behandlung automatischer Quellen

Es gibt zwei wichtige Unterschiede zwischen automatisch ermittelten und regulären (d.h. in einer Regel aufgeführten) Quellen. Der erste ist, daß man automatische Quellen nicht nicht über $(n) oder $(*) referenzieren kann; diese Variablen enthalten ausschließlich die in der Regel explizit aufgeführten Quellen.

Zweitens ist Yabu toleranter gegenüber nicht erreichbaren automatischen Quellen. Wenn eine automatische Quelle nicht vorhanden ist und auch keine Regel dafür existiert, dann entfernt Yabu die Quelle stillschweigend, statt einen Fehler zu melden. Dieses Verhalten ist sinnvoll, wenn durch Änderung von Quelldateien eine Abhängigkeit weggefallen ist.

Ein besonderes Problem sind Abhängigkeiten von generierte Dateien. Solche Dateien muß man immer explizit als Quellen aufführen. Dazu ein Beispiel: die Datei "app.c" benötigt die Headerdatei "defs.h", die wiederum bei Bedarf automatisch erzeugt wird:

app.o: app.c
   gcc -o $(0) -c $(1)
   [auto-depend]
   gcc -MM -c $(1)
defs.h:
   gen-defs -o $(0)

Dieses Buildfile funktioniert nur dann wie beabsichtigt, wenn die Datei "defs.h" bereits vorhanden ist und nie gelöscht wird. Soll Yabu "defs.h" bei Bedarf neu erzeugen, dann muß man sie explizit als Quelle aufführen:

app.o: app.c defs.h
   gcc -o $(0) -c $(1)
   [auto-depend]
   gcc -MM -c $(1)
defs.h:
   gen-defs -o $(0)

Eine Vereinfachung

Neuere Versionen des GCC kennen die Option "-MD", mit der sich die Erstellung der Abhängigkeiten noch etwas effizienter machen läßt. Ruft man ihn mit "-MD" auf, dann schreibt der GCC die beim Kompilieren gefundenen Quellen in eine Datei mit der Endung .d. Im Autodepend-Skript kann man dann die .d-Datei lesen und spart den zweiten Compileraufruf. Eine typische Regel zum Kompilieren beliebiger C-Dateien sieht dann so aus:

%.o: %.c
   $(CC) -MD $(CFLAGS) -o $(0) -c $(1)
[auto-depend]
   cat %.d

Die .d-Datei wird übrigens nicht weiter benötigt; man könnte sie im Autodepend-Skript sogleich wieder löschen.

.18 Verarbeitung von Bibliotheken

Yabu kann Dateien innerhalb einer (ar-kompatiblen) Bibliothek wie gewöhnliche Dateien behandeln. Die Syntax hierfür ist die gleiche wie bei Make, nämlich: "BIBLIOTHEK(DATEI)". Dateien in Bibliotheken können sowohl in den Zielen als auch in den Quellen einer Regel vorkommen. Ein Beispiel:

lib.a:: lib.a(file1.o) lib.a(file2.o) lib.a(file3.o)

Man beachte, daß die Bibliothek (lib.a) hier als Alias definiert ist. Mit einer gewöhnlichen Regel (":" statt "::") würde Yabu die Änderungszeit von lib.a benutzen, um zu entscheiden ob die Regel ausgeführt werden soll. Das ist aber nicht erwünscht, entscheidend sind nämlich die nur die Änderungszeiten der einzelnen Dateien innerhalb der Bibliothek.

Ein weiteres Beispiel, bei dem eine Bibliothek als Ziel auftritt:

mylib.a(obj1.o): obj1.cc
   $(CC) -o obj1.o obj1.cc
   ar r mylib.a obj1.cc

Im Unterschied zu Make wird die Variable $(0) übrigens nicht besonders behandelt, sie hätte hier den Wert "mylib.a(obj1.o)".

Explizit ausgeschriebene Regeln wie im letzten Beispiel sind in der Praxis meist zu aufwendig. Normalerweise wird man die Bibliotheks-Syntax deshalb in Prototypregeln benutzen, wie das folgende Beispiel zeigt:

mylib.a(%.o): %.o
   ar r mylib.a $1

Eine weitere Vereinfachung besteht darin, daß bei den Quellen einer Regel (nicht jedoch bei den Zielen!) innerhalb der "(...)" eine ganze Liste von Dateien stehen kann. Statt

target: lib(file1) lib(file2) lib(file2)

darf man also

target: lib(file1 file2 file2)

schreiben. Damit ist es möglich, die Dateiliste als Variable zu schreiben:

OBJS=file1 file2 file2
target: lib($(OBJS))

.19 Spezielle Ziele

» » »

Einige Ziele behandelt Yabu weder als Alias noch als Datei, sondern als Anweisung, spezielle Aktionen auszuführen. Alle diese speziellen Ziele beginnen mit einem Ausrufezeichen bestehen ansonsten aus Großbuchstaben und "_".

..1 !INIT - Initialisierung

Vor allen anderen Zielen versucht Yabu, das Ziel !INIT zu erreichen. Mit Hilfe einer Regel für !INIT kann man somit beliebige Aktionen definieren, die Yabu bei jedem Aufruf ausführen soll. Gibt es keine Regel für !INIT, dann passiert nichts (insbesondere tritt kein Fehler auf). Prototyp-Regeln werden nie benutzt, auch wenn !INIT auf das Ziel-Muster passen würde:

%INIT:           <----- wird für !INIT  n i c h t  benutzt
   echo INIT

Yabu behandelt das Ziel !INIT – genauso wie "all" – immer als Alias, unabhängig davon ob die Regel mit ":" oder "::" geschrieben wurde. Das gilt allerdings nur für !INIT selbst, nicht für etwaige Quellen:

INIT:  init1 init2
INIT:: init1 init2    <----- äquivalent

init1::
   echo "ok"

init2:                <----- FEHLER: kein Alias
   echo "auch ok?"

..2 !CLEAN_STATE - Status löschen

Das Ziel !CLEAN_STATE ist immer erreichbar und bewirkt daß Yabu alle Statusinformationen aus Buildfile.state löscht. Sofern danach keine neuen Statusinformationen anfallen – etwa durch Ausführen eines Autodepend-Skriptes – ist die Statusdatei beim Ende von Yabu leer und wird deshalb gelöscht. !CLEAN_STATE kann nur als Quelle in einer Regel benutzt werden. Taucht es als Ziel in einer Regel auf, dann ergibt dies keine Fehler, die Regel wird aber niemals ausgeführt werden.

Das folgende Beispiel zeigt die typische Verwendung von !CLEAN_STATE:

clean: clean-objs clean-docs !CLEAN_STATE
    rm -f $(PROGRAMS)

Beim Aufruf "yabu clean" würde Yabu zunächst versuchen, die Ziele "clean-objs" und "clean-docs" zu erreichen, danach alle Statusinformationen löschen, und schließlich das Skript ausführen.

..3 !ALWAYS - Immer aktualisieren

Das Ziel !ALWAYS wird bei jedem Aufruf von Yabu aktualisiert, ohne daß hierfür eine Regel vorhanden sein muß (tatsächlich würde Yabu eine solche Regel ignorieren). Alle von !ALWAYS abhängigen Ziele sind also bei jedem Aufruf von Yabu veraltet und werden ebenfalls aktualisiert. Zum Beispiel bewirkt

prog: $(OBJS) !ALWAYS
  cc -o $(0) $(OBJS)

daß Yabu das Programm prog jedes Mal neu kompiliert, sofern es als Ziel ausgewählt ist.

Das wiederspricht dem Grundprinzip, Ziele nur zu aktualisieren wenn sie veraltet sind. Tatsächlich braucht man !ALWAYS nur in Ausnahmefällen oder bei der Fehlersuche. Häufige Verwendung von !ALWAYS ist ein Hinweis darauf, daß die Beschreibung der Abhängigkeiten im Buildfile fehlerhaft oder zumindest unvollständig ist.

.20 Unterprojekte: die !project-Anweisung

Kurz und knapp

Motivation

Größere Softwareprojekte bestehen fast immer aus Komponenten, die mehr oder weniger unabhängig voneinander weitentwickelt werden. Für diese Fälle enthält Yabu mit den sogenannten Unterprojekten einen Mechanismus, mit dem man mehrere unabhängige Buildfiles in einem übergeordneten Buildfile zusammenführen kann. Dadurch bleiben die Unterprojekte einfach – im Idealfall ist von der Einbettung in ein größeres Projekt nichts zu merken. Zugleich kann aber der Verwalter des übergeordneten Projektes Abhängigkeiten zwischen den Unterprojekten päzise bis herunter auf einzelne Ziele beschreiben. Bei dem oft praktizierten Verfahren, Makefiles in Unterverzeichnissen rekursiv durch unabhängigke make-Prozesse abzuarbeiten, ist das dagegen schwierig. Tatsächlich findet man in der Praxis oft Behelfslösungen, welche die Buildzeiten unnötig verlängern oder in manchen Szenarien gar nicht korrekt arbeiten (siehe dazu auch [PM98]).

Ein Unterprojekt definiert man mit einer !project-Anweisung. Zum Beispiel besagt

!project gui/Buildfile

daß sich im Unterverzeichnis "gui" ein unabhängiges Projekt mit eigenem Buildfile befindet. Die Anweisung erwartet als Argument den Namen eines Buildfiles (der natürlich auch anders als "Buildfile" lauten kann) in einem Unterverzeichnis des aktuellen Verzeichnisses. Das untergeordnete Buildfile kann sich auch mehrere Verzeichnisebenen tiefer befinden. Es muß allerdings immer unterhalb des aktuellen Verzeichnisses liegen, der Pfad darf also nicht mit "/" beginnen:

!project components/libraries/openssl/buildfile.ssl  ----> Ok
!project /common/openssl/Buildfile                   ----> FEHLER

Eine !project-Anweisung bewirkt zweierlei. Zum einen liest Yabu das angegebene Buildfile. Im Unterschied zu .include wird aber nicht einfach der Dateiinhalt an der aktuellen Stelle eingefügt, sondern es entsteht ein eigenständiges (Unter-)Projekt. Das heißt, Variablen, Konfigurationen und Regeln im Unterprojekt und im Hauptprojekt sind unabhängig voneinander und stören sich nicht gegenseitig. Eine Ausnahme sind die von Yabu vordefinierten Variablen (siehe Systemvariablen), die global definiert sind und überall den gleichen Wert haben. Das gleiche gilt für Optionen, die in der Konfigurationsdatei definiert wurden (siehe Parallel und verteilt: Yabu im Netzwerk): auch diese gelten übergreifend für alle Projekte.

Zum Einlesen des Buildfiles wechselt Yabu temporär in das Projektverzeichnis. Dadurch ist gewährleistet, daß Yabu das Buildfile genauso interpretiert, als würde Yabu direkt im Projektverzeichnis ausgeführt. Das betrifft unter anderem die Wirkung von $(.find), $(.dirfind) und $(.glob), aber auch die Interpretation relativer Dateinamen durch .include.

Der zweite Wirkung von !project betrifft die Behandlung von Zielen. Immer wenn der Name eines Ziels mit dem Projektverzeichnis beginnt, entfernt Yabu den Verzeichnisnamen und delegiert das modifizierte Ziel an das Unterprojekt. Zum Beispiel würde Yabu in

!project gui/Buildfile
all:: gui/all

beim Ziel "gui/all" feststellen, daß es mit "gui/" beginnt und das Ziel als "all" an das Unterprojekt delegieren. Ohne den Unterprojekt-Mechanismus würde man ein ähnliches Verhalten mit

all::
   cd gui; yabu all

erreichen.

Umgekehrt werden Ziele des Unterprojektes, deren Name mit "../" beginnt, an an übergordnete Projekt delegiert. Steht zum Beispiel in gui/Buildfile die Regel

viewer: $(VIEWER_OBJS) ../libs/libpng.so

dann wird Yabu, um "../libs/libpng.so" zu erreichen, das Ziel "libs/libpng.so" an das übergeordnete Buildfile delegieren. Man beachte, daß trotz des Delegationsmechanismus das Buildfile im Unterprojekt eigenständig bleibt. Ein Entwickler, der ausschließlich im Unterprojekt arbeitet, kann Yabu im Unterverzeichnis gui starten. Die obige Regel setzt dann voraus, daß "../libs/libpng.so" bereits existiert, andernfalls wird das Ziel nicht erreicht.

Liegen zwischen Haupt- und Unterprojekt mehr als eine Verzeichnisebene, dann funktioniert die Delegation über alle Ebenen hinweg, aber nicht in die dazwischenliegenen Ebenen. Als Beispiel betrachten wir

!project components/libraries/openssl/buildfile.ssl

Im Hauptprojekt würde das Ziel "components/libraries/openssl/clean" an das Unterprojekt delegiert, nicht aber "components/libraries/clean". Umgekehrt würde im Unterprojekt das Ziel "../../../config.h" (als "config.h") an das Hauptprojekt delegiert, nicht aber "../config.h".

Hierarchien von Unterprojekten

Gibt es mehr als ein Unterprojekt, dann läuft die die Delegation eines Ziel unter Umständen über mehrere "Zwischenstationen" ab. Der einfachste Fall, in dem das vorkommen kann, ist ein Hauptprojekt mit zwei Unterprojekten:

!project app/Buildfile
!project lib/Buildfile

Steht nun zum Beispiel in app/Buildfile die Regel

app: $(OBJS) ../lib/libssl.a

dann wird "../lib/libssl.a" zunächst an das Hauptprojekt delegiert. Von dort leitet Yabu das Ziel – dessen Name nunmehr "lib/libssl.a" lautet – schließlich an lib/Buildfile weiter.

Ein weiterer typischer Fall, in dem solche Ketten auftreten, sind mehrstufige Projekthierarchien. Ein Unterprojekt kann nämlich seinerseits Unterprojekte enthalten. So könnte beispielsweise das Ziel "lib/ssl/libssl.a" zuerst an das Unterprojekt lib/Buildfile und von dort an das Unterprojekt ssl/Buildfile delegiert werden.

.21 Programmeinstellungen

» » » »

Das Verhalten von Yabu läßt sich in gewissen Grenzen an die eigenen Bedürfnisse anpassen, und zwar mit sogenannten Programmeinstellungen. Diese steuern unter anderem die Sprache und das Format von Meldungen sowie das Verhalten bei Fehlern. Einstellungen werden beim Start von Yabu festgelegt und gelten dann für den gesamten Programmlauf. Es gibt vier Stellen, an denen Einstellungen auftreten können, und zwar (nach absteigender Priorität geordnet):

  1. Optionen auf der Kommandozeile.

  2. Einstellungen in einem !settings-Abschnitt im Buildfile (des Hauptprojekts, siehe unten!).

  3. Einstellungen in der benutzerspezifischen Konfigurationsadtei $HOME/.yaburc.

  4. Einstellungen in der globalen Konfigurationsdatei yabu.cfg (-g bzw. YABU_CFG_DIR).

Viele Einstellungen können an allen vier Stellen vorkommen. Bei Konflikten gilt die Einstellung mit der höchsten Priorität. Zum Beispiel überstimmt eine Einstellung aus dem Buildfile die entsprechenden Einstellungen aus globaler und benutzerspezifischer Konfigurationsdatei, kann aber durch eine Kommandozeilenoption selbst überstimmt werden. Alle Einstellungen sind optional. Wird eine Einstellung an keiner der vier Stellen festgelegt, dann gilt ein Standardwert. Standardwerte sind weiter unten zu allen Einstellungen angegeben.

Im Falle von Unterprojekten (siehe Unterprojekte: die !project-Anweisung) gilt eine zusätzliche Regel. Yabu wertet nur im Haupt-Buildfile die !settings-Anweisung aus, in den Unterprojekten sind etwaige Einstellungen wirkungslos. Das gilt auch, wenn das Hauptprojekt gar keine Einstellungen enthält.

..1 Kommandozeile

Optionen auf der Kommandozeile beginnen mit "-" und stehen vor den Zielen. Aufeinanderfolgende Optionen dürfen zusammengefaßt werden, also zum Beispiel

yabu -nevv

statt

yabu -n -e -v -v

Optionen, die einer zweiwertigen (ja-/nein-)Einstellung entsprechen, können als Groß- oder Kleinbuchstabe angegegben werden. Zum Beispiel beziehen sich -a und -A auf die gleiche Einstellung. Hierbei gilt die Konvention, daß der Großbuchstabe den Standardwert der Einstellung auswählt, und der Kleinbuchstabe den anderen Wert.

Die meisten Optionen haben eine Entsprechung in der Konfigurationsdatei. In diesem Fall enthält die Tabelle nur einen Verweis auf den folgenden Abschnitt.

-a/-A

Entspricht auto_dependencies = false/true

-c Konfig

Wählt eine globale Konfiguration aus. Siehe Auswahl der Konfiguration

-D Auswahl

Aktiviert die Ausgabe von internen Datenstrukturen zur Fehlersuche. Auswahl ist eine Kombination von Buchstaben, die festlegt, welche Daten Yabu ausgibt. Erlaubt sind "c" (Optionen), "p" (Programmeinstellungen), "r" (Regeln), "t" (Ziele nach Phase 1), "T" (Ziele nach Phase 2), "v" (Variablen) und "A" (alles). Siehe Programmoptionen zur Fehlersuche.

-e/-E

Entspricht echo = true/false

-f Datei

Definiert den Namen der Beschreibungsdatei (Standard: Buildfile)

-g Verzeichnis

Name des globalen Konfigurationsverzeichnisses. Der Standardwert ist durch die Umgebungsvariablen YABU_CFG_DIR festgelegt. Es gibt keine entsprechende Einstellung in yabu.cfg, .yaburc oder Buildfile.

-j/-J

Entspricht use_server = false/true

-k/-K

Entspricht max_warnings=0/-1

-m/-M

Entspricht auto_mkdir = true/false

-n

"Probelauf": Yabu versucht die angegeben Ziele zu erreichen, führt aber die damit verbundenen Skripte nicht aus. Diese Option impliziert -e (Echo ein). Ist -n aktiv, dann liest und benutzt Yabu die Statusdatei, aktualisiert sie jedoch nicht. Auf Autokonfigurationsskripte (siehe Auswahl der Konfiguration) und Include-Skripte (siehe Die .include- und .include_output-Anweisung) hat -n keine Auswirkung; diese Skripte werden immer ausgeführt.

-nn

Wie -n, aber zusätzlich ist die Prüfung der Änderungszeiten außer Kraft gesetzt. Das heißt, Yabu betrachtet alle Ziele als veraltet, selbst wenn die Dateizeit später als die aller Quellen ist. Ein Beispiel:

yabu -nn ZIEL

gibt alle Kommandos aus, um ZIEL zu erreichen, und zwar unabhängig vom aktuellen Kompilierzustand.

-p/-P

Entspricht parallel_build = false/true

-q

Weniger Meldungen. Jedes '-q' erniedrigt verbosity um eins.

-r/R

Entspricht use_yaburc = false/true

-s

Unterdrückt die Verwendung der Statusdatei (siehe Die Statusdatei (Buildfile.state)). Entspricht use_state_file=false (leerer Wert) in .yaburc.

-S Shell

Entspricht shell

-v

Mehr Meldungen. Jedes -v erhöht verbosity um eins.

-V

Ausgabe der Versionsnummer.

-y Algo

Wählt den Algorithmus, mit dem Yabu entscheidet, ob eine Datei bezüglich ihrer Quellen veraltet ist. Algo ist einer der folgenden Werte:

mt

Make-kompatibles Verfahren, basierend auf den Änderungszeiten.

mtid

Änderungszeiten wie Prüfsummen behandeln.

cksum

Prüfsummen.

Weitere Details siehe unter . Der Default ist der zuletzt benutzte (und in Buildfile.state) gespeicherte Algorithmus. Existiert Buildfile.state nicht, dann ist der Default -y mt.

-?

Ausgabe eines Hilfetextes.

..2 Globale Konfigurationsdatei (yabu.cfg)

Wurde beim Start von Yabu mit der Option -g ein globales Konfigurationsverzeichnis gesetzt, dann liest Yabu die Datei yabu.cfg in diesem Verzeichnis. Es gilt als Fehler wenn die Datei nicht existiert. yabu.cfg enthält neben Programmeinstellungen weitere Daten, zum Beispiel Informationen über die verfügbaren Yabu-Server im Netzwerk (siehe Parallel und verteilt: Yabu im Netzwerk). Einstellungen beginnen mit der Zeile

!settings

und haben die Form

//Name//=//Wert//

wobei links und rechts vom Gleichheitszeichen beliebige viele Leerzeichen und Tabs stehen können. Der !settings-Abschnitt endet mit dem Dateieende oder mit dem Beginn eines anderen Abschnitts (also einer Zeile die mit '!' beginnt). Leerzeilen und Kommentarzeilen – beginnend mit '#' – werden ignoriert. Fortsetzungszeilen mit '\' wie im Buildfile sind dagegen nicht erlaubt.

auto_mkdir

Automatisches Erzeugen von Verzeichnissen. Boolean. Default: false. Bestimmt das Verhalten, wenn Yabu eine Datei regenerieren soll, aber das übergeordnete Verzeichnis nicht existiert. Per Default ignoriert Yabu fehlende Verzeichnisse und vertraut darauf, daß das Build-Skript gegebenenfalls das Verzeichnis anlegt. Ist auto_mkdir=true gesetzt, dann erzegt Yabu fehlende Vezeichnisse automatisch. Das funktioniert auch über meherer Ebenen. Soll zum Beispiel "obj/linux/file.o" erzeugt werden, dann würde Yabu die Verzeichnisese "obj" und "obj/linux" bei Bedarf automatisch anlegen. Kommandozeile: -m/-M.

auto_dependencies

Automatisch erzeugte Abhängigkeiten benutzen. Boolean. Default: true. Per Default führt Yabu Autodepend-Skripte aus und benutzt die Ausgaben in späteren Programmläufen (siehe Autodepend-Skripte). Setzt man auto_dependencies=false, dann führt Yabu Autodepend-Skripte nicht aus und ignoriert etwaige gespeicherte Autodepend-Ausgaben. Kommandozeile: -a/-A.

echo

Kommandos ausgeben. Boolean. Default: false. Ist diese Einstellung auf true gesetzt, dann gibt Yabu alle Kommandos vor der Ausführung auf der Standardausgabe aus. In der Ausgabe sind bereits alle Variablen und Platzhalter ersetzt, man sieht also genau das, was die Shell ausführt. Kommandozeile: -e/-E. Siehe auch echo_after_error.

echo_after_error

Gescheiterte Kommandos ausgeben. Boolean. Default: true. Ist echo abgeschaltet, dann kann man mit dieser Einstellung festlegen, daß Yabu Kommandos trotzdem ausgibt, nachdem ein Fehler aufgetreten ist. Ist echo eingeschaltet, dann hat echo_after_error keine Bedeutung.

highlight_e

Hervorhebung von Fehlermeldungen. String der Form "BEGIN|END". Default: "".

Der Wert muß, wenn er nicht leer ist, die Form "BEGIN|END" haben, wobei BEGIN und END Folgen beliebiger Zeichen außer "|" sind. Auch Steuerzeichen sind erlaubt; es gilt die in C übliche Syntax, und zusätzlich steht "\e" für ESC (siehe das Beispiel unten).

Yabu gibt BEGIN vor und END nach jeder Fehlermeldung aus. Damit kann man, ein geeigentes Terminal vorausgesetzt, Fehlermeldungen farblich hervorheben, indem man für BEGIN und END geeignete Sequenzen von Steuerzeichen wählt. Ein Beispiel:

highlight_e=\e[1;31m|\e[0m

highlight_w

Hervorhebung von Warnungen. String. Default: "". Wie highlight_e, aber für Warnungen. Warnungen führen im Unterschied zu Fehlermeldungen nicht zum Programmabbruch. Beispiele sind die Ausgabe nach einen Fehler bei der Skriptausführung oder die Meldung, daß ein Yabu-Server unerreichbar ist. Ist highlight_w leer und highlight_e gesetzt, dann verwendet Yabu highlight_e auch für Warnungen.

highlight_0

Hervorhebung von informativen Meldungen. String. Default: "". Wie highlight_e, aber für informative Meldungen.

highlight_s

Hervorhebung von Skripten. String. Default: "". Wie highlight_e, gilt aber für die Ausgabe von Skripten mit Option -e oder nach einem Fehler.

locale

Sprache und Zeichensatz. String. Default: "". Überstimmt, falls gesetzt, den Wert von LC_MESSAGES, aus dem Yabu die Sprache und den Zeichensatz für Meldungen ableitet. Die Sprache bestimmt Yabu aus den ersten beiden Zeichen des Wertes, zum Beispiel bewirkt "locale=de_DE.utf8" die Ausgabe in Deutsch. Yabu unterstützt zwei Zeichensätze: ISO8859-15 und UTF-8. UTF-8 wird immer dann benutzt, wenn locale die Zeichenfolge "utf" oder "UTF" enthält.

max_output_lines

Maximale Anzahl von Ausgabezeilen pro Kommando. Integer. Standardwert: 0. Ein Wert N>0 begrenzt die Ausgabe von Skripten auf die ersten N und die letzten drei Zeilen. Alle dazwischen liegenden Zeilen werden unterdrückt. Besteht das Skript aus mehr als einer Zeile, dann gilt die Begrenzung der Ausgabe individuell für jede Zeile {bzw.} für jeden mit "{"..."}" gebildeten Block.

Ist max_output_lines=0, dann wird die Ausgabe nicht begrenzt.

Ist max_output_lines=-1, dann gilt ebenfalls keine Beschränkung der Ausgabe. Der Unterschied zu max_output_lines=0 ist, daß Yabu die Ausgaben zwischenspeichert und erst nach Beendigung des Kommandos ausgibt. Bei der parallelen Auführung von Skripten kann das nützlich sein, denn man vermeidet so, daß sich Ausgaben mehrerer Skripte vermischen. Diesen willkommenen Nebeneffekt hat man auch bei max_output_lines>0.

max_warnings

Verhalten bei Warnungen. Integer (-1..unbegrenzt). Default: -1. Legt fest, wieviele nicht-kritische Meldungen (Warnungen) Yabu ausgibt, bevor die Programmausführung abbricht. Wichtigstes Beispiel für eine Warnung ist die Ausgabe, nachdem Yabu wegen eines Fehlers im Skript ein Ziel nicht erreicht hat.

Der Standardwert -1 bedeutet "kein Limit", das heißt, Yabu versucht nach einem gescheiterten Skript, alle verbleibenden Ziele zu erreichen. Der Wert 0 führt zum sofortigen Abbruch bei einer Warnung. Werte N>0 zögern den Abbruch bis zur (N+1)-ten Warnung hinaus.

Kommandozeile: -k/-K.

parallel_build

Skripte parallel ausführen. Boolean. Default: true. Abschalten dieser Option bewirkt, daß Yabu Skripte nicht parallel sondern nacheinander ausführt. Das gilt sowohl für Skripte, die auf verschiedene Server verteilt würden, als auch für die parallele Ausführung von Skripten auf dem gleichen Server (max>1 in yabu.cfg).

shell

Interpreter für Skripte. String. Default: "/bin/sh". Legt den Namen des Programms fest, welches Yabu zur Ausführung eines Skriptes startet. Das Programm muß die Syntax "-c Skript" beherrschen. Die Einstellung gilt für alle Skripte aus, bei denen kein Interpreter explizit festgelegt ist ("#!"-Syntax, siehe Skripte). Kommandozeile: -S.

use_state_file

Name der Statusdatei. Boolean. Default: true. Die Einstellung "false" bewirkt, daß Yabu keine Statusdatei (siehe Die Statusdatei (Buildfile.state)) benutzt. Kommandozeile: -s.

use_server

Yabu-Server benutzen, falls verfügbar. Details siehe Lokale Ausführung erzwingen mit 'use_server'. Boolean. Default: true. Schaltet man diese Option aus, dann verarbeitet Yabu Skripte nur noch lokal, ohne einen Server zu kontaktieren. Ein weiterer Effekt von use_server=no ist, daß Yabu Skripte nicht parallel ausgeführt.

use_yaburc

Persönliche Konfiguration in $HOME/.yaburc benutzen. Boolean. Default: true. Diese Einstellung ist nur in der globalen Konfigurationsdatei sinnvoll (in .yaburc ist sie erlaubt, hat aber verständlicherweise keine Wirkung). Der Wert false bewirkt, daß Yabu die benutzerspezifischen Einstellungen nicht verwendnet. Gegebenenfalls kann der der Benutzer beim Aufruf mit der Option -R die Auswertung von .yaburc erzwingen.

verbosity

Umfang von Meldungen. Integer. Default: 0. Legt fest, wieviele Meldungen Yabu ausgibt. Höhere Werte erzeugen mehr Meldungen, was bei der Fehlersuche hilfreich sein kann. Ist der Wert kleiner als 0, werden auch Fehlermeldungen unterdrückt.

..3 Die Datei $HOME/.yaburc

Das Format ist wie bei yabu.cfg, jedoch kann die einleitende "!settings"-Zeile entfallen. Andere Abschnitte außer !settings, die in yabu.cfg vorkommen können, sind nicht erlaubt.

..4 !settings-Anweisung

Mit einem !settings-Abschnitt kann man im Buildfile gewisse Einstellungen vornehmen, die global für alle Ziele und alle Konfigurationen gelten. Die allgemeine Syntax ist

!settings
   Name = Wert
   ...

wobei Name eines der oben beschriebenen beschriebenen Schlüsselwörter sein muß. Im Buildfile angegebene Einstellungen haben Priorität vor yabu.cfg und $HOME/.yaburc, können aber durch die Kommandozeile überstimmt werden.

Man beachte, daß daß Yabu im !settings-Abschnitt keine Ersetzung von (Phase-2-)Variablen vornimmt. Falls nötig, kann man hierfür Präprozessor-Variablen verwenden. Zum Beispiel würde

SHELL=/bin/tcsh
!settings
  shell=$(SHELL)

nicht funktionieren, wohl aber

.shell=/bin/tcsh
!settings
  shell=$(.shell)

.22 Die Statusdatei (Buildfile.state)

Um den Programmablauf zu optimieren, speichert Yabu verschiedene Informationen, die über die reinen Änderungszeiten hinausgehen, in einer speziellen Statusdatei. Der Name der Statusdatei entsteht aus dem Namen des Buildfiles durch Anhängen von ".state". Im Normalfall (ohne -f) ist er also "Buildfile.state".

Ist beim Start von Yabu diese Datei vorhanden, dann liest Yabu sie ein und wertet alle enthaltenen Informationen aus. Während des Programmlaufes werden in der Regel Statusinformationen neu erzeugt oder geändert. Yabu schreibt diese beim Programmende in die Statusdatei zurück. Das geschieht allerdings nur bei einem regulären Ende, nicht beim Abbruch im Fehlerfall. Ebenso unterbleibt die Aktualisierung der Statusdatei, wenn man Yabu mit der Option -n startet.

Yabu verwendet bei jedem Programmlauf stets nur eine Statusdatei. Für untergeordnete Buildfiles, die mittels .include eingebunden sind, wird keine separate Statusdatei angelegt.

Die Daten in Buildfile.state sind nicht essentiell, man kann also die Datei jederzeit gefahrlos löschen. Beim nächsten Lauf von Yabu werden die Statusdaten dann neu generiert.

Dateiformat

Die Statusdatei ist zeilenweise organisiert, jede Zeile besteht aus einer Liste von Argumenten, die durch TABs getrennt sind. Das erste Argument in jeder Zeile bestimmt den Typ des Eintrags, die übrigen Argumente sind typspezifisch. Drei Typen sind definiert: yabu, tsa und target.

yabu

Dateikopf (1. Zeile der Datei). Dient zur Erkennung gültiger Statusdateien. Enthält die erste Zeile keine gültiges yabu-Element mit richtiger Versionsnummer, dann verwirft Yabu die Statusdatei und erzeugt einen neue.

tsa

Enthält den ausgewählten Algorithmus zum Vergleich von Dateien (siehe ).

target

Die Zeile enthält Angaben zu einem Ziel, und zwar (in dieser Reihenfolge):

default_targets

Enthält die per Option -d als Standard gesetzten Ziele.

.23 Kommandozeile und Environment

Die allgemeine Syntax beim Aufruf von Yabu ist

yabu Optionen [-c Konfig] [-f Datei] [Ziel] ...
yabu Optionen [-c Konfig] [-f Datei] .

Alle Optionen sind in Kommandozeile beschrieben. Alle restlichen Argumente sind Ziele, die Yabu in der angegebenen Reihenfolge zu erreichen versucht.

Exit-Status

Yabu gibt einen der folgenden Werte zurück:

0

Alle Ziele wurden erreicht.

1

Mindestens ein Ziel wurde nicht erreicht.

2

Ein nicht ignorierbarer Fehler ist aufgetreten, Yabu wurde abgebrochen.

Konfigurationsdateien

Existiert im Homeverzeichnis die Datei .yaburc, dann liest Yabu diese ein und interpretiert den Inhalt wie einen !settings-Abschnitt im Buildfile (siehe im Abschnitt Programmeinstellungen). Die !settings-Zeile fehlt allerdings, und die Einrückung der Zeilen ist nicht relevant.

Environment

LANG, LC_MESSAGES

Sprache und Zeichensatz für Meldungen. Enthält die LC_MESSAGE-locale die Zeichenfolge "utf" oder "UTF", dann benutzt Yabu für Meldungen den UTF-8, ansonsten ISO-8859-15.

HOME

Homeverzeichnis. Hier erwartet Yabu die persönlichen Einstellungen des Benutzers in der Datei ".yaburc".

LOGNAME, USER

Benutzerkennung, den Yabu für die Authentisierung gegenüber dem Yabu-Server verwendet. Der Wert muß mit der tatsächlichen Benutzerkennung übereinstimmen, ansonsten scheitert die Anmeldung.

YABU_CFG_DIR

Gibt das globale Konfigurationsverzeichnis an. Der Wert kann bei Bedarf durch die Kommandozeilenoption -g überschrieben werden.