Automatisches Erzeugen der Abhängigkeiten mit gcc/g++

Als Ausgangspunkt der folgenden Gedanken dient das kleine makefile aus dem letzten Abschnitt:

CXX = g++             # der zu verwendende C++-Compiler
CXXFLAGS = -c         # Optionen zum Kompilieren von C++

CC = gcc              # Compiler für C und gleichzeitig Linker

haupt: haupt.o up.o db.o
        $(CXX) $^ -o $@

haupt.o: haupt.cpp up.h db.h

up.o: up.cpp up.h db.h

db.o: db.cpp db.h

.c.o:
        $(CXX) $(CFLAGS) $<

Das Linkkommando ist wirklich projektspezifisch, und kann beim Erstellen des makefile leicht festgelegt werden; es wird sich selten ändern, und eine solche Änderung ist auch nicht fehlerkritisch.

Alle anderen projektspezifischen Angelegenheiten (außer den Abhängigkeiten der Quelltexte) können mit Makrodefinitionen so angelegt werden, daß sie leicht wartbar sind.

Der einzige fehlerkritische Punkt besteht in den Abhängigkeiten der Quelltexte untereinander. Diese zu finden ist intellektuell nicht sehr anspruchsvoll: eine irgendwas.o hängt von irgendwas.cpp ab; weiterhin muß man den Quelltext nach #include "..."-Anweisungen durchsuchen, alle so gefundenen Dateinamen als weitere prerequisites vermerken, sowie die gefundenen Includedateien rekursiv wiederum nach #include "..."-Anweisungen durchsuchen.

Auch wenn diese Arbeit prinzipiell nicht schwierig ist, so ist sie doch ab einer gewissen Projektgröße mühsam und fehlerträchtig, zumal man bei jeder Änderung an #include "..."-Anweisungen das makefile sorgfältig korrigieren muß.

Da man eine solche mühsame, aber eigentlich nicht zu schwierige Aufgabe am besten delegiert, stellt sich die Frage, wem man das Herausfinden der Abhängigkeiten überlassen könnte. Es drängt sich geradezu auf, ein Programm zu schreiben (unter vernünftigen Systemen vielleicht als Shellskript?), welches die Regeln kennen muß, nach denen der Compiler Includedateien sucht, und dementsprechend die Abhängigkeiten liefert.

Wer mit einem GNU-System arbeiten darf, hat auch hier Glück: Der gcc beziehungsweise g++ kennen die Optionen -M und -MM. Damit aufgerufen kompilieren sie die angegebenen Quelltexte nicht, sondern liefern nur zur Standardausgabe die Abhängigkeiten, praktischerweise gleich im passenden Format für das makefile.

Die beiden Varianten unterscheiden sich darin, daß -M alle Includedateien auswertet, während -MM nur die mit "..." eingebundenen Dateien durchsucht (nicht die mit <...> eingebundenen).

Beispielaufruf:

klaus@aw38:~/skript_cpp_20011124/module > gcc -MM *.cpp
db.o: db.cpp db.h
haupt.o: haupt.cpp up.h db.h
up.o: up.cpp up.h db.h

Diese Ausgabe könnte man jetzt manuell in das makefile übernehmen; eleganter ist es aber, die Abhängigkeiten in einer eigenen Datei stehen zu lassen (beispielsweise dep.make) und stattdessen in das makefile eine include-Anweisung zu schreiben. Dadurch wird der Inhalt der angegebenen Datei beim Lesen des makefile mit ausgewertet. Der Vorteil dieser Vorgehensweise liegt darin, daß man jetzt eine eigene Datei für die Abhängigkeiten hat, die man als Target im Makefile angeben kann:

CXX = g++             # der zu verwendende C++-Compiler
CXXFLAGS = -c         # Optionen zum Kompilieren von C++
CC = gcc              # Compiler für C und gleichzeitig Linker

haupt: haupt.o up.o db.o
        $(CXX) $^ -o $@

dep.make:
        $(CXX) $(CFLAGS) -MM haupt.cpp up.cpp db.cpp > $@

include dep.make

Beim ersten Aufruf existiert die Includedatei noch nicht; weil aber eine Regel für das Target dep.make vorhanden ist, wird die Datei von make automatisch erzeugt.

Als Lohn der Mühe muß man sich nicht mehr im Detail um die Abhängigkeiten der Quelltexte kümmern, sondern braucht nur nach Änderungen an Includeanweisungen einmalig:
make dep.make
aufrufen, um das makefile zu aktualisieren.

AnyWare@Wachtler.de