SelfLinux Programmierung Make Abschnitt 5 | SelfLinux-0.13.0

| Make | Autor Formatierung GFDL

#### 6 Projekt-Management

Für kleinere Programmpakete ist das Makefile meist noch recht übersichtlich. Allerdings verleiten die vielen Möglichkeiten, die make bietet, dazu dass man sich schnell im Regel-Dschungel die Orientierung verliert und das Makefile immer mehr ein undurchsichtiges Eigenleben entwickelt. Und wehe, das Wissen über den Aufbau und Ablauf der Makefiles konzentriert auf einen einzelnen Spezialisten. Wenn dieser dann nicht mehr zur Verfügung steht, kann die weitere Pflege und Wartung der Makefiles den eigentlichen Entwicklungsaufwand übersteigen.

Während man bei kleineren Projekten das Makefile notfalls nochmals aufsetzen kann, ist dies bei größeren Projekten oft mit erheblichem Aufwand verbunden. Daher sollte man auch (oder gerade) bei Makefiles nach dem Motto

> So einfach wie möglich - aber nicht einfacher

handeln.

Dieses Kapitel beschäftigt sich mit verschiedenen Aspekten, wie man mit make größere Projekte verwalten kann. Später werden wir Makefile-Richtlinien kennenlernen, die die Einarbeitung und Wartung von (eigenen oder fremden) Makefiles erleichtern können.

#### 6.1 Schwierigkeiten

Einige der Probleme, die den Einsatz von make erschweren, sind:

- Verzeichnis-Baum
- Bedingte Kompilierung (#if ... #endif)
- versteckte Abhängigkeiten (zum Beispiel über Header-Dateien)
- Versionierung

Manche der Probleme resultieren aus Unzulänglichkeiten des Compilers, manche resultieren aus Annahmen und Beschränkungen einiger Unix-Werkzeuge. Diese spiegeln sich zum Teil in den eingebauten Suffix-Regeln wieder.

Ursprünglich war make nur zur Vereinfachung der Kompilierung gedacht, hat sich aber über die Jahre zu einem mächtigen Entwicklungswerkzeug gemausert. Nicht zuletzt auch deswegen, weil es inzwischen eine Reihe von Werkzeugen gibt, die um make herum gebaut wurden, um die Einschränkungen aufzuheben.

#### 6.2 Dummy-Ziele

In der Praxis werden Dummy-Ziele recht häuig eingesetzt, um mehrere Ziele zusammenzufassen:

    # compile all

    all : anna berta carmen

    anna : anna.c
            $(CC) -g -o anna anna.c

    berta : berta.c
            $(CC) -g -o berta berta.c

    carmen : carmen.c
            $(CC) -g -o carmen carmen.c

Der Entwickler braucht nur make all einzugeben und sämtliche Programme werden übersetzt.

#### 6.3 Timestamp-Ziele

Eine etwas subtilere Variante von Dummy-Zielen sind Timestamp-Targets. Damit werden Ziele bezeichnet, die zwar angelegt werden, aber nicht als Ziel gebraucht werden. In Wirklichkeit werden sie zur Synchronisation von Aktivitäten verwendet:

    TIMESTAMP.strip : anna berta carmen
            strip $?
            touch $@

Was macht dieses Ziel? Falls TIMESTAMP.strip nicht existiert oder eines der abhängigen Dateien anna, berta oder carmen neuer ist, werden die entsprechenden Dateien ge-strip-t (das strip-Kommando entfernt die Symbol-Tabelle aus dem Programm. Dadurch wird das Programm kürzer, kann aber dafür nicht mehr debuggt werden.) und danach TIMESTAMP.strip angelegt bzw. mit einem neuen Zeitstempel versehen (über das touch-Kommando).

Die Datei TIMESTAMP.strip dient also nur dazu, festzustellen ob anna, berta oder carmen schon einen strip" hinter sich haben. Diesen Trick findet man häufiger in Makefiles. Wenn Sie sich also schon immer gefragt haben, zu was Dateien der Größe 0 gut sein sollen, hier ist eine mögliche Antwort.

Allerdings hat diese Lösung auch einen Haken: man sieht der Datei TIMESTAMP.strip nicht an, zu was sie gut sein soll und ein ordnungsliebender Mensch könnte leicht auf die Idee kommen, diese Datei zu löschen, da sie die Größe 0 hat - weg damit! Daher ist es besser, dieser Datei einen sinnvollen Inhalt zu geben, damit sie

- eine Größe > 0 hat und
- damit der ahnungslose Benutzer einen Schimmer bekommt, zu was diese Datei gut sein könnte.

Dies kann man zum Beispiel durch folgende Regel erreichen:

    TIMESTAMP.strip : anna berta carmen
            strip $?
            echo "last strip of anna, berta or carmen:" > $@
            date >> $@

#### 6.4 Rekursives make

Am wenigsten problematisch ist es, wenn man sämtliche Dateien in einem einzigen Verzeichnis hat. Leider ist dieses Vorgehen bei größeren Projekten nicht praktikabel und üblicherweise hat man seine Dateien über mehrere Verzeichnisse verteilt.

#### 6.4.1 Verteiltes make

Eine Möglichkeit, mit dem Verzeichnisbaum fertig zu werden, besteht darin, in jedes Verzeichnis ein Makefile zu plazieren, das über das Makefile im übergeordneten Verzeichnis aufgerufen wird.

Das oberste Makefile könnte dabei folgendermaßen aussehen:

    SUBDIRS = src lib

    all :
            for d in $(SUBDIRS); do \
                (cd $$d; make all)  \
            done

Voraussetzung dafür ist natürlich, dass die drunterliegende Makefiles ein Ziel all besitzen.

#### 6.4.2 Weitergabe von Makros

Der rekursive Aufruf von make ist auch dazu geeignet, Informationen und Flags durchzureichen.

Beispiel:

    CFLAGS     = -O
    DEBUGFLAGS = -g $(CFLAGS)

    testbin :
            make bin "CFLAGS=$(DEBUGFLAGS)"

In diesem Beispiel wird durch make testbin dasselbe Makefile noch ein Mal aufgerufen, jedoch mit geänderten CFLAGS. Mit demselben Verfahren können auch Makros in drunterliegenden Makefiles überschrieben werden.

GNU-make und die meisten make-Versionen besitzen auch ein internes MAKE-Makro. Damit lautet die obere testbin-Regel:

    testbin :
            $(MAKE) bin "CFLAGS=$(DEBUGFLAGS)"

Der Vorteil des internen MAKE-Makros ist die Weitergabe der Optionen beim Aufruf von make. Wird beispielsweise make mit der Option -n aufgerufen, so wird damit auch alle weiteren makes mit -n aufgerufen (die Option -n zeigt nur die Kommandos an, führt sie aber nicht aus).

#### 6.4.3 Wichtige Makros

Jedes Makefile hat seine eigenen Makros. Um die Verwaltung und Verwirrung gering zu halten, sollte jedes Makefile dieselben Makro-Namen besitzen. Jeder make-Aufruf sollte wichtige Makros weiterreichen.

Beispiel:

    all :
            for d in $(SUBDIRS); do       \
                (cd $$d;                  \
                make all "CFLAGS=$(CLAGS) \
                    LDFLAGS=$(LDFLAGS)    \
                    LIBFLAGS=$(LIBFLAGS)) \
            done

#### 6.5 Header-Dateien

Objekt-Dateien hängen nicht nur von C-Sourcen, sondern auch von Header-Dateien ab, d.h. man müsste diese eigentlich mit in die Abhängigkeiten aufnehmen:

    love.o : love.c darling.h
            $(CC) love.c

Dies wird man aber in den seltensten Fällen in Makefiles antreffen, und zwar meist aus folgenden Gründen:

- Faulheit des Programmierers
- versteckte Abhängigkeiten
- zu dynamisch
- zu großer Overhead

Glücklicherweise erhält der Programmierer hier Unterstützung vom (GNU-)Compiler: Mit der Option -M generiert der Compiler eine Liste von Abhängigkeiten, die ins Makefile übernommen werden können:

    prompt% gcc -M love.c
    love.o: love.c darling.h

Einfacher geht es mit dem Programm makedepend:

    depend:
            makedepend -- $(CFLAGS) -- $(SRC_FILES)

Es fügt an das Ende des Makefiles die fehlenden Abhängigkeiten ein:

    love.o : love.c
            $(CC) love.c

    depend:
            makedepend -- $(CFLAGS) -- $(SRC_FILES)

    # DO NOT DELETE

    love.o: darling.h

Zusammen mit der ersten Abhängigkeit (love.o : love.c) wird jetzt love.c neu übersetzt, wenn sich love.c oder darling.h ändert.

#### 6.6 Globale Definitionen (include-Anweisung)

Viele Makefiles innerhalb verschiedener Verzeichnisse eines Projekts sehen sich in großen Teilen ähnlich: es werden die gleichen CFLAGS definiert, der gleiche Compiler aufgerufen, die gleichen Suffix-Regeln verwendet, usw. Was liegt näher, als diese Gemeinsamkeiten in einer gemeinsamen Datei zu verwalten?

Glücklicherweise kennen GNU-make und viele andere make-Varianten eine include-Anweisung, mit der diese gemeinsame Datei eingebunden werden kann:

    include common.mk

Hiermit wird die Datei common.mk eingebunden. Syntaktisch sieht das ganze dann so aus, dass diese Datei hier an diese Stelle hineinkopiert wird.

Bei der Verwendung der include-Anweisung ist darauf zu achten, das include am Zeilenanfang steht und dahinter mindestens ein Leerzeichen oder Tabulator-Zeichen folgt.