Make: Ein Werkzeug (nicht nur) zur Programmerstellung
Make wird primär als Hilfsmittel bei der Kompilierung von größeren Programmen
verwendet. Hauptsächlich für Programme die in der Programmiersprache C
geschrieben sind, aber es lässt sich für alle anderen Compiler Sprachen
ebenfalls verwenden.
Letztlich ist es nicht nur bei der Programmierung sinnvoll einzusetzen,
sondern immer dann, wenn automatisiert Ergebnis-Dateien aus ein oder
mehreren Quell-Dateien erstellt werden.
Da Make aber für die Programmierung entwickelt wurde, kann damit auch am einfachsten die Arbeitsweise und die notwendige Konfiguration erläutert werden.
Ein Beispielprogramm in C
Unser (recht sinnfreies) Beispielprogramm besteht aus drei einzelnen
Quelldateien sowie zwei Header Dateien zur Deklaration der Funktionen.
Der Name des ablauffähigen Programms soll bsp
sein:
bsp.c
/** bsp.c **/ # include "a.h" # include "b.h" int main (int argc, char*argv[]) { afunc ("main()"); bfunc ("main()"); return 0; }
/** a.c **/ # include <stdio.h> #define extern # include "a.h" #undef extern #include "b.h" void afunc (char *callermesg) { printf ("afunc() called from %s\n", callermesg); bfunc ("afunc()"); }
/** b.c **/ # include <stdio.h> #define extern # include"b.h" #undef extern void bfunc (char *callermesg) { printf ("bfunc() called from %s\n", callermesg); }
In allen C-Dateien wird die Datei b.h
über die Include-Anweisung
eingefügt. Die Header Datei a.h
jedoch nur in bsp.c
und in a.c
.
Der Vorgang des Kompilierens erfolgt in der Regel in zwei Stufen:
-
Übersetzten der einzelnen Quelltextdateien in Objektcode durch den entsprechenden Compiler (in unserem Fall
cc
) -
Zusammenführen (linken) der Objektdateien sowie der Funktionen aus der Standardbibliotheken und Erstellen des ausführbaren Programms.
Damit sind vier Kommandoaufrufe nötig:
-
Aus
bsp.c
die Dateibsp.o
erzeugen$ cc -c bsp.c
-
Aus
a.c
die Dateia.o
erzeugen$ cc -c a.c
-
Aus
b.c
die Dateib.o
erzeugen$ cc -c b.c
-
Aus
bsp.o
,a.o
undb.o
das Programmbsp
erzeugen$ cc -o bsp bsp.o a.o b.o
Da es sich um recht viele einzelne Kommandoaufrufe handelt, könnte man
diese in einer Batchdatei ablegen und darüber ausführen.
Allerdings ist das einerseits aufwändig, insbesondere wenn das Programm aus
hunderten von Einzeldateien zusammengebaut wird, und andererseits
ineffektiv.
Ist das Programm einmal erstellt, und man ändert lediglich noch eine
Kleinigkeit z.B. an der Funktion bfunc()
, müssen ja nicht alle Kommandos
erneut ausgeführt werden: Es reicht die Datei b.c
neu zu übersetzen und
alle Objektdateien erneut zu einem Programm zusammen zu binden.
$ cc -c b.c
$ cc -o bsp bsp.o a.o b.o
Doch wie kann dieser Vorgang automatisiert ablaufen? Nun, die Übersetzung einer Quelltextdatei muss genau dann durchgeführt werden, wenn
-
die dazu gehörende Objektdatei noch nicht existiert, oder
-
die Quelltextdatei ein jüngeres Datum hat als die entsprechende Objektdatei.
Der gleiche Zusammenhang gilt auch für die Programmdatei. Der Linker muss dann aufgerufen werden, wenn mindestens eine Objektdatei „neuer“ ist als die Programmdatei.
Bis dahin ist das noch mit Bordmitteln der Shell zu erreichen:
#!/bin/sh ## ## bsp-maker.sh Example shell script to compile bsp ## ## Attention: lack of header dependencies! prog=bsp sourcefiles="main.c a.c b.c" objectfiles=`echo $sourcefiles | sed -e "s/\.c/.o/g"` # one way to get the obj file list ## create every object file newobj=false for src in $sourcefiles do obj=`basename $src .c` # strip off ".c" obj="$obj.o" # add ".o" instead if test ! -f $obj -o $src -nt $obj # obj don't exist, or src is newer than obj then echo "cc -c $src" cc -c $src newobj=true fi # objectfiles="$objectfiles $obj" # alternate way to get the object file list done ## link all object files together if test ! -f $prog -o $newobj = true # prog is not there or at leat one newer obj file then echo "cc -o $prog $objectfiles" cc -o $prog $objectfiles fi
Allerdings enthält das Shell Skript einen Fehler: Ändert sich eine der Header Dateien wird der Quelltext nicht neu übersetzt. Dabei fließen die Inhalte der Header Dateien ja in den Übersetzungs Prozess mit ein.
Zusätzlich zu der Abhängigkeit von der jeweiligen Quelltextdatei besteht also außerdem eine Abhängigkeit von all den Header Dateien, die in der jeweiligen Quelldatei inkludiert werden.
Der korrekte Abhängigkeitsbaum für unser Beispiel sieht daher so aus:
Leider lässt sich das nicht mehr sehr elegant in ein Shell-Skript bauen.
Make’s Makefile
Um solche mehrstufige, und unter Umständen sehr komplexe Abhängigkeiten zu
kontrollieren wurde make entwickelt.
Gesteuert wird es über eine Konfigurationsdatei, deren Standardname
makefile
oder Makefile
ist.
Das grundlegende Makefile für unser Beispiel würde so aussehen:
## Makefile to explain Make (Basic version) ## ## create executable out of three C source & two header files bsp: bsp.o a.o b.o # the first target is the default one cc -o bsp bsp.o a.o b.o bsp.o: bsp.c a.h b.h cc -c bsp.c a.o: a.c a.h b.h cc -c a.c b.o: b.c b.h cc -c b.c # rule syntax: target: dependencies command(s) to create <target> from <dependencies> the command lines *MUST* start with a <TAB>
Mit diesem Makefile
im aktuellen Verzeichnis generiert ein Aufruf des
Kommandos make das ausführbare Programm und wickelt alle jeweils dazu
notwendigen Tätigkeiten in der richtigen Reihenfolge selbsttätig ab.
$ make cc -c bsp.c cc -c a.c cc -c b.c cc -o bsp bsp.o a.o b.o $ make make: 'bsp' is up to date.
Ändert sich eine Quellkomponente, werden jetzt nur noch die notwendigen Tätigkeiten ausgeführt um das Programm zu erstellen:
$ touch a.h $ make cc -c bsp.c cc -c a.c cc -o bsp bsp.o a.o b.o
Smart Make
Eingebaute Regeln
Make „weiß“ das aus einer C-Quelldatei eine Objektdatei generiert werden kann in dem der C-Compiler entsprechend aufgerufen wird. Die allgemeine Regel dafür sieht in Makefile Syntax wie folgt aus und wurde Make bereits eingepflanzt:
%.o: %.c $(CC) $(CFLAGS) -c -o $< $@
Die Regel bedeutet, dass eine Objektdatei (.o
) beliebigen Namens aus
einer C-Quelldatei (.c
) mit dem gleichen Basisnamen (%
) erzeugt werden
kann, indem das Kommando cc
mit einigen Optionen und diverser
Variablenersetzung aufgerufen wird.
Die Variable $<
wird in der Anwendung durch das aktuelle Ziel, und $@
durch die Abhängigkeitsdatei ersetzt.
Im Makefile kann die Variable CFLAGS
gesetzt werden um entsprechende
Optionen an den C-Compiler durchzureichen.
Mit diesen „eingebauten“ Regeln kann das Makefile
viel kompakter
formuliert werden:
## Makefile to explain Make ## ## create executable out of three C source & two header files ## ## Simpler version with use of Make built-in rules CFLAGS = -Wall bsp: bsp.o a.o b.o $(CC) $(LDFLAGS) -o bsp bsp.o a.o b.o # Header dependencies bsp.o a.o: a.h b.h b.o: b.h
Auch wenn die Regeln für die Erstellung der Objektdateien (.o
wird aus
.c
erstellt) nun nicht mehr explizit angegeben werden müssen, ist der
Hinweis auf die Abhängigkeit von den Header Dateien trotzdem nötig. Make
kann ja nicht wissen welche Header Datei in welchem C-Quelltext per
include
-Anweisung eingefügt wird.
Allerdings kann uns auch dabei ein wenig Shell Programmierung helfen:
#!/bin/sh # # dependencies.sh # Create Make dependency rules from C source files # PATH=/bin:/usr/bin grep '^\s*#\s*include\s*"' /dev/null $* | sed -e 's/#[^"]*"/ /' -e 's/"$//' -e "s/\.c/.o/"
Das Skript holt mit grep gerade die lokalen Include Anweisungen aus den C-Quellen. Anschließend extrahiert sed aus jeder Zeile den Dateinamen und korrigiert die Dateikennung auf ".o". Das Ergebnis lässt sich so bereits in ein Makefile packen.
$ dependencies *.c
a.o: a.h
a.o: b.h
b.o: b.h
bsp.o: a.h
bsp.o: b.h
Die Ausgabe wird jedoch bei vielen Include- und/oder C-Quellen recht länglich. Im Makefile könnte das komprimiert werden, indem alle Abhängigkeiten einer Objektdateien in eine Zeile geschrieben werden.
Glücklicherweise erzeugt der C-Compiler über eine spezielle Option exakt die Regeln für ein Makefile, sodass wir unser Shell-Skript nicht weiter bemühen müssen:
$ cc -MM bsp.c a.c b.c
bsp.o: bsp.c a.h b.h
a.o: a.c a.h b.h
b.o: b.c b.h
Die Ausgabe des Kommandos muss nur noch in das Makefile kopiert
werden.
[Oder man legt die Ausgabe in einer Datei ab, und fügt
diese in das Makefile ein.]
Make Variablen
Innerhalb des Makefiles
können Variablen benutzt werden, wodurch eine
bessere Strukturierung möglich wird.
So sind ja die Objektdatei lediglich Zwischenprodukte und die kreative
Arbeit des Programmierens schlägt sich in den Quelltextdateien nieder.
Sinnigerweise stehen daher im Makefile die Namen der Quelltextdateien. Da die zugehörigen Objektdateien aber ebenfalls benötigt werden, und das Pflegen von zwei Listen fehleranfällig ist, wird die Liste der Objektdatei am einfachsten aus der Liste der Quelltextdateien generiert:
## Makefile to explain Make ## ## create executable out of three C source & two header files ## ## Version with Variables PROG = bsp SRC = bsp.c a.c b.c HEADER= a.h b.h CFLAGS= -Wall OBJ = $(SRC:.c=.o) $(PROG): $(OBJ) $(CC) $(LDFLAGS) -o $(PROG) $(OBJ) # Header dependencies bsp.o a.o: a.h b.h b.o: b.h
Pseudoziele
Darüber hinaus werden sogenannte Pseudoziele nützlich. Im Makefile wird alles dokumentiert was für das gesamte Programm notwendig ist. Damit ist es der ideale Ort um alle Verwaltungstätigkeiten ebenfalls dort zu hinterlegen.
Typische Pseudoziele zur Verwaltung sind z.B.
-
clean
-
zum Löschen der Zwischendateien
-
tar
-
zum Einpacken aller Dateien die für das Erstellen des Programs notwendig sind in einem
tar
-File -
install
-
zum Installieren des ausführbaren Programmes im Suchpfad
-
print
-
um die Quelltexte auszudrucken
und einige andere Ziele mehr.
Damit kommt man zu einem Makefile
-Template welches in den meisten Programmen
Verwendung finden kann.
## Makefile to explain Make ## ## create executable out of three C source & two header files ## ## Version with variables and pseudo targets PROG = bsp SRC = bsp.c a.c b.c HEADER= a.h b.h LIBS = CFLAGS= -Wall TARSRC= $(SRC) $(HEADER) Makefile TARDST= $(PROG).tar INSTALLDIR = $(HOME)/bin # no need to change anything below this line OBJ = $(SRC:.c=.o) all: $(PROG) $(PROG): $(OBJ) Makefile $(CC) $(LDFLAGS) -o $(PROG) $(OBJ) $(LIBS) ## Pseudo targets # define all empty targets as empty .PHONY: install clean cleanall print depend # don't delete target on interrupt .PRECIOUS: .print help: ## print this help message @grep -e '^[a-z][a-z]*:\s*##' Makefile | sort all: ## create all targets install: ## install the executable strip $(PROG) cp $(PROG) $(INSTALLDIR) tags: ## create tag file for vi tags: $(SRC) ctags $(SRC) tar: ## create tar file @$(MAKE) $(TARDST) $(TARDST): $(TARSRC) tar cvf $(TARDST) $(TARSRC) clean: ## clean up (remove object files) $(RM) $(OBJ) cleanall: ## remove object files, exectuables and all other non-source files $(RM) $(OBJ) $(RM) $(PROG) tags .print dependencies.db print: ## print all modified source and header files @$(MAKE) .print .print: $(SRC) $(HEADER) @echo "pr -l72 -n $? | lpr" @touch .print sleep 5 # run "cc -MM $(SRC)" to generate the header dependencies... depend: ## update header dependencies (dependencies.db) cc -MM $(SRC) > dependencies.db # ...and include the file -include dependencies.db
Nach der vollständigen Programmentwicklung lassen sich damit alle
Programmquellen sehr einfach in ein tar
-file einpacken, das erstellte
Programm installieren, und das Arbeitsverzeichnis anschließend aufräumen:
$ make tar tar cvf bsp.tar bsp.c a.c b.c a.h b.h Makefile bsp.c a.c b.c a.h b.h Makefile $ make install strip bsp cp bsp /home/hoz/bin $ make cleanall rm -f bsp.o a.o b.o rm -f bsp tags .print dependencies.db
Make für nicht Programmierer
Auch wenn Make originär für die Unterstützung beim Programmieren entwickelt wurde, kann es natürlich überall dort zum Einsatz kommen, wo aus Eingabedateien mittels eines Programmes Ausgabedateien generiert werden müssen.
Beispiele dafür sind
-
Webseiten Erstellung über Templates
-
Dokumenten Erstellung über
LaTeX
odergroff
-
Größenanpassungen von Bilddateien (z.B. für Webseiten mittels
convert
) -
Grafiken generieren über little Languages wie z.B. Graphiz (
dot
) oderpic
-
Router Konfigurations Dateien über Templates erstellen
Make Kommandooptionen
Bisher haben wir make immer ohne Optionen aufgerufen, da dies der
Regelfall ist.
Soll ausnahmsweise die Konfiguration für make nicht aus der Datei
Makefile
oder makefile
gelesen werden, kann eine alternative
Konfigurationsdatei über die Option -f
spezifiziert werden.
Wenn lediglich ein Testlauf stattfinden soll, ohne das tatsächlich
Kommandos ausgeführt werden, ist die Option -n
zu verwenden. Alle
weiteren Optionen werden eher selten genutzt.
-f file
|
Die Konfiguration nicht aus makefile oder Makefile sondern aus der angegebenen Datei entnehmen |
-n
|
(no execution) |
-s
|
(silent) |
-t
|
(touch) |
-p
|
(print) |
-q
|
(query) |
-r
|
(rules) |
-j n
|
(jobs) |
-k
|
(keep going) |
-B
|
(always make) |
-W file
|
(what if) |
--trace
|
(trace) |
-p
|
(print-data-base) |
Bei dem Aufruf von Make können auch noch Variablen gesetzt werden, die
dann die im Makefile
gesetzten Werte überschreiben.
Das funktioniert, da Make den Wert der internen Variablen auch aus
Umgebungsvariablen gleichen Namens bezieht.
$ make CFLAGS=-Wpedantic cc -Wpedantic -c -o a.o a.c cc -o bsp bsp.o a.o b.o
Make in großen Projekten
Das Programm Make ist bereits Mitte der 1970er Jahre entwickelt worden, und liefert heute noch einen unschätzbaren Vorteil bei der Programmentwicklung. Nicht wenige Projekte umfassen viele hundert C-Quelldateien, oftmals verteilt auf mehrere Unterverzeichnisse.
Die mehr als 500 C-Quelldateien der Open Source Software BIND, ein DNS Authoritative und Recursive Name Server, sind z.B. auf mehr als 20 Unterverzeichnisse verteilt, die insgesamt 44 einzelne Makefiles beeinhalten. Ohne ein „Built-Management“ Werkzeug wie make wäre die Erstellung der diversen Binaries kaum zu bewältigen. So jedoch reicht ein
$ make
$ sudo make install
um nicht nur alle Programme die zu BIND dazugehören zu übersetzen, sondern diese dann auch noch als Superuser in den entsprechenden Programmverzeichnissen zu installieren.
Dabei werden die Makefiles nicht mehr händisch geschrieben sondern
automatisch durch das Kommando automake
[automake ist ein
Teil der autoconfig Werkzeuge.]
aus Template Dateien erzeugt.
Typischerweise beginnt daher die Installation eines Open Source Programms
mit einem configure
Aufruf, bei dem häufig auch noch spezielle Features
des Programms spezifiziert und einkompiliert werden können. Danach
existieren erst die Makefiles und die obigen make
Kommandos wissen was
zu tun ist. Genaueres steht in der Regel in einer README oder
README.md Datei.
[Früher waren Readme Dateien reine Textdateien.
Heutzutage werden sie meist in Makrkdown Syntax verfasst. Dies wird durch
die Dateiendung .md
angezeigt.]
Bei BIND sieht der vollständige Ablauf nach dem Download der Quellen z.B. so aus:
$ wget https://downloads.isc.org/isc/bind9/9.19.13/bind-9.19.13.tar.xz
$ unxz bind-9.19.13.tar.xz
$ tar xf bind-9.19.13.tar
$ cd bind-9.19.13/
$ ./configure --with-libidn2
...
$ make
...
$ sudo make install
...
Literatur
-
[Wiki] Wikipedia: Make
-
[GNU] Free Software Foundation: GNU Make Manual
-
[Schreiner] Axel T. Schreiner: Professor Schreiners Unix Sprechstunde, Hanser Verlag, 1987
-
[Raymond] Eric Steven Raymond: The Art of Unix Programming, Addison Wesley, 2003, Kapitel 15