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:

Die Funktion main() in bsp.c
/** bsp.c **/
# include "a.h"
# include "b.h"

int     main (int argc, char*argv[])
{
        afunc ("main()");
        bfunc ("main()");
        return 0;
}
Die Quelltextdatei für afunc() …
/** 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()");
}
...und bfunc()
/** 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:

  1. Übersetzten der einzelnen Quelltextdateien in Objektcode durch den entsprechenden Compiler (in unserem Fall cc)

  2. 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 Datei bsp.o erzeugen

    $  cc -c bsp.c
  • Aus a.c die Datei a.o erzeugen

    $  cc -c a.c
  • Aus b.c die Datei b.o erzeugen

    $  cc -c b.c
  • Aus bsp.o, a.o und b.o das Programm bsp 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

  1. die dazu gehörende Objektdatei noch nicht existiert, oder

  2. 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
Zusätzliche Abhängigkeiten

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:

Dependency Tree
bsp.c a.h b.h cc -c -o bsp.o bsp.c bsp.o a.c a.h b.h cc -c -o a.o a.c a.o b.c b.h cc -c -o b.o b.c b.o cc -o bsp bsp.o a.o b.o bsp

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:

Basic Makefile
##      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:

Optimiertes Makefile
##      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 mit Variablen
##      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 mit Pseudozielen
##      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 oder groff

  • Größenanpassungen von Bilddateien (z.B. für Webseiten mittels convert)

  • Grafiken generieren über little Languages wie z.B. Graphiz (dot) oder pic

  • 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.

Die wichtigsten make-Optionen
-f file

Die Konfiguration nicht aus makefile oder Makefile sondern aus der angegebenen Datei entnehmen

-n

(no execution)
Keine Kommandos ausführen

-s

(silent)
Keine Kommandos bei der Ausführung anzeigen

-t

(touch)
Das Modifikationsdatum der Ziele wird auf den aktuellen Stand gebracht, ohne die Kommandos auszuführen

-p

(print)
Die eingebauten Regeln und Makrodefinitionen werden auf dem Bildschirm ausgeben.

-q

(query)
Setzt nur den Rückgabewert von Make auf 0 ⇒ "up to date" oder 1

-r

(rules)
Die eingebauten Regeln werden nicht verwendet.

-j n

(jobs)
Paralleles Make mit n Prozessen.

-k

(keep going)
Auch nach dem ersten Fehler noch weitermachen

-B

(always make)
Ziel auf jeden Fall neu erstellen

-W file

(what if)
Was müsste gemacht werden wenn file neueren Datums wäre

--trace

(trace)
Zeigt die Informationen warum ein Ziel neu gemacht werden muss (die Regel) und welches Kommando dazu benutzt wird.

-p

(print-data-base)
Zeigt alle Regeln, auch die Eingebauten. Möchte man ausschließlich diese sehen gibt man /dev/null als Makefile an (make -p -f /dev/null)
[Will man den Einfluss der Umgebungsvariablen ebenfalls ausschließen sollte der Aufruf env - make -f /dev/null -p 2>/dev/null lauten]

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