41. make I

Das Programm make hilft, Projekte zu verwalten. In seinen Steuerdateien, den Makefiles, steht, wie Zieldateien oder targets aus Quelldateien oder prerequisites werden. Make kann damit auch herausfinden, ob ein target neu erzeugt werden muss.

Im einfachsten Fall sieht das so aus:

edelstring.o: edelstring.c edelstring.h
  cc -Wall   -c -o edelstring.o edelstring.c

Wichtig: Der Whitespace vor dem cc ist ein Tab.

Die Bedeutung dieser Regel ist: edelstring.o (das target, vor dem Doppelpunkt der ersten Zeile) ist abhängig von edelstring.c und edelstring.h (den prerequisites oder dependencies für edelstring.o). Für make bedeutet das, dass – wenn es beschließt, edelstring.o überhaupt zu brauchen – es zunächst mal nachsieht, ob es edelstring.o schon gibt. Wenn das nicht so ist, muss es auf jeden Fall gebaut werden. Wenn es das aber gibt, prüft es zunächst, ob alle prerequisites älter sind als edelstring.o – Sinn dieser Prüfung ist, dass, wenn sie erfüllt ist (und nichts komisches mit der Rechneruhr passiert ist), das Ergebnis des Compilerlaufs jetzt nicht verschieden vom letzten Compilerlauf sein dürfte, denn der hat ja etwa zum Zeitpunkt des Timestamps von edelstring.o und damit nach der letzten Änderung sowohl von edelstring.c als auch von edelstring.h stattgefunden – und nichts anderes hat (nach diesem Modell) Einfluss auf das Ergebnis der Kompilation. Wenn make diese Situation findet, unterlässt es die Ausführung der Kommandos.

Natürlich ist dieses Modell vereinfacht – das Kompilationsergebnis könnte sich z.B. auch ändern, weil sich das Makefile selbst geändert hat, weil sich der Compilers geändert hat usf. Nun ließen sich viele dieser Parameter im Makefile erfassen (es spricht z.B. nichts dagegen, das Makefile selbst in die Dependencies aufzunehmen, vor allem, wenn man viel am Makefile rumbastelt), doch ist das in der Regel der Mühe nicht wert. Wir kehren später nochmal zu dieser Frage zurück.

Nach der Kopfzeile (die ggf. mit einem Backslash am Ende fortgesetzt werden kann) kommen die Kommandos (die im Wesentlichen einfach der shell übergeben werden), die zur Erzeugung des targets aus den prerequisites nötig sind. Sie werden in der Regel (müssen aber nicht) zur Erzeugung einer Datei mit dem Namen des targets führen.

Es ist wichtig, dass alle Zeilen mit Kommandos mit (mindestens) einem Tab anfangen – diese etwas unglückliche (weil nicht immer sichtbare) Konvention erlaubt make die Unterscheidung der Abhängigkeitszeilen von den Kommandozeilen. Hat man dort z.B. blanks stehen, kommen obskure Fehlermeldungen wie “missing separator” oder ähnliches.

Was aber, wenn wir plötzlich andere Optionen bräuchten oder lieber mit gcc statt mit cc compilieren möchten? Was, wenn das Programm auf einer anderen Maschine compiliert werden soll, die andere Optionen braucht oder deren C-Compiler vcpp heißt?

Parametrisierung des Makefiles durch Variablen. Make macht Textersetzung, ziemlich analog zum Präprozessor. Konventionell schreibt man makes Variblen groß:

CC = gcc
CFLAGS = -Wall
edelstring.o: edelstring.c edelstring.h
  $(CC) $(CFLAGS) -c -o edelstring.o edelstring.c

Die Definition von Variablen geht mit einer schlichten Zuweisung, ihre Verwendung ist etwas ungewohnt: Ein $-Zeichen ist das Signal, dass hier eine Variablenersetzung stattfinden soll, und es sind zusätzlich noch Klammern um den Variablennamen nötig.

