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.
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:
Einlesen der Datei und Vorverarbeitung. Siehe Dateiformat und Vorverarbeitung.
Ersetzen von Präprozessor-Variablen (siehe Präprozessor-Variablen) und Verarbeiten von Anweisungen (siehe Die .include- und .include_output-Anweisung und Die .foreach-Anweisung) und Makros (siehe Makros).
Verarbeitung von Yabu-Anweisungen wie !settings
, !options
,
!configure
und !export
.
Erzeugung des Regelwerks.
Einlesen der Statusdatei (siehe Die Statusdatei (Buildfile.state)).
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.
Dateiformat
Bevor Yabu ein Buildfile interpretiert, durchläuft der Inhalt eine Vorverarbeitung, die aus drei Schritten besteht:
Leerzeichen und Tabs am Ende einer Zeile werden abgeschnitten.
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
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.
Kurz und knapp
Datei einfügen:
.include rules.yabu <--- Name relativ zum Buildfile .include <defaults.yabu> <--- Name relativ zu $YABU_CFG_DIR/include
Skript ausführen, falls die Datei nicht existiert:
.include local.yabu cvs update local.yabu
Ausgabe eines Skriptes in das Buildfile einfügen:
.include_output Kommandos...
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:
Buildfile
../common/Buildfile.glob
../common/arch/Buildfile.i386
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.
Kurz und knapp
Syntax (Beispiel):
.foreach i server client Text $(.i) .endforeach
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.
Kurz und knapp
Präprozessor-Variable definieren:
.var=Wert der Variablen
Wert der Variablen: $(.var)
Präprozessor-Variablen werden sofort ersetzt. Eine Variable muß vor ihrer Verwendung definiert werden.
Beachte den Unterschied zu gewöhnlichen Variablen (ohne ".")!
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.
Kurz und knapp
Alle Dateien, die mit .c enden: $(.glob:*.c)
Die zugehörigen .h-Dateien: $(.glob:*.c=*.h)
Platzhalter "*" unf "**" funktionieren wie bei Yabu-Variablen, nicht wie von der Shell gewohnt!
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.
Kurz und knapp
Alle Dateien in include mit der Endung .h oder .hpp:
$(.find(include)(**.h **.hpp))
Alle .h-Dateien in include oder Unterverzeichnissen davon:
$(.find(include/)(**.h))
Alle Dateien in src und Unterverzeichnissen, ausgenommen CVS-Dateien:
$(.find(lib/)(** !**/CVS/* !**/CVS/*.*))
Verzeichnis nur durchsuchen, wenn es existiert:
$(.find(?include/)(**.h))
Muster beziehen sich immer auf den gesamten Pfad.
$(.find)
und $(.dirfind)
werden bei Einlesen des Buildfiles ausgewertet.
Das Ergebnis enthält niemals symbolische Links oder spezielle Dateien.
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.
Kurz und knapp
Makrodefinition:
.define makroname arg1 arg2 arg3 ... # Text, Verweise auf Argumentwert mit $(.arg1) .enddefine
Makro benutzen:
.makroname wert1 wert2 wert3 ...
oder
.makroname arg1=wert1 arg2=wert2 arg3=wert3 ...
Standardwerte für Argumente festlegen:
.define makroname arg1(default1) arg2(default2) arg3(default3) ...
Lokale Makros definieren: .define local:name ...
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:
%-Platzhalter arbeiten implizit, das heißt, Yabu erzeugt die passende Regel bei Bedarf. Ein Makro dagegen wird erst dann wirksam, wenn es explizit verwendet wird.
Eine Prototypregel kann – für einen gegebenen Satz von Platzhalterwerten – stets nur eine einzige Regel erzeugen. Ein Makro kann dagegen beliebig viele Regeln enthalten.
Prototypregeln folgen einem starren Schema. Die erzeugte Regel leitet sich vollständig aus dem Namen des Ziels ab; andere Variationen sind nicht möglich. Makros dagegen haben beliebige Parameter, deren Werte man beim Aufruf des Makros unabhängig voneinander frei wählen kann.
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".
Kurz und knapp
Variablen definieren:
OBJS=aaa bbb OBJS+=ccc <----- aaa bbb ccc OBJS?=bbb ddd <----- aaa bbb ccc ddd
Variablen benutzen: $(OBJS)
Verwendung einer undefinierten Variablen ist ein Fehler.
Variablen werden ersetzt, nachdem das gesamte Buildfile verarbeitet ist. Die Reihenfolge von Variablendefinitionen spielt keine Rolle.
Transformationsregeln:
OBJS=aaa.o bbb.o lib.a $(OBJS:*.o=*.c) <----- FEHLER bei "lib.a" $(OBJS?:*.o=*.c) <----- aaa.c bbb.c lib.a $(OBJS!:*.o=*.o) <----- aaa.c bbb.c
Alternatives Platzhalterzeichen (an Stelle von "*"):
$(OBJS^:^.o=^.c)
Mehrere Platzhalter:
$(OBJS:*.*=*2/*1.c) <----- aaa.o -> o/aaa.c
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.
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!
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.
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.
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.
Kuz und knapp
Optionen definieren:
!options ssl with_x11 os(linux win32 netbsd solaris aix)
Bedingte Zuweisung:
CFLAGS[solaris] += -I/usr/sfw/include LFLAGS[solaris+inet] += -lsocket -lnsl
oder
!configuration [solaris] CFLAGS += -I/usr/sfw/include LFLAGS[inet] += -lsocket -lnsl
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 parametrisiert man die vorkommenden Varianten mit Hilfe von Optionen und Konfigurationen. Beispielsweise könnte man eine spezielle Debug-Konfiguration für Testzwecke definieren.
Unterschiede zwischen den Varianten werden auf unterschiedliche Werte gewisser Variablen zurückgeführt. Bei C-Programmen ist es zum Beispiel üblich, Compileroptionen über der Variablen CFLAGS festzulegen. Ihr Wert läßt sich für jede Konfiguration individuell festlegen.
Jedes zu erreichende Ziel hat eine bestimmte Konfiguration. Sie bestimmt die Werte der konfigurationsabhängigen Variablen, aber auch, welche Regeln Yabu zur Erreichung des Ziels verwendet.
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").
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:
Alle Optionen sind undefiniert oder ausgeschaltet
Genau eine Option ist eingeschaltet, alle anderen sind ausgeschaltet
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)
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
Aus A⊆B und B⊆A folgt A=B
A⊆B und B⊆C impliziert A⊆C
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). |
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.
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.
Kurz und knapp
Regeln für normale und Alias-Ziele:
ziel: quelle1 quelle2 ... script alias:: quelle1 quelle2 ... script
Sequentelle Bearbeitung der Quellen erzwingen:
ziel: quelle1 , quelle2 , ...
Konfigurationsauswahl in einer Regel:
ziel: [KONFIG] quelle1 quelle2 ...
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:
Die (nicht eingerückte) Kopfzeile definiert die Abhängigkeit eines oder mehrerer Ziele von einer Reihe von Quellen. Sie hat zwei mögliche Formen:
Ziel ... : [Konfig] Quelle ... Ziel ... :: [Konfig] Quelle ...
Sowohl die Konfigurationsauswahl [Konfig] – siehe unten! – als auch die Liste der Quellen können entfallen, aber die Regel muß mindestens ein Ziel haben. Beispiel: Die Zeile
file.o : file.c
definiert, daß file.o
von file.c
abhängt.
Alle Elemente der Kopfzeile werden durch eine beliebige Kombination
von Leerzeichen und TABs getrennt.
Die Verwendung von "::" kennzeichnet alle Ziele als Alias (siehe unten).
Alle auf die Kopfzeile folgenden, eingerückten Zeilen bilden das Skript. Es enthält die Kommandos, mit denen das Ziel bei Bedarf aus den Quellen regeneriert werden kann. Beispiel:
file.o : file.c cc -o file.o file.c
Das Skript ist optional. Fehlt es, dann dient die Regel ausschließlich zur Definition einer Abhängigkeit.
Ein (optionales) Autodepend-Skript (siehe Autodepend-Skripte). Dieses beginnt mit der ebenfalls eingerückten Zeile "[auto-depend]" und umfaßt wie das Skript alle folgenden eingerückten Zeilen. Beispiel:
file.o : file.c cc -o file.o file.c [auto-depend] cc -MM -c file.c
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.
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:
Alle passenden Regeln ohne Skript tragen ihre rechte Seite zur Liste der Quellen bei.
Aus den passenden Regeln mit Skript wählt Yabu eine aus und ignoriert alle übrigen. Die ausgewählte Regel steuert ebenfalls ihre rechte Seite zur Liste der Quellen bei und bestimmt außerdem das Skript, das Yabu ausführt um das Ziel zu erreichen.
Die Auswahl basiert auf einer Punktezahl, die sich aus dem Zielmuster wie folgt errechnet: jedes Zeichen außer "%" zählt +50, und jedes "%" zählt -1. Die Regel mit der höhsten Punktzahl wird benutzt. Mit anderen Worten: Yabu bevorzugt Regeln, deren Zielmuster mit dem gegebenen Ziel an möglichst vielen Stellen exakt übereinstimmt und, als zweites Kriterium, Regeln mit möglichst wenigen %-Platzhaltern.
Gibt es zwei oder mehr Regeln mit maximaler Punktezahl, tritt ein Fehler auf. Zum Beispiel würde Yabu im Buildfile
prog.o: prog.c ... io_%.o: io_%.c ... %.o: %.c ...
für das Ziel "prog.o" die erste Regel auswählen (300 Punkte), und für "io_file.o" die zweite (249 Punkte gegenüber 99 für Regel 3). Die Anzahl der Quellen oder der Inhalt des Skriptes spielt für die Regelauswahl keine Rolle.
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).
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.
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.
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.
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:
Die Regel ist nur anwendbar, wenn die von ihr ausgewählte Konfiguration zur aktuellen Konfiguration kompatibel ist. Ist das nicht der Fall, wird die Regel für das gegebene Ziel gar nicht erst in Betracht gezogen.
Wenn Yabu die Regel tatsächlich verwendet, wird die ausgewählte Konfiguration aktiviert, und zwar sowohl beim Ermitteln der Quellen als auch für die Ausführung des Skripts. Mit anderen Worten: alle Variablen in der Liste der Quellen und im Skript nehmen die Werte aus der angegebenen Konfiguration an.
Die neue Konfiguration vererbt sich aber nicht auf die Quellen. Wenn Yabu versucht, die Quellen zu erreichen, gilt also zunächst wieder die Startkonfiguration, und der Regelauswahlprozeß beginnt von vorne.
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:
Yabu wendet den Inhalt der [...] auf die aktuelle Konfiguration an. Mit anderen Worten, die Option "with_ssl" wird eingeschaltet.
Etwaige Variablen in der Quellenliste (kommt im Beispiel nicht vor) werden ersetzt. Dabei gilt die ausgewählte Konfiguration.
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.
Yabu führt das Skript aus. Hierbei ist die ausgewählte Konfiguration aktiv. Das könnte zum Beispiel den Wert von $(LIBS) beeinflussen.
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.
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.
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:
Ist eine Quelle oder mehrere Quellen unerreichbar, wird das Ziel ebenfalls als unerreichbar markiert und fällt damit heraus.
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:
Das Ziel ist ein Alias (Aliase gelten immer als veraltet).
Eine Quelle ist jüngeren Datums als das Ziel.
Die Regel hat sich seit dem letzten Aufruf von Yabu geändert.
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.
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).
Um Festzustellen, ob ein Ziel veraltet in Bezug auf seine Quellen ist, kann Yabu drei verschiedene Verfahren einsetzen.
Änderungszeiten,
Änderungszeiten wie Prüfsummen behandeln und
echte Prüfsummen.
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.
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:
Das Ziel war bereits erreicht, d.h. es existierte bereits und war auch nicht veraltet in Bezug auf seine Quellen.
Das Ziel wurde erneut erreicht, das zugehörige Skript endete ohne 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.
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
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):
Yabu führt das Skript aus, weil abc.h bezüglich abc.x veraltet ist.
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:
Für jedes Ziel wurde in Phase 2 die gleiche Regel mit Skript ausgewählt (siehe ).
Anzahl und Inhalt der '%'-Platzhalter ist identisch.
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.
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.
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.
Kurz und knapp
Bis zu 9 '%'-Platzhalter sind erlaubt
'%' und '%%' funktionieren wie '*' und '**' bei Variablen.
Platzhalter werden in den Quellen und im Skript ersetzt.
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%".
Kurz und Knapp
$(0) steht für das Ziel
$(1), $(2), ... stehen für die Quellen
$(*) ist die Liste aller Quellen
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.
Kurz und knapp
Skripte werden wie bei Make durch Einrückung markiert.
Mehrzeilige Skripte mit "{" ... "}"
Alternativ: mehrzeilige Skripte mit vorangestelltem "|"
target: source |command |command...
Skript-Interpreter ggf. in der ersten Zeile angeben:
target: source #!/bin/perl Perl-Kommandos...
Skripte erben im allgemeinen nicht das Environment des Benutzers.
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:
Das Skript wird immer als ganzes und niemals zeilen- oder blockweise ausgeführt. Die oben beschriebene Blockbildung mit "{" und "}" kann nicht benutzt werden.
Der Interpreter erhält als (einziges) Argument den Namen einer temporären Datei, welche das auszuführende Skript – ohne die erste Zeile – enthält.
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:
Alle Systemvariablen (siehe Systemvariablen) werden mit dem Präfix "YABU" in die Skriptumgebung exportert. Zum Beispiel steht im Skript $(_TIMESTAMP) als $YABU_TIMESTAMP zur Verfügung.
Wird das Skript durch einen Yabu-Server im Netzwerk ausgeführt, dann beziehen sich die vier Systemvariablen MACHINE, SYSTEM, RELEASE und HOSTNAME auf den ausführenden Server. Die übrigen Systemvariablen wie TIMESTAMP, CWD und auch PID (!) werden dagegen durch Yabu selbst festgelegt und sind für alle Skripte identisch.
YABU_CHOST ist der Name des Rechners, auf dem der steuernde Yabu-Prozeß läuft.
YABU_TARGET ist der Name des zu erreichenden Zieles.
YABU_CFG_DIR ist der mit -g bzw. über die Umgebungsvariable YABU_CFG_DIR definierte Name des globalen Konfigurationsverzeichnisses (siehe [cluster]).
LOGNAME, USER und TERM werden aus dem Environment des Benutzers übernommen.
PATH=/usr/bin:/bin
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.
Kurz und knapp
Syntax für Autodepend-Skripte:
Ziel: Quellen ... [auto-depend] Kommando(s)
Ausführung erfolgt nach dem Build-Skript
Standardausgabe des Autodepend-Skriptes hat die Form "Ziel: Quelle ..."
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.
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))
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 "_".
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?"
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.
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.
Kurz und knapp
Syntax (Beispiel):
!project libs/ssl/Buildfile
Delegation von Zielen in beide Richtungen:
"libs/ssl/XXX" in Buildfile wird zu "XXX" in libs/ssl/Buildfile
"../../XXX" in Buildfile wird zu "XXX" in Buildfile
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.
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):
Optionen auf der Kommandozeile.
Einstellungen in einem !settings-Abschnitt im Buildfile (des Hauptprojekts, siehe unten!).
Einstellungen in der benutzerspezifischen Konfigurationsadtei
$HOME/.yaburc
.
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.
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 |
-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 |
-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 |
-k/-K |
Entspricht |
-m/-M |
Entspricht |
-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 |
-q |
Weniger Meldungen. Jedes '-q' erniedrigt |
-r/R |
Entspricht |
-s |
Unterdrückt die Verwendung der Statusdatei (siehe Die Statusdatei (Buildfile.state)).
Entspricht |
-S Shell |
Entspricht |
-v |
Mehr Meldungen. Jedes -v erhöht |
-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:
Weitere Details siehe unter .
Der Default ist der zuletzt benutzte (und in Buildfile.state) gespeicherte
Algorithmus.
Existiert Buildfile.state nicht, dann ist der Default |
-? |
Ausgabe eines Hilfetextes. |
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 |
Gescheiterte Kommandos ausgeben.
Boolean. Default: true.
Ist |
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_0 |
Hervorhebung von informativen Meldungen.
String. Default: "".
Wie |
highlight_s |
Hervorhebung von Skripten.
String. Default: "".
Wie |
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 Ist |
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_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. |
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.
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)
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):
Name
Konfiguration, in der das Ziel zuletzt erreicht wurde
Zeitpunkt, an dem das Ziel zuletzt erreicht wurde (Format: zwei Hexadezimalzahlen, durch '.' getrennt. Die erste Zahl enthält die Zeit in Sekunden seit 1970, die zweite, sofern das Betriebssystem dies unterstützt, den Sekundenbruchteil in ns).
Identifikation der Regel, über die das Ziel zuletzt erreicht wurde. Yabu verwendet diesen Wert, um nach einer Änderung des Buildfiles die betroffenen Ziele zu aktualisisieren.
Liste der Quellen
default_targets
Enthält die per Option -d
als Standard gesetzten Ziele.
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:
Alle Ziele wurden erreicht.
Mindestens ein Ziel wurde nicht erreicht.
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. |