Programmiertool make
Willemers Informatik-Ecke

Mehrere Quelldateien verwaltet man mit make

Sobald ein Programm aus mehreren Dateien besteht, hat man schnell den Punkt erreicht, wo es sinnvoll ist, sich einen kurzen Batch für das Compilieren zu schreiben. Statt dessen kann man aber auch einen Makefile schreiben. Das ist nicht viel aufwändiger, hat aber den Vorteil, dass das Programm make überwacht, welche Änderungen bei der neuesten Version zu berücksichtigen ist. Das heißt, dass make nie mehr übersetzt, als erforderlich ist.

Das Projekt meinprog besteht aus den Sourcedateien haupt.c, test.c und tools.c. Jede dieser Dateien hat eine Headerdatei (haupt.h, test.h und tools.h), die es jeweils selbst einbindet. Dazu bindet haupt.c jede andere Headerdatei ein und jedes Modul bindet die globalen Definitionen aus haupt.h ein.

Eine Datei Makefile wird angelegt und darin wird der Weg der Compilierung beschrieben. Das Programm meinprog hängt von den Dateien test.o, haupt.o und tools.o ab:

meinprog:  test.o haupt.o tools.o

Die darauf folgenden Zeilen beschreiben, wie die Zieldatei hergestellt werden kann. Die Datei meinprog wird generiert, indem man den Compiler aufruft und die Objektdateien als Parameter angibt. Der Compiler merkt von sich aus, dass er hier nur linken soll, da keine echten Quellcodedateien vorhanden sind. Solche Aktionszeilen müssen mit einem Tabulator beginnen. Es dürfen keine Leerzeichen verwendet werden.

meinprog:  test.o haupt.o tools.o
        cc -o meinprog test.o haupt.o tools.o

Wer nun einfach make aufruft, erhält überraschenderweise bereits ein komplett übersetztes Programm. Auf dem Bildschirm erscheinen folgende Zeilen:

gaston> make
cc    -c -o test.o test.c
cc    -c -o haupt.o haupt.c
cc    -c -o tools.o tools.c
cc -o meinprog test.o haupt.o tools.o
gaston>

Tatsächlich "weiß" make, wie man aus c-Dateien o-Dateien macht und da er mit den Regeln des Makefiles die o-Dateien nicht erzeugen kann, sieht make im aktuellen Verzeichnis nach, ob es Dateien gibt, aus denen er o-Dateien machen kann.

Ändert man die Datei test.c, so wird nur test.c übersetzt und meinprog neu gebunden. Ändert man allerdings test.h, passiert nichts. make kennt die Abhängigkeiten der Header nicht. Damit make auch Änderungen der Headerdateien überwacht, ist folgende Änderung im Makefile notwendig.

test.o : test.c test.h haupt.h

tools.o : tools.c tools.h haupt.h

haupt.o : haupt.c haupt.h test.h tools.h

Eine Aktionszeile müsste hier eigentlich etwa so lauten:

test.o : test.c test.h haupt.h
        cc -c -o test.o test.c

Da aber make die erforderliche Aktion bereits kennt, braucht man sie nicht zu erwähnen.

Geringster Aufwand

Das Programm make ist also ein Tool, um Zieldateien aus den Quelldateien mit minimalem Aufwand zu generieren. Dazu wird in einer Datei namens makefile oder Makefile, die Abhängigkeiten der Dateien beschrieben und die Programmaufrufe festgelegt, die aus den jeweiligen Quellen die Ziele generieren. make erkennt, wenn eine der Quelldateien neuer als die Zieldatei ist, und ruft die Generierungsprogramme auf, bis die Zieldateien neuer als die jeweiligen Quellen sind oder eine Aktion scheitert.

Ein Makefile hat Einträge der folgenden Grundstruktur:

Ziel : Abhängigkeiten
               Generierungskommando

Regeln

Diese Grundstruktur nennt man Regel. Eine neue Regel muss mit einer Leerzeile von der vorherigen getrennt werden. Der Leerraum vor dem Generierungskommando muss ein Tabulatorzeichen sein. Es können auch mehrere Kommandozeilen nacheinander angegeben werden. Alle müssen mit Tabulator eingerückt sein. Die Kommandozeilen werden jeweils in einer separaten Shell abgearbeitet. In manchen Situationen gibt das Seiteneffekte, die man berücksichtigen muss. Beispiel:

try :
       cd .. ; pwd
       pwd

Die Ergebnisse der beiden Aufrufe von pwd sind nicht gleich. Der Wechsel mit cd .. gilt nur für die aktuelle Zeile. In der nächsten Zeile wird wieder aus dem bisherigen Verzeichnis gearbeitet.

cd .. ; pwd
/home/arnold/my/src/unix
pwd
/home/arnold/my/src/unix/make

Hängen also Kommandos so zusammen, dass sie in einer gemeinsamen Shell bearbeitet werden müssen, sollten sie in dieselbe Zeile geschrieben werden und mit Semikola getrennt werden. Bei langen Zeilen kann mit einem Backslash die Zeile in der nächsten Zeile fortgesetzt werden.

Als Kommentarzeichen gilt das # in der ersten Spalte.

Makros im Makefile

make arbeitet mit Makros

Durch die Verwendung von Makros können die Makefiles besser strukturiert und flexibler gestaltet werden. Makros sind Variablen, denen Zeichenketten zugewiesen werden und dann durch Voranstellen eines $-Zeichens ausgewertet werden. Durch das Zusammenfassen von Dateinamen oder Optionen kann ein Makefile kürzer und vor allem besser lesbar werden.

Im Beispiel werden die Objektdateien zusammen behandelt und zweimal aufgezählt, einmal in der Abhängigkeitsbeschreibung von meinprog und dann im Compileaufruf.

meinprog:  test.o haupt.o tools.o
        cc -o meinprog test.o haupt.o tools.o

Hier kann man eine Variable OBJS definieren, die die Objekte bezeichnet. Durch Einsetzen von OBJ ergibt sich die folgende Makedatei.

OBJS = test.o haupt.o tools.o

meinprog: $(OBJS)
      cc -o meinprog $(OBJS)

Vordefinierte Makros

Es ist möglich, mehrere Ziele mit einer Regel zu behandeln. So könnte beispielsweise $(OBJS) als Ziel verwendet werden. Die einzelnen Ziele werden nacheinander aufgelöst. Im Generierungskommando kann auf das aktuelle Ziel Bezug genommen werden. Dazu gibt es vordefinierte Makros, die hier am Beispiel haupt.o aufgezeigt werden.

Makro Bedeutung
$@ Dateiname des Ziels (haupt.o)
$* Basisnamen des Ziels (haupt)

Suffixregeln

Die Suffixregeln beschreiben den Übergang einer Dateiendung zu einer anderen. Eine solche Regel erkennt man daran, dass das Ziel die zwei Dateiendungen mit dem jeweiligen Punkt am Anfang direkt hintereinander stehen hat.

.quell.ziel:

Der typischste Übergang ist sicher der von C-Sourcen zu Objekten. Die Sourcen enden auf c und die Objektdateien auf o. Die entsprechende Suffixregel lautet dann:

.c.o:
         cc -c $<

Das interne Makro $< darf nur bei Suffixregeln verwendet werden und bezeichnet das aktuelle Ziel.

Mehrere Ziele

Voneinander abhängige Ziele

Ein Makefile kann mehrere Programme generieren. Das macht man dann gern, wenn gleiche Quelltexte für mehrere Projekte gebraucht werden, die vielleicht sogar noch voneinander abhängig sind. Ein typisches Beispiel sind Client- und Serverprogramme, die in den Headern gleiche Datenstrukturen verwenden.

all: client server

client: $(SENDHEADER) $(COMMONOBJS) $(CLTOBJS)
        ...

server: $(SENDHEADER) $(COMMONOBJS) $(SRVOBJS)
        ...

Zielabhängigkeiten

Das erste Ziel ist immer das Ziel des gesamten Makefiles. In diesem Fall würde also beim Aufruf von make erst das Ziel all generiert werden. Da es keinerlei verbundene Aktion gibt, wird lediglich geprüft, ob die Abhängigkeiten erfüllt sind. Entsprechend wird als nächstes client und dann server gebildet. Es ist nicht zwingend, aber üblich, das Pseudoziel, das alle Programme eines Makefile generiert, all zu nennen.

make als Installationstool

Oft werden Makefiles auch zur Installation verwendet. Dazu wird ein Pseudoziel install eingeführt, das überprüft, ob alle Dateien des Projektes an den richtigen Stellen vorhanden sind und ansonsten als Aktion einfache Kopierbefehle absetzen. Aufgerufen wird so eine Installation mit make und als Parameter das Ziel install.

make install