Meist sind Kommandos für fast alle Regeln gleich. Dafür gibt es Pattern-Rules, die eine Regel für eine ganze Klasse von Transformationen beschreiben, hier z.B. von irgendeiner C-Datei zu irgendeiner Objektdatei:

%.o: %.c
  $(CC) $(CFLAGS) -c -o $@ $<

Diese Regel besagt, dass, wenn make feststellt, dass es eine Datei foo.o bauen soll, es erstmal sehen soll, ob es eine Datei foo.c sieht. Wenn das so ist, werden die Kommandos ausgeführt, wobei in der Variable $@ der Name des Targets und in $< der Name der ersten Abhängigkeit gespeichert wird.

Normalerweise kennt make schon die üblichsten Pattern-Regeln – was bei einem konkreten make so eingebaut ist, zeigt make -p. Wenn man nichts Spezielles vor hat, ist die Regel oben also meist überflüssig.

Die Pattern-Rules sind eine Erweiterung des GNU make über das originale Uralt-make, das für diesen Zweck so genannte suffix rules hatte. Diese funktionieren ganz ähnlich, sehen aber erheblich blöder aus und enthalten mehr Fallen. Es ist heute m.E. durchaus zulässig, von Leuten, die Programme komplilieren wollen, GNU make zu verlangen, so dass ich die Verwendung von Pattern-Rules gegenüber suffix rules empfehle.

Damit sind nun leider unsere Abhängigkeiten verloren gegangen. Make akzeptiert aber “einsame” Abhängigkeitszeilen ohne Kommandos und fügt die Kommandos dann aus Pattern Rules ein.

edelstring.o: edelstring.c edelstring.h
estr_test.o: estr_test.c edelstring.h

Das Erzeugen und Aktuellhalten dieser Abhängigkeiten ist auch mühsam; um sich diese Arbeit zu sparen, kann man das Programm makedepend einsetzen, das dies automatisch erledigt. Der Aufruf

makedepend -s "# Don't delete this line -- makedepend depends on it" \
  edelstring.c estr_test.c

hängtz.B. Zeilen wie

# Don't delete this line -- makedepend depends on it
edelstring.o: /usr/include/stdlib.h /usr/include/features.h
edelstring.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h
edelstring.o: /usr/lib/gcc-lib/i686-pc-linux-gnu/2.95.3/include/stddef.h
edelstring.o: /usr/include/string.h edelstring.h
...
an das Makefile an. Offensichtlich werden auch die Standard-Header berücksichtigt (es gibt Versionen von makedepend, die das unterdrücken können). Der Sicherheit halber sollte immer die -s-Option angegeben werden, weil verschiedene makedepends verschiedene Default-Markierungszeilen verwenden und ohne diese Option Makefiles, die mit Dependencies ausgeliefert werden (sollte man nicht tun) auf anderen Maschinen komische Fehlermeldungen liefern (weil prerequisites in den Regeln stehen, die es auf der anderen Maschine gar nicht gibt).

Normalerweise wird makedepend aus einer Regel depend im Makefile heraus verwendet, was etwa so aussehen könnte:

SRCS = file1.c file2.c ...
depend:
  makedepend -s "# Don't delete this line -- makedepend depends on it"\
    -- $(CFLAGS) -- $(SRCS)

Die Idee ist, dass alle Abhängigkeiten auf dem neuesten Stand sind, wenn man make depend tippt. Die CFLAGS sind hier nötig, weil man dort etwa zusätzliche Verzeichnisse angeben kann, in denen nach Headerdateien gesucht werden soll – makedepend muss sowas wissen.

Jetzt müssen wir nur noch dafür sorgen, dass unser Programm insgesamt gebaut wird. Eine mögliche Regel dafür könnte so aussehen:

OBJS = estr_test.o edelstring.o
estr_test: $(OBJS)
  $(CC) -o $@ $(LDFLAGS) $(CFLAGS) $^

Diese Regel ist etwas “verschwenderisch”. Zum einen müsste hier eigentlich nur noch der Linker aufgerufen werden (wir haben ja nur noch Objektdateien), aber die üblichen Compilerfrontends wissen selbst, was sie zu tun und zu lassen haben, wenn sie Objektdateien bekommen. Zum zweiten müssten hier auch nicht mehr die CFLAGS angegeben werden, denn es wird ja nicht mehr kompiliert. Es ist aber auch bequem, sie drin zu lassen, weil z.B. Compilerflags zum Erzeugen von Debugging- oder Profiling-Information durchaus Einfluss auf das Linkergebnis haben können und man auf diese Weise auf der sicheren Seite ist, ohne Schaden anzurichten.

Die von make vordefinierte automatische Variable $^ fügt einfach alle Dependencies ein. In der Tat sieht in etwa so auch die Pattern Rule von “viele .o” auf “Datei ohne Extension” aus, man hätte also auch einfach

estr_test: $(OBJS)

ohne weitere Kommandos schreiben können.

Make steuert auch die Erzeugung dieser Folien. Ein paar Ausschnitte aus dem Makefile:

%.epsi:%.tex
  tex $<
  dvips $*
  ps2epsi $*.ps
  rm -f $*.ps $*.log $*.dvi
...
view: folien.ps
  gv -seascape -media A4r folien.ps
...
skript.ps: skript.dvi
  $(DVIPS) $(DVIPS_SKR) skript.dvi
  rm skript.dvi

skript: skript.ps

Hier habe ich eine weitere automatische Variable verwendet, $*, die den Namen des Targets ohne Extension enthält – heißt das Target etwa fig_foo.epsi, expandiert $* zu fig_foo.

make als deklarative Sprache

make ist nicht nur als Hilfsprogramm interessant – tatsächlich ist es ein Prototyp für anwendungsspezifische Spezialsprachen. Das sind, häufig in Anwendungen eingebettete formale Sprachen, die die Definition der in der Anwendung benötigten Daten und Verfahren besonders leicht machen (sollen). Als solche sind sie häufig eher deklarativer Natur, wie eben auch make.

Der Vorteil deklarativer Sprachen wird hier auch recht klar. Ein Makefile-Fragment wie

foo: foo.c
  gcc -o foo foo.c

(das ja letztlich Abhängigkeiten zwischen verschiedenen Dateien “deklariert” lässt sich noch recht leicht in eine prodezurale Form bringen (ich verwende ein paar Funktionen, die es so nicht gibt, die aber leicht zu definieren wären):

if (!exists("foo") | isnewer("foo.c", "foo")) {
  system("gcc -o foo foo.c");
}

(Beachtet, wie praktisch hier die short circuit evaluation ist: Wenn foo gar nicht existiert, wird isnewer gar nicht gefragt, ob es neuer ist als foo.c, und das ist gut, weil isnewer bestimmt nicht gutartig reagiert, wenn es nach dem Datum nicht existierender Dateien gefragt wird).

Wenn ihr analoges bei nichttrivialen Makefiles probiert, habt ihr im Nullkommanichts ein unglaubliches Spaghetti von ifs, das niemand mehr durchschaut – oder ihr schreibt euch geschickte Funktionen, die aber gemeinsam mit den geeigneten Datenstrukturen letztlich wieder eine Art kleine deklarative Sprache bilden werden.

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

TEX-Quelltext in der Datei onepage.tex wird durch das Kommando

tex onepage

in eine so genannte DVI-Datei onepage.dvi verwandelt (DVI steht für device independent, Geräteunabhängig). Will man sie drucken, will man in der Regel Postscript haben, das wiederum durch den Befehl

dvips onepage.dvi

in einer weiteren Datei onepage.ps erzeugt wird. Vielleicht will man jetzt ein (Bitmap-) Bild daraus machen. Dies geht mit dem Kommando

pstopnm -stdout onepage.ps > onepage.pnm

Nun sind pnm-Bilder nicht gepackt, und so hätte man zur Speicherung gerne png-Bilder. Diese Wandlung besorgt ein Programm pnmtopng:

pnmtopng onepage.pnm > onepage.png

Schreibt Pattern-Rules für GNU make für jeden Schritt.


Markus Demleitner

Copyright Notice