Debugging von Makefiles - Praktische Debugging-Tipps für die Entwicklung in der Praxis
Hinweis: Da dieser Artikel schon vor Jahren aus dem Internet verschwunden ist, bewahren wir ihn hier auf.
Von John Graham-Cumming
John ist Mitbegründer von Electric Cloud, einem Unternehmen, das sich auf die Verkürzung von Softwareentwicklungszeiten konzentriert. Er kann unter jgc@electric-cloud.com kontaktiert werden.
Donald Knuth sagte einmal, dass er, wenn er die Wahl zwischen zwei Sprachen hätte, diejenige mit dem besseren Debugger wählen würde. Aus diesem Grund würde er sich wahrscheinlich nie für Make entscheiden. Dessen Debugging-Möglichkeiten sind praktisch nicht vorhanden.
In diesem Artikel gehe ich auf dieses Problem ein, indem ich praktische Debugging-Tipps gebe, die ich zum Debuggen von Makefiles in der Praxis verwendet habe. Zur Veranschaulichung: Ich verwende GNU Make wegen seiner breiten Plattformunterstützung und seiner umfangreichen Sprache. Leider bietet diese reichhaltige Sprache nicht viele Möglichkeiten zum Debuggen. Tatsächlich scheint das Kommandozeilen-Flag -d von GNU Make eine Ausgabe zu produzieren, die für den Autor von GNU Make nützlicher ist als für jemanden, der Probleme mit Makefiles hat.
Die einzige GNU Make Kommandozeilenoption, die für das Debuggen von Makefiles wirklich nützlich ist, ist -p, und alles, was sie tut, ist, den Inhalt von GNU Make's eingebauten Regeln und Makros und alle Regeln oder Makros, die im Makefile definiert sind, auszugeben. Zugegeben, das ist eine enorme Menge an Informationen, die bei der Fehlersuche nützlich sein können, aber sie zählen kaum als Debugger.
Im Großen und Ganzen lassen sich die Probleme beim Debuggen von Makefiles in drei Kategorien einteilen:
- Wie hoch ist der Wert von Makro X und wie hat es ihn erhalten?
- Warum wurde die Datei foo erstellt?
- Wie hängt die Ausgabe der Make-Protokolldatei mit den Regeln im Makefile zusammen?
Was ist der Wert von Makro X?
Häufig erfordert das Debuggen von Makefiles die Ermittlung des Wertes eines Make-Makros. Da Make keinen Debugger hat, ist es schwierig, den Wert eines Makros herauszufinden, ohne die Makefiles nach Makrodefinitionen zu durchsuchen.
Sehen Sie sich dieses Beispiel-Makefile an:
X=$(YS) hate $(ZS) Y=dog YS=$(Y)$(S) Z=cat ZS=$(Z)$(S) S=s .PHONY: all all: @echo done
Wenn Sie die Definitionen von X, Y, S, YS und ZS verfolgen, können Sie feststellen, dass X den Wert Hunde hassen Katzen hat. In einem echten Makefile ist es so gut wie unmöglich, die Definition eines Makros zurückzuverfolgen. Der einfachste Weg, den Wert eines Makros in einem Makefile herauszufinden, ist die Verwendung der Funktion $(warning...). Wenn GNU Make auf $(warning...) stößt, gibt es die in der Warnung enthaltene Meldung zusammen mit dem Namen des Makefiles und der Zeilennummer aus. $(warning...) ist GNU Make's printf-Stilder Fehlersuche.
Das Hinzufügen von $(warning X is $(X)) als letzte Zeile dieses Makefiles veranlasst GNU Make zu drucken:
Makefile:11: X is dogs hate cats
Aber der printf-Stilist in Make genauso unflexibel wie in anderen Sprachen. Das Makefile muss ständig geändert und neu ausgeführt werden, während man nach der Ursache eines Problems sucht. Was wir brauchen, ist ein interaktiverer Ansatz.
Sie können eine GNU Make-Musterregel hinzufügen, die es ermöglicht, den Namen (oder die Namen) der zu druckenden Makros in der GNU Make-Kommandozeile anzugeben. Dieser Ansatz ist flexibler als die Modifikation des Makefiles; alles, was Sie brauchen, ist eine einzige Musterregel, um den Wert eines beliebigen Makros zu drucken. Das Hinzufügen dieser Zeile zum vorherigen Beispiel-Makefile bedeutet, dass es möglich ist, make print-X einzugeben, um den Wert von X zu erhalten , oder sogar make print-X print-XS print-Y, um die Werte einer Reihe von Makros zu erhalten:
print-%: ; @echo $* is $($*)
Das funktioniert, weil GNU Make versucht, die in der Befehlszeile angegebenen Ziele zu erzeugen. Wenn Sie make print-X eingeben, entspricht das Ziel print-X der Regel print-%, wobei % dem Namen der Variablen (in diesem Fall X) entspricht. Der mit % übereinstimmende Teil wird in der automatischen GNU Make-Variable $* gespeichert ; $* wird im Befehl für die print-%-Regel zweimal verwendet: Zuerst wird es verwendet, um den Namen der Variablen zu drucken, dann wird es verwendet, um den Wert der Variablen zu erhalten, indem $($*) geschrieben wird . Wenn $($*) gesehen wird, expandiert GNU Make $* (zum Beispiel zu X) und expandiert dann die resultierende Makroreferenz (zum Beispiel $(X)).
Es gibt zwei Probleme mit dem Druck-%-Stil:
- Wenn sich der Wert des Makros innerhalb eines Makefiles ändert (was ein ungewöhnliches, aber nicht unmögliches Ereignis ist), gibt print-% nur den endgültigen Wert des Makros an (den Wert des Makros, nachdem alle Makefiles geparst wurden). Die einzige gute Lösung für dieses Problem ist die Rückkehr zum $(warning...) -Stil.
- GNU Make lässt auch Makros mit lokalem Geltungsbereich in Form von zielspezifischen Makros zu. Das Makefile-Beispiel kann modifiziert werden, um einen zielspezifischen Wert für das X-Makro in den Geltungsbereich der all-Regel aufzunehmen:
print-%: ; @$(error $* is $($*)) $(filter-out print-%, $(MAKECMDGOALS)): $(filter print-%, $(MAKECMDGOALS))
Erstens wurde die print-%-Regel so geändert, dass sie, wenn sie ausgeführt wird, die gleiche Meldung wie vorher ausgibt, aber GNU Make mit einem Fehler anhält. Dies ist erforderlich, weil man, um den zielspezifischen Wert des Makros zu erhalten, GNU Make sagen muss, dass es diese Ziele bauen soll, und da man eigentlich nicht will, dass GNU Make baut (man will nur, dass der zielspezifische Makrobereich eingerichtet wird), stoppt der mit $(error...) erzeugte fatale Fehler GNU Make, sobald es den Wert des angeforderten Makros gedruckt hat.
Um den zielspezifischen Makroumfang zu erhalten, gibt die verbesserte Version an, dass die print-%-Regel eine Voraussetzung für das Ziel ist, für das es ein zielspezifisches Makro geben kann. Dies wird mit der normalen GNU Make-Syntax gemacht. Wenn Sie zum Beispiel den Wert von X aus der all-Regel heraus drucken wollen, geben Sie make all print-X ein. Die zweite Zeile in dieser Verbesserung wird übersetzt mit:
all: print-X
Der $(filter-out...) entfernt zunächst alles in der Befehlszeile (gespeichert in der GNU Make-Variablen MAKECMDGOALS), was nicht dem Muster print-% entspricht (zum Beispiel nur die all in all print-X); der $(filter...) macht das Gegenteil: Es behält nur die Dinge, die mit print-% übereinstimmen (z.B. nur das print-X in all print-X).
Da zielspezifische Makros an alle Voraussetzungen eines Ziels vererbt werden, bedeutet das Schreiben von all: print-X, dass die Regel print-X (die von der Musterregel print-% behandelt wird) alle zielspezifischen Makros für all definiert hat. Das ist genau das, was Sie wollen, und die Eingabe von all : print-X im oben genannten Beispiel gibt alles aus:
Makefile:12: *** X is cats love dogs. Stop.
Die zielspezifische, makrofähige Version von print-% kann immer noch den globalen Wert eines Makros ausgeben, indem sie alle Ziele in der Befehlszeile auslässt. Wenn Sie zum Beispiel make print-X eingeben, wird der globale Wert von X ausgegeben:
Makefile:12: *** X is dogs hate cats. Stop.
Neben dem Wert eines Makros kann es auch hilfreich sein, herauszufinden, wo es definiert wurde (z.B. wurde es in einem Makefile, in der Umgebung oder vielleicht auf der Make-Befehlszeile überschrieben).
GNU Make bietet die Funktion $(origin...) , die einen String zurückgibt, der beschreibt, wie ein Makro definiert wurde. Eine Änderung der print-%-Regel bewirkt, dass diese Information zusätzlich zum Wert des Makros ausgegeben wird:
print-%: ; @$(error $* is $($*) (from $(origin $*)))
Und so macht print-X jetzt Drucke:
Makefile:12: *** X is dogs hate cats (from file). Stop.
Das (from file) zeigt an, dass X in einem Makefile definiert wurde. Käme X aus der Umgebung, würde es (from environment) heißen. Wenn X in der Make-Befehlszeile überschrieben würde - zum Beispiel durch die Eingabe von make print-X X=foo -würden Sie diese Ausgabe sehen:
Makefile:12: *** X is foo (from command line). Stop.
Eine letzte Verbesserung von print-% besteht darin, dass es sowohl die Definition als auch den Wert eines Makros ausgeben kann. GNU Make's verwirrend benannte Funktion $(Wert...) macht genau das:
print-%: ; @$(error $* is $($*) ($(value $*)) (from $(origin $*)))
Wenn Sie nun make print-X eingeben, erhalten Sie diese nützliche Debugging-Ausgabe:
Makefile:12: *** X is dogs hate cats ($(YS) hate $(ZS)) (from file). Stop.
Mehr über das Debuggen von GNU Make-Makros finden Sie in meinem Artikel "Makefile Debugging": Tracing Macro Values", der zeigt, wie man GNU Make dazu bringt, jede Stelle, an der ein Makro in einem Makefile verwendet wird, mitzuteilen.
Warum wurde File foo gebaut?
Hier ist ein Beispiel für ein Makefile, das zwei Objektdateien (foo.o und bar.o) aus den entsprechenden C-Quelldateien (foo.c und bar.c) erzeugt. Die beiden Objektdateien werden miteinander verknüpft, um eine ausführbare Datei namens "runme" zu erzeugen:
.PHONY: all all: runme runme: foo.o bar.o @$(LINK.o) $^ -o $@ foo.o: foo.c bar.o: bar.c %.o: %.c @$(COMPILE.C) -o $@ $<
Nehmen wir nun an, dass runme, foo.o und bar.o alle auf dem neuesten Stand sind. Die Änderung von foo.c erzeugt eindeutig foo.o neu und verknüpft runme neu. In diesem Beispiel können Sie die Kette der Abhängigkeiten verfolgen und sehen, dass die Neuverknüpfung von runme durch eine Änderung von foo.c verursacht worden sein könnte. In einem größeren Makefile ist es nahezu unmöglich, Abhängigkeiten zu verfolgen.
Wenn Sie die Option -d von GNU Make verwenden, um die Kette zu verfolgen, die foo.c dazu veranlasst, foo.o zu bauen, was wiederum runme dazu veranlasst, sich neu zu verlinken, müssen Sie 556 Zeilen an Debugging-Informationen durchgehen. Stellen Sie sich vor, Sie versuchen das Gleiche mit einem echten Makefile. Eine kleine Änderung im Makefile bewirkt jedoch, dass GNU Make genau diese Kette von Ereignissen aufzeigt. Wenn man folgendes in das Makefile einfügt, gibt GNU Make Informationen über jede Regel aus, die ausgeführt wird und warum sie ausgeführt wurde:
OLD_SHELL := $(SHELL) SHELL = $(warning [$@ ($^) ($?)])$(OLD_SHELL)
Hier ist die Ausgabe, wenn foo.c gerade geändert wurde, aber alles andere auf dem neuesten Stand war:
Makefile:11: [foo.o (foo.c) (foo.c)] Makefile:5: [runme (foo.o bar.o) (foo.o)]
Diese Zeilen enthalten eine Menge an Informationen. Jede Zeile enthält den Namen des Makefiles und die Zeilennummer der Regel, die gerade ausgeführt wird (Zeile 11 ist die Musterregel %.o: %.c , die verwendet wird, um foo.o aus foo.c zu bauen, und Zeile 5 ist die Regel, um runme zu linken). Innerhalb der eckigen Klammern gibt es drei Informationen: das Ziel, das gebaut wird (zuerst baut das Makefile foo.o und dann runme); dann die vollständige Liste der Voraussetzungen dieses Ziels (also wird foo.o aus foo.c gebaut und runme wird sowohl aus foo.o als auch aus bar.o gebaut); schließlich gibt es die Namen aller Voraussetzungen, die neuer sind als das Ziel (foo.c ist neuer als foo.o und foo.o ist neuer als runme).
Die zweite Information in Klammern gibt den Grund an, warum ein Ziel erstellt wurde: Es ist die Liste der neueren Voraussetzungen, die eine Neuerstellung erzwingen. Wenn die Klammern leer sind, bedeutet dies, dass es keine neueren Voraussetzungen gibt und dass das Ziel neu erstellt wird, weil es nicht existiert.
Wie das funktioniert, nenne ich den "SHELL-Hack ". GNU Make speichert den Pfad der Shell, die es zum Ausführen von Befehlen verwendet, in der Variable SHELL. Es ist möglich, SHELL so zu verändern, dass Sie Ihre eigene Shell einstellen können. Wenn Sie das tun, prüft GNU Make den Wert von SHELL jedes Mal, wenn es eine Regel ausführen will. Diese Überprüfung pro Regel gibt uns die Möglichkeit, etwas auf Basis der einzelnen Regeln zu tun. Im oben erwähnten Code wird die aktuelle Shell in OLD_SHELL gespeichert, indem der Wert von $(SHELL) mit einer sofortigen Zuweisung (:=) abgegriffen wird. Dann wird die SHELL modifiziert, indem sie mit einem Aufruf von $(warning...) vorangestellt wird . $(warning...) verwendet drei automatische Variablen von GNU Make, um den Namen des zu erstellenden Ziels ($@), die komplette Liste der Voraussetzungen ($^) und die Liste der Voraussetzungen, die neuer als das Ziel sind ($?), auszugeben.
Da GNU Make jedes Mal SHELL expandiert, wenn es eine Regel ausführen will, und zwar nachdem es die automatischen Variablen für die Regel gesetzt hat, wird das $(warning...) expandiert, was die oben gezeigte Ausgabe ergibt. Dieser SHELL-Hack ist sehr mächtig und kann sehr effektiv eingesetzt werden, um die Ausgabe der GNU Make-Logdatei mit Makefiles zu verknüpfen.
Wie hängt die Ausgabe von Protokolldateien zusammen?
Im vorigen Abschnitt erzeugte ein kompletter Build (z.B. wenn runme, foo.o und bar.o vor der Eingabe von make fehlten) keine Ausgabe. Das liegt daran, dass allen Befehlen im Makefile ein @ vorangestellt wurde , was GNU Make daran hindert, sie als Echo auszugeben.
Die Option -n von GNU Make bietet eine Möglichkeit, alle Befehle zu echoen, ohne sie auszuführen. Dies ist praktisch für Trockenübungen des Makefiles. Die Ausgabe von make -n für das Beispiel-Makefile ist:
g++ -c -o foo.o foo.c g++ -c -o bar.o bar.c cc foo.o bar.o -o runme
In diesem Beispiel ist es relativ einfach, diese Zeilen mit bestimmten Regeln im Makefile in Verbindung zu bringen; bei großen, realen Makefiles kann dies jedoch schwierig sein. Und da -n diese Befehle nicht wirklich ausführt, ist es keine Hilfe, wenn man versucht herauszufinden, warum eine Regel nicht korrekt ausgeführt wurde.
Eine Lösung ist, jedes @ durch $(AT) zu ersetzen . Setzt man AT im Makefile auf @, verhält es sich wie vorher. Wird AT auf leer gesetzt, werden die Befehle als Echo ausgegeben. Das Makefile kann zum Beispiel so geändert werden, dass $(AT) statt @ verwendet wird :
.PHONY: all all: runme runme: foo.o bar.o $(AT)$(LINK.o) $^ -o $@ foo.o: foo.c bar.o: bar.c %.o: %.c $(AT)$(COMPILE.C) -o $@ $< AT := @
Dies verhält sich genauso wie zuvor, außer dass es möglich ist, die Definition von AT auf der Kommandozeile zu überschreiben, so dass GNU Make die Befehle ausgibt, die es ausführt. Wenn Sie make AT= ausführen, wird das @ von jedem Befehl entfernt und die gleiche Ausgabe wie bei make -n erzeugt, nur dass die Befehle tatsächlich ausgeführt werden.
Natürlich ist es unrealistisch, jeden Befehl in einem Makefile zu ändern.
Der SHELL-Hack kann modifiziert werden, um GNU Make zur Ausgabe von Befehlsinformationen zu veranlassen. Fast alle Shells haben eine Option -x, die sie dazu veranlasst, die Befehle, die sie gerade ausführen wollen, auszugeben. Indem man SHELL so modifiziert, dass es -x enthält (und ein $(warning...)), das die Position der Regel im Makefile ausgibt) und die Modifikation in ein ifdef einpackt, kann man detaillierte Debugging-Informationen je nach Bedarf aktivieren/deaktivieren. Hier ist der SHELL-Hack für den Befehlsdump:
ifdef DUMP OLD_SHELL := $(SHELL) SHELL = $(warning [$@])$(OLD_SHELL) -x endif
Wenn DUMP definiert ist, wird SHELL geändert, um die Option -x hinzuzufügen und den Namen des Ziels auszugeben, das gerade gebaut wird. Wenn foo.o, bar.o und runme fehlen, erzeugt die Eingabe von make DUMP=1 diese Ausgabe unter Verwendung des SHELL-Hacks:
Makefile:11: [foo.o] + g++ -c -o foo.o foo.c Makefile:11: [bar.o] + g++ -c -o bar.o bar.c Makefile:5: [runme] + cc foo.o bar.o -o runme
Jede zu erstellende Datei steht in eckigen Klammern mit dem Namen des Makefiles und dem Ort, an dem die Regel zum Erstellen dieser Datei zu finden ist. Die Shell gibt den genauen Befehl aus, mit dem die Datei gebaut werden soll (mit einem vorangestellten +).
Mit nur vier Zeilen GNU Make-Code können Sie eine leere Protokolldatei in eine Datei mit vollständigen Datei-, Zeilennummer- und Befehlsinformationen umwandeln. Dies ist nützlich bei der Fehlersuche in einer fehlgeschlagenen Regel. Führen Sie einfach das Makefile mit DUMP=1 erneut aus, um herauszufinden, was gebaut wurde und wo es sich im Makefile befand. Der einzige Nachteil des SHELL-Hacks ist, dass er GNU Make verlangsamt. Das liegt daran, dass GNU Make versucht, die Verwendung der Shell zu vermeiden, wenn es möglich ist; wenn GNU Make zum Beispiel einen einfachen g++-Aufruf durchführt, dann kann es die Shell vermeiden und den Befehl direkt ausführen. Wenn SHELL in einem Makefile geändert wird, schaltet GNU Make diese Optimierung aus.
Zusätzliche Ressourcen
Wenn diese Ideen Ihren Appetit auf ein besseres GNU Make Debugging anregen, schauen Sie sich das GNU Make Debugger Projekt(http://gmd.sf.net) an. GMD ist ein interaktiver Debugger für GNU Make, der vollständig mit GNU Make-Funktionen geschrieben wurde und Haltepunkte sowie die Möglichkeit bietet, den Wert und die Definition jedes Makros zur Laufzeit zu untersuchen.
Und wenn Sie etwas wirklich Cooles auf der Grundlage des SHELL-Hacks machen, lassen Sie es mich wissen. Ich würde mich freuen, davon zu hören.