Yabu in der Praxis Tipps und Tricks

5 Yabu in der Praxis – Tipps und Tricks

Aliase nicht als Quellen verwenden

Wie ihr Name bereits zum Ausdruck bringt, sind Alias-Ziele dazu gedacht, dem Anwender von Yabu die Arbeit zu erleichtern. Ein Alias macht häufig benötigte Ziele oder Kombinationen von Zielen unter einem kurzen, einprägsamen Namen verfügbar. In solchen Fällen tritt der Alias im Buildfile immer als Ziel auf der linken Seite einer Regel auf. Zum Beispiel:

debug:: $(PROGRAMS:*=debug/*)

Ein Alias kann aber auch als Quelle vorkommen, wie das folgende Beispiel zeigt:

all:: $(PROGRAMS)
tar:: all
  tar tf all.tar $(PROGRAMS)

Die Verwendung von Aliasen in dieser Weise ist aber nicht unbedenklich. Yabus Algorithmus basiert auf Änderungszeiten von Dateien, ein Alias ist aber per Definition keine Datei. Tatsächlich gilt ein Alias immer als veraltet, und damit auch alle davon abhängigen Ziele. Im obigen Beispiel würde mit jedem Aufruf von "yabu tar" die TAR-Datei neu erstellt, selbst wenn sich keine der in $(PROGRAMS) aufgeführten Dateien geändert hat.

Deshalb sollte man wenn möglich als Quellen keine Aliase verwenden. Das läßt sich oft durch Einführen einer weiteren Regel erreichen. Das obige Buildfile könnte man etwa so umschreiben:

all:: $(PROGRAMS)
tar:: all.tar
all.tar: $(PROGRAMS)
  tar tf all.tar $(PROGRAMS)

"Störende" Quellen aus $(*) entfernen

Manchmal benötigt man in einem Skript alle Quellen bis auf eine oder einige wenige. Ein Beispiel:

prog: defs.h src1.c src2.c src3.c
  cc -o $(0) src1.c src2.c src3.c

Da die Datei "defs.h" nicht mitkompiliert werden darf – offenbar wird sie von srcX.c per #include benutzt – kann man $(*) nicht verwenden. In diesem einfachen Beispiel läßt sich das leicht durch Verwendung einer Variablen umgehen:

SRCS=src1.c src2.c src3.c
prog: defs.h $(SRCS)
  cc -o $(0) $(SRCS)

Es bleibt aber eine unschöne Wiederholung der Quellen im Skript, und in komplexeren Fällen wird die Regel dadurch unübersichtlicher.

Eine elegante Lösung besteht darin, die "störenden" Quellen in eine zweite Regel zu verlagern:

prog: src1.c src2.c src3.c
  cc -o $(0) $(*)
prog: defs.h

Da die zweite Regel kein Skript hat, fügt sie dem Ziel "prog" lediglich eine weitere Quelle ("defs.h") hinzu – sie verändert aber nicht den Wert von $(*).

Mehrere Regeln für ein Ziel

Gelegentlich kommt es vor, daß Dateien mit der gleichen Namenserweiterung aus verschiedenen Quellen erzeugt werden. Zum Beispiel könnte ein Programm aus C- und FORTRAN-Quellen kompiliert werden. Im Buildfile wünscht man sich dann folgende Regeln

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

Das scheitert jedoch daran, daß Yabu für ein gegebenes Ziel nur eine Regel auswählt, und dabei nur das Ziel selbst (nicht die Quellen) betrachtet. Für "abc.o" würden beide Regel zutreffen, und ein Fehler wäre die Folge.

Das gleiche Problem tritt auf, wenn man Dateien aus verschiedenen Quellverzeichnissen kompilieren, die Ergebnisse aber in einem gemeinsamen Verzeichnis ablegen will. Auch hier führt der naheliegende Ansatz

build/%.o: unix/%.c
    cc -o -c $(0) $(1)
build/%.o: general/%.c
    cc -o -c $(0) $(1)

zu einem Fehler, da Yabu beide Regeln als gleichwertig betrachtet.

Die Lösung besteht darin, die "Herkunft" der Dateien im Dateinamen zu kodieren. Die obigen Regeln für C- und FORTRAN-Quellen könnte man zum Beispiel so schreiben:

%_c.o: %.c
    cc -o -c $(0) $(1)
%_f.o: %.f
    g77 -o -c $(0) $(1)

Spezialisierung von Prototypregeln

Für ein gegebenes Ziel wählt Yabu unter den Regeln mit Skripte höchstens eine aus; alle anderen bleiben unberücksichtigt. Das kann man dazu benutzen, allgemeine Prototypregeln für Spezialfälle mit einer anderen Regel zu "überschreiben". Hat die betreffende Regel kein Skript, dann kann man sich oft damit behelfen, ein "Dummy"-Skript hinzuzufügen.

Beispiel: die allgemeine Regel

all-%-%:: client-%1-%2 server-%1-%2 testsuite-%1-%2

soll für den Spezialfall "%1=win23" zu

all-win32-%:: client-win32-%2

abgewandelt werden. Das Hinzufügen der zweiten Regel hat allerdings nicht den gewünschten Effekt: beispielsweise würde Yabu für das Ziel "all-win32-gcc" immer noch beide Regeln berücksichtigen, so daß die zweite Regel gar nichts bewirkt.

Schreibt man aber

all-%-%:: client-%1-%2 server-%1-%2 testsuite-%1-%2
  true
all-win32-%:: client-win32-%2
  true

dann greift der in beschriebene Auswahlmechanismus. Yabu würde nun für "all-win32-gcc" die zweite Regel auswählen und die erste Regel ignorieren.

Optionale Makroargumente: .foreach als Ersatz für .if

Makroargumente, die eine Liste von Werten enthalten, führen manchmal zu Fehlern wenn die Liste leer ist. Yabu kennt zwar keine .if-Anweisung, mit der man auf ein leeres Argument reagieren könnte, aber in manchen Fällen kann man sich auch mit einer .foreach-Schleife behelfen.

Als Beispiel betrachten wir einen Codegenerator, der aus einer Modelldatei eine oder mehrere C-Quelldateien und Headerdateien erzeugt. Die entsprechenden Regeln sollen mit Hilfe eines Makros erzeugt werden:

.define generate model srcs hdrs

$(.srcs): $(.model)
    gen_impl $(1)
$(.hdrs): $(.model)
    gen_decl $(1)
$(.srcs:*.c=*.o): $(.hdrs)

.enddefine

Ein typischer Aufruf wäre

.generate aaa.mod
    srcs=aaa1.c aaa2.c aaa3.c
    hdrs=aaa1.h aaa2.h

Dies würde Regeln zur Erzeugung der C- und Header-Dateien liefern, und außerdem die Kompilate der C-Dateien von allen Header-Dateien abhängig machen.

Ein Problem tritt auf, wenn ein Modell gar keine Header erzeugt. Der Aufruf

.generate bbb.mod
    srcs=bbb1.c bbb2.c
    hdrs=

führt zu einem Syntaxfehler, denn die zweite, von .generate erzeugte Regel lautet nun:

 : bbb.mod             <----- SYNTAXFEHLER
    gen_decl $(1)

Die Lösung besteht darin, eine foreach-Schleife zu verwenden:

.define generate model srcs hdrs

$(.srcs): $(.model)
    gen_impl $(1)
.foreach h $(.hdrs)
$(.h): $(.model)
    gen_decl $(1)
.endforeach
!serialize $(.hdrs)
$(.srcs:*.c=*.o): $(.hdrs)

.enddefine

Ein leerer Wert von $(.hdrs) ist nun kein Problem mehr: die .foreach-Schleife wird in diesem Fall gar nicht ausgeführt, und es entsteht keine unvollständige Regel. Die !serialize-Anweisung ist dann wichtig, wenn $(.hdrs) mehr als ein Element enthält; sie verhindert, daß Yabu "gen_decl" parallel ausführt. In der ursprünglichen Fassung ohne .foreach braucht man keine !serialize-Anweisung, denn dort stehen alle Header-Dateien in einer Regel und bilden automatisch eine Gruppe.