大学.xyzzy.de — make und Makefile

© 2006 Robert Klein

Was ist das, make und Makefile

make ist ein Steuerungsprogramm. Es führt automatisch die Anweisungen aus, die dazu nötig sind, um aus einem C-Quelltext ein ausführbares Programm zu machen. Dabei ist make nicht auf C-Programme beschränkt. Eigentlich kann man mit make alle "Quelltexte" in "Zieltexte" übersetzen, solange es für die Zwischenschritte dahin Programme gibt, die man auf der Kommandozeile aufrufen kann.

Natürlich macht make das nicht vollkommen automatisch, sondern braucht dazu eine Steuerungsdatei, Makefile genannt. Der Name Makefile ist außerdem der Name der Datei, die make als Steuerungsdatei verwendet, wenn keine andere Datei als Steuerungsdatei angegeben ist.

Mit einer Steuerungsdatei kann man aber nicht nur ein "Ziel", nur ein ausführbares Programm erreichen. Man kann darin auch mehrere Ziele angeben, etwa blatt5 für das ausführbare Programm zum Aufgabenzettel und blatt5htmldocs für eine automatisch zu erzeugende HTML-Dokumentation. Wenn beim Aufruf kein Ziel angegeben wird, dann versucht make das erste im Makefile stehende Ziel zu erzeugen.

Was muss ich tun?

make wird so aufgerufen:

make

Mit einer anderen Steuerungsdatei, hier mache-blatt5 und dem Ziel blatt5, sieht der Aufruf so aus:

make -f mache-blatt5 blatt5

Kommen wir jetzt mal zu einem Makefile. Ich habe hier einen Makefile für en Aufgabenzettel 5, den mit den komplexen Zahlen. Ein ganz einfacher Makefile sieht so aus:

blatt5: blatt5.c
        gcc -Wall -std=c99 -pedantic -o $@ $?

Dieser einfache Makefile besteht nur aus zwei Zeilen. In der ersten stehen stehen drei Dinge, zuerst das Ziel, blatt5, dann ein : und danach die Voraussetzungen, die da sein müssen, damit das Ziel erreicht werden kann.

Im Beispiel ist die Datei "blatt5.c" Voraussetzung, sie wird benötigt, damit das Ziel erreicht werden kann.

Der Doppelpunkt hat hier auch eine besondere Bedeutung. Er trennt nicht nur das Ziel von seinen Voraussetzungen, es sagt auch, dass das Ziel nur dann neu erzeugt werden soll, wenn die Voraussetzungen jünger sind.

Im Beispiel wird blatt5 nur dann neu erzeugt, wenn die Voraussetzung, die Datei blatt5.c neueren Datums ist, als das Ziel blatt5. Wenn das Ziel noch nicht existiert wird es auch erzeugt. Es gibt noch andere Zeichen, mit denen das Zeil von seinen Voraussetzungen getrennt werden kann, und die dann andere Regeln haben, wann das Ziel erzeugt wird. Die sind jetzt erst mal uninteressant.

In den nachfolgenden Zeilen stehen dann — durch ein Tab eingerückt — die Anweisungen, mit denen man das Ziel erhält. Im Beispiel ist es einfach der gcc-Aufruf.

Die ersten drei Optionen nach gcc sind zum Teil wohl schon bekannt. Mit -wall erhalte ich alle Warnungen, -std=c99 sagt gcc, dass ich C in der 1999 von der ISO normierten Form verwende. -pedantic sagt dem gcc weiterhin, dass er sehr kleinkariert sein soll und schon die kleinsten Ungenauigkeiten an den Pranger stellt.

Interessant wird es jetzt: Mit -o filename wird die Ausgabedatei bestimmt. Im Beispiel-Makefile wird das $@ vom make-Programm durch den Namen des Ziels ersetzt. Genauso wird das $? durch alle Voraussetzungen ersetzt, die nicht aktuell sind.

Mehrere Ziele

Man kann mehrere Aufgaben mit einem Makefile erledigen. Ich habe jetzt noch die Aufgabe 6.1 gelöst, und dazu den Makefile erweitert:

blatt6-1: blatt6-1.c
        gcc -Wall -std=c99 -pedantic -o $@ $?

blatt5: blatt5.c
        gcc -Wall -std=c99 -pedantic -o $@ $?

Hier haben wir jetzt zwei Ziele, jeweils mit Voraussetzungen und Anweisungen, wie das Ziel erzeugt wird.

Wie man sieht, habe ich das Ziel blatt6-1 an den Anfang gestellt. Da Blatt 5 fertig ist, und ich das entsprechende Ziel nicht mehr so oft aufrufe habe ich die nächste Aufgabe an den Anfang gestellt, damit ich make aufrufen kann, ohne das neue Ziel mit angeben zu müssen. Der Name meiner Steuerungsdatei ist Makefile, den muss ich auch nicht mit angeben.

Das Ziel blatt6-1 wird dann so erzeugt:

make

Für das Ziel blatt5, dass jetzt nicht mehr als erstes Ziel im Makefile steht, muss ich jetzt diesen Aufruf verwenden:

make blatt5

Ein typischer Makefile hat sehr oft auch ein Ziel all, mit dem (fast) alle Ziele erzeugt werden:

blatt6-1: blatt6-1.c
        gcc -Wall -std=c99 -pedantic -o $@ $?

blatt5: blatt5.c
        gcc -Wall -std=c99 -pedantic -o $@ $?

all: blatt5 blatt6-1

Neu ist hier, dass beim Ziel all keine Dateien Voraussetzung sind, sondern andere Ziele. Ein Ziel kann also nicht nur von Dateien, sondern auch von anderen Zielen abhängen.

Bei fertigen Projekten, wie man sie im Internet findet, ist das Ziel all oft das erste im Makefile, damit man auf der Kommandozeile einfach nur make eintippen muss.

Ich will nur die Quelldateien zur Abgabe!

Auch dafür bietet make eine Lösung. Bisher waren unsere Ziele Dateien. Wenn wir nur die Quelldateien haben wollen, dann definieren wir uns ein unechtes Ziel. Im Englischen nennt man das "phony". Im Makefile erklären wir zuerst ein Ziel als "phony" und definieren es dann:

.PHONY: clean

blatt6-1: blatt6-1.c
        gcc -Wall -std=c99 -pedantic -o $@ $?

blatt5: blatt5.c
        gcc -Wall -std=c99 -pedantic -o $@ $?

all: blatt5 blatt6-1

clean:
        rm -f blatt5.exe blatt5 blatt6-1.exe blatt6-1

In diesem Makefile wird zuerst das Ziel clean als "phony" deklariert. Ich habe es jetzt ganz am Ende definiert. Der Schalter -f sorgt dafür, dass die Dateien gelöscht werden, ohne vorher nachzufragen. Ausserdem bekomme ich dann keine Fehlermeldung, wenn eine Datei nicht existiert.

Ich habe hier immer zwei Varianten löschen lassen, einmal nur blatt5 und blatt6-1 und dann noch einmal jeweils mit der Dateiendung .exe. Dadurch funktioniert der Makefile unter Windows und Unix.

Variablen

Zwei Variablen haben Sie jetzt schon im Makefile verwendet: @ und ?. Dass eine Zeichenkette eine Variable ist, erkennt make daran, dass davor ein $ steht. Oben im gcc-Aufruf sind das $@ und $?. Diese beiden Variablen sind innerhalb der Anweisungen gültig, die das jeweilige Ziel erzeugen.

Zum Beispiel kann ich die Variablen verwenden, um anzugeben, welchen Compiler ich verwenden will. Hier ist ein Beispiel, in dem ich Variablen für gcc uns Dave Hansons lcc gesetzt habe:

# uncomment the following 2 lines, when using gcc
#CC=gcc
#COPTIONS=-Wall -std=c99 -pedantic
# uncomment the following 2 lines, when using Dave Hansons lcc
CC=C:/lcc/bin/lc.exe
COPTIONS=-A -ansic -pedantic



.PHONY: clean

blatt6-1: blatt6-1.c
        $(CC) $(OPTIONS) -o $@ $?

blatt5: blatt5.c
        $(CC) $(OPTIONS) -o $@ $?

all: blatt5 blatt6-1

clean:
        rm -f blatt5.exe blatt5 blatt6-1.exe blatt6-1

Ein Programm mit mehreren Quelldateien erstellen

Alle größeren Programme bestehen aus mehr als einer Quelldatei. In dieser Anleitung habe ich bisher nur Programme mit einer Quelldatei in den Beispielen verwendet. Bei mehreren Dateien werden zuerst die einzelnen Objektdateien erzeugt und dann gelinkt:

CC=gcc
COPTIONS=-Wall -std=c99 -pedantic
LD=$(CC) -Wl
LDFLAGS=

.PHONY: clean

OBJECTS=meinprog.o list.o

meinprog: $(OBJECTS)
        @echo "Linking $<..."
        $(LD) $? -o $@ $(LDFLAGS)

all: meinprog

clean:
        rm -f meinprog.exe meinprog *.o
        
.c.o:
        @echo "Compiling $<..."
        $(CC) $(COPTIONS) $(CFLAGS) -c $< -o $@

Die letzten drei Zeilen sind hier die interessantesten; damit wird eine allgemeine Regel angegeben, wie aus Dateien, die auf .c enden Dateien erstellt werden, die auf .o enden, oder anders ausgedrpckt, aus C-Dateien Objekt-Dateien.

In der Regel zum Erstellen des Programms meinprog werden jetzt nicht mehr die C-Dateien vorausgesetzt, sondern die Objektdateien. Die habe ich hier in einer Extra-Variablen aufgeführt, $(OBJECTS).

Wie man sieht, wird zum Verbinden der Objektdateien, dem Linken, auch der gcc verwendet, den ich hier mit der Variablen $(LD) aufrufe.

Hier im Beispiel habe ich noch ein paar @echo Befehle eingefügt, die ausgeben, was gerade passiert.

Beim Ziel clean lasse ich dann auch noch die Objektdateien löschen.

Kommt (vielleicht) noch

Fettes Beispiel

Der folgende Makefile hat mir im Kurs Betriebssysteme, Übung 3 im Sommersemester 2004 gute Dienste geleistet. Teile davon habe ich auch für andere Aufgaben weiterverwendet. Keine Angst, so ein komplizierter Makefile wird während des Studiums nicht verlangt.

SHELL=/bin/sh
MAKE=make

CP=cp
MV=mv

INCDIR=../include
LIBDIR=../lib

CC=gcc
COPTIONS=-O3 -Wall -ansi
CFLAGS=-I${INCDIR}

LD=$(CC) -Wl
LDFLAGS=-L${LIBDIR} -s --trace

AS=as

AR=ar
ARFLAGS=rcv

INSTALL=/usr/bin/install -c
INSTALLDIR = /usr/bin/install -c -m 755 -d

.SUFFIXES:      .c .o

.PHONY: clean tex

all: gibaus gibaus_ex sema install gibaus_ex2

sema:   libsemaphore.a

gibaus: gibaus.o
        $(LD) $? -o $@ $(LDFLAGS)

gibaus_ex:      gibaus_ex.o
        $(LD) $? -o $@ $(LDFLAGS)

gibaus_ex2:     gibaus_ex2.o
        $(LD) $? -o $@ $(LDFLAGS) -lsemaphore

testsem1:       testsem1.o
        $(LD) $? -o $@ $(LDFLAGS) -lsemaphore

testsem2:       testsem2.o
        $(LD) $? -o $@ $(LDFLAGS) -lsemaphore

libsemaphore.a: semaphore.o
        @echo "Creating Library..."
        $(AR) $(ARFLAGS) $@ $?
        @echo "Done..."

install:        libsemaphore.a
        @echo "Installing semaphore library and header file..."
        $(INSTALLDIR) $(LIBDIR)
        $(INSTALL) $? $(LIBDIR)
        $(INSTALLDIR) $(INCDIR)
        $(INSTALL) semaphore.h $(INCDIR)
        @echo "Done..."
        @echo ""

tex:
        for i in *.h *.c ; do \
                cat ../head >> $${i}-t.tex ; \
                echo \\lstset\{language=C++\} >> $${i}-t.tex ; \
                echo \\lstinputlisting\{$$i\} >> $${i}-t.tex ; \
                cat ../foot >>$${i}-t.tex ; \
                latex $${i}-t.tex ; \
                dvips $${i}-t.dvi -o $${i}.ps ; \
                rm -f $${i}-t.dvi ; \
                psnup -pa4 -Pa4 -d -n 2 $${i}.ps $${i}-2.ps ; \
                rm -f $${i}.ps ; \
                pdflatex $${i}-t.tex ; done

clean:
        rm -f *.s *.o *.a *~ *.aux *.log *.tex *.ps *.pdf *.exe *.core
        rm -f a.out gibaus gibaus_ex gibaus_ex2 testsem1 testsem2

.c.o:
        @echo "Compiling $<..."
        $(CC) $(COPTIONS) $(CFLAGS) -c $< -o $@

Impressum, Datenschutzerklärung &c