Makefiles anhand der besten Beispiele lernen
(makefiletutorial.com)- Makefiles werden vor allem genutzt, um die C/C++-Kompilierung großer Projekte zu automatisieren, indem nur die nötigen Build-Schritte anhand von Dateiänderungen erneut ausgeführt werden
- Make vergleicht die Dateisystem-Zeitstempel von Targets und Abhängigkeiten und führt Befehle nur aus, wenn das Target fehlt oder eine Abhängigkeit neuer ist
- Eine Grundregel besteht aus
targets: prerequisitesund per Tab eingerückten Befehlen; Einrückung mit Leerzeichen kann dazu führen, dassmakefehlschlägt - Variablen, Wildcards, automatische Variablen, Pattern-Regeln, Bedingungen, Funktionen, rekursives Make,
include,.PHONYund.DELETE_ON_ERRORkönnen den Build-Ablauf erweitern - Die Cookbook-Beispiele erzeugen aus C/C++-Quellen in
src/Build-Artefakte inbuild/und richten mit-MMD -MPsowie-includeeine automatische Abhängigkeitsverwaltung ein
Wann man ein Makefile braucht
- Makefiles werden in großen Programmen verwendet, um zu entscheiden, welche Dateien neu kompiliert werden müssen
- Das zentrale Beispiel des Tutorials ist die Kompilierung von C/C++-Dateien
- Make kann auch außerhalb der Kompilierung für Aufgaben genutzt werden, bei denen abhängig von Dateiänderungen eine Reihe von Befehlen ausgeführt werden soll
- Als andere Build-Tools für C/C++ werden SCons, CMake, Bazel und Ninja vorgestellt
- Für Java gibt es Ant, Maven und Gradle; Sprachen wie Go, Rust und TypeScript haben eigene Build-Tools
- Interpretersprachen wie Python, Ruby und reines JavaScript müssen bei Dateiänderungen nicht neu kompiliert werden, daher sind Werkzeuge wie Makefiles dort nicht zwingend erforderlich
Grundlegendes Ausführungsmodell von Make
- Die Beispiele funktionieren, indem man in einem Terminal mit installiertem
makeeine Datei namensMakefileerstellt und dann in diesem Verzeichnismakeausführt - Eine Grundregel folgt dieser Form
targets: prerequisites- ein mit Tab beginnender
command
- Ein Target ist meist ein Dateiname; die Befehle sind die Schritte, um dieses Target zu erzeugen
- Prerequisites sind Dateien, die vor der Befehlsausführung vorhanden sein müssen, und werden auch Dependencies genannt
- Befehlszeilen in einem Makefile müssen zwingend mit einem Tab-Zeichen eingerückt werden; bei Leerzeichen schlägt
makefehl
Zeitvergleich von Targets und Abhängigkeiten
make helloführt die mit dem Targethelloverknüpften Befehle aus, aber nicht, wenn bereits eine Dateihelloexistiert- Target und Dateiname sind direkt miteinander verknüpft; üblicherweise erzeugen die Befehle eines Targets eine Datei mit demselben Namen wie das Target
- In der Regel
blah: blah.cwird der Kompilierbefehl nur ausgeführt, wennblahfehlt oderblah.cneuer ist alsblah - Make verfolgt Änderungshistorien nicht direkt, sondern verwendet Dateisystem-Zeitstempel als Heuristik
- Wenn man eine Datei ändert und danach ihre Änderungszeit in die Vergangenheit setzt, kann Make fälschlich annehmen, dass die Datei nicht geändert wurde
Default-Target und clean
- Wenn bei
makekein Target angegeben wird, wird das erste Target im Makefile als Standard ausgeführt - Das Target
allwird verwendet, um mehrere Targets auf einmal zu erstellen; steht es als erste Regel, baut schon ein einfachesmakealle Targets cleanwird üblicherweise als Target zum Löschen von Build-Artefakten verwendet, ist aber kein spezielles reserviertes Wort in Make- Wenn
cleanweder das erste Target noch eine Abhängigkeit eines anderen Targets ist, muss es explizit wiemake cleanaufgerufen werden, damit es ausgeführt wird - Wenn tatsächlich eine Datei namens
cleanexistiert, wird das Targetcleanmöglicherweise nicht ausgeführt; das wird später mit.PHONYgelöst
Variablen und String-Verarbeitung
- Make-Variablen können nur Strings enthalten; das Tutorial empfiehlt allgemein die Verwendung von
:= - Variablenreferenzen verwenden die Form
$(name)oder${name} - Einfache und doppelte Anführungszeichen haben in Make selbst keine besondere Bedeutung, sondern sind nur Zeichen, die in einer Variable stehen
- Es gibt im Wesentlichen zwei Arten, Variablen zu definieren
=: rekursive Variable, bei der Variablen bei der Verwendung gesucht und expandiert werden:=: einfach expandierte Variable, bei der nur Werte expandiert werden, die zum Definitionszeitpunkt bereits definiert sind
?=setzt einen Wert nur für Variablen, die noch nicht gesetzt sind+=fügt einer Variable einen Wert hinzu- Nicht definierte Variablen werden als leerer String behandelt
- Leerzeichen am Zeilenende werden nicht entfernt; um eine Variable mit genau einem Leerzeichen zu erzeugen, kann man Techniken wie
$(nullstring)verwenden
Wildcards und automatische Variablen
- In Make werden sowohl
*als auch%als Wildcards bezeichnet, haben aber unterschiedliche Bedeutungen *sucht im Dateisystem nach passenden Dateinamen; statt es direkt in Variablendefinitionen zu verwenden, ist es sicherer, es wie$(wildcard *.o)in die wildcard-Funktion einzuschließen- Wenn
*zu keiner Datei passt, kann es außerhalb derwildcard-Funktion unverändert stehen bleiben %wird in Regeln und bestimmten Funktionen verwendet; der passende Teil wird Stem genannt- Häufig verwendete automatische Variablen sind:
$@: Name des Targets$?: Abhängigkeiten, die neuer sind als das Target$^: alle Abhängigkeiten$<: die erste Abhängigkeit
Implizite Regeln und Pattern-Regeln
- Make besitzt implizite Regeln für die C-Kompilierung, sodass Builds auch ohne explizit angegebene Kompilierbefehle ablaufen können
- Typische implizite Regeln haben folgende Form
n.owird ausn.cin der Form$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@erzeugtn.owird ausn.ccodern.cppin der Form$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@erzeugtnwird ausn.oper Linker-Befehl erzeugt
- Wichtige Variablen in impliziten Regeln sind
CC,CXX,CFLAGS,CXXFLAGS,CPPFLAGSundLDFLAGS - Statische Pattern-Regeln haben die Form
targets...: target-pattern: prereq-patterns ...; der bei%im Target passende Stem wird in die Abhängigkeits-Patterns eingesetzt - Pattern-Regeln enthalten wie
%.o: %.cein%im Target und können wie selbst definierte implizite Regeln verwendet werden - Doppel-Doppelpunkt-Regeln
::erlauben mehrere Regeln für dasselbe Target, werden aber selten verwendet
Befehlsausführung und Fehlerbehandlung
- Wenn vor einem Befehl
@steht, wird der Befehl selbst nicht ausgegeben make -sverhält sich so, als stünde vor jeder Zeile ein@- Jede Befehlszeile wird effektiv in einer neuen Shell ausgeführt, daher wirkt sich ein
cd ..in einer Zeile nicht auf die nächste Zeile aus - Um denselben Shell-Zustand beizubehalten, muss man es in eine Zeile schreiben, etwa
cd ..; echo \pwd``, oder Zeilen mit Backslashes verbinden - Die Standardshell ist
/bin/shund kann beispielsweise mitSHELL=/bin/bashgeändert werden - Um eine Shell-Variable mit
$innerhalb eines Makefiles zu verwenden, nutzt man$$ - Die Fehlerbehandlung hängt von der Ausführungsweise ab
make -k: führt trotz Fehlern andere mögliche Aufgaben weiter aus-vor einem Befehl: ignoriert Fehler dieses Befehlsmake -i: wendet Fehlerignorieren auf alle Befehle an
- Wenn Make mit
ctrl+cunterbrochen wird, löscht Make das gerade erstellte aktuelle Target
Rekursives Make und Umgebungsvariablen
- Wenn innerhalb eines Makefiles erneut make aufgerufen wird, sollte statt
makedie spezielle Variable$(MAKE)verwendet werden $(MAKE)reicht Make-Flags weiter und behandelt die Auswirkungen von Flags angemessen- Beim Start erzeugt Make automatisch Make-Variablen aus den zu diesem Zeitpunkt gesetzten Umgebungsvariablen
- Die Direktive
exportsetzt Variablen in der Shell-Umgebung der Befehle aller Recipes - Damit Variablen auch in einem untergeordneten Make verfügbar sind, müssen sie
exportiert werden - Mit
.EXPORT_ALL_VARIABLESkönnen alle Variablen exportiert werden - Wenn mehrere Targets gleichzeitig angegeben werden, etwa
make clean run test, werden sie in der Reihenfolgeclean,run,testausgeführt
Bedingungen und Funktionen
- Bedingungen bestehen aus
ifeq,ifneq,ifdef,ifndef,elseundendif ifdefexpandiert keine Variablenreferenz, sondern prüft nur, ob eine Variable definiert ist- Mit
$(MAKEFLAGS)undfindstringkann geprüft werden, ob Flags wiemake -iweitergegeben wurden - Make-Funktionen werden vor allem zur Textverarbeitung eingesetzt und in der Form
$(fn, arguments)oder${fn, arguments}aufgerufen - Wichtige Funktionen sind:
subst: String-Ersetzungpatsubst: Ersetzung von Wörtern nach Patternforeach: Transformation einer durch Leerzeichen getrennten Wortlisteif: nutzt das zweite Argument, wenn das erste nicht leer ist, sonst das drittecall: ruft eine Variable wie eine einfache Funktion auf und verwendet Parameter wie$(1)und$(2)shell: ruft die Shell auf, ersetzt Zeilenumbrüche aber durch Leerzeichenfilter: wählt Elemente aus einer Liste aus, die einem bestimmten Pattern entsprechenfilter-out: wählt Elemente aus, die einem bestimmten Pattern nicht entsprechen
include, vpath, .PHONY, .DELETE_ON_ERROR
include filenames...veranlasst Make, ein oder mehrere andere Makefiles einzulesenincludeist nützlich, wenn Makefiles gelesen werden sollen, die durch Optionen wiegcc’s-Merzeugt wurdenvpath <pattern> <directories>gibt Verzeichnisse an, in denen nach bestimmten Abhängigkeitsdateien gesucht wird- Mit der Variable
VPATHkann ein ähnliches Suchpfadverhalten global festgelegt werden - Der Backslash
\wird verwendet, um lange Befehle auf mehrere Zeilen aufzuteilen - Wenn man ein Target zu
.PHONYhinzufügt, verwechselt Make dieses Target nicht mit einer gleichnamigen Datei .DELETE_ON_ERRORlöscht das Target einer Regel, wenn während der Regelausführung ein Befehl einen Exit-Status ungleich 0 zurückgibt.DELETE_ON_ERRORgilt nicht wiePHONYnur für das direkt davor stehende Target, sondern für alle Targets
Makefile-Cookbook-Beispiel
- Das Cookbook-Beispiel bietet ein Makefile-Template für mittelgroße Projekte
- Wenn C/C++-Dateien im Ordner
src/liegen, bestimmt das Makefile automatisch Kompilierziele und Abhängigkeiten - Die wichtigsten Variablen sind:
TARGET_EXEC := final_programBUILD_DIR := ./buildSRC_DIRS := ./src
SRCSfindet mitfindund$(shell ...)Dateien wie*.cpp,*.cund*.sOBJSerzeugt Build-Artefaktpfade, indem jedem QuelldateinamenBUILD_DIRvorangestellt und.oangehängt wirdDEPSersetzt.odurch.dund erzeugt so die Liste der Abhängigkeits-MakefilesINC_DIRSfindet alle Verzeichnisse untersrc/, undINC_FLAGSstellt jedem Verzeichnis das Präfix-IvoranCPPFLAGS := $(INC_FLAGS) -MMD -MPkonfiguriert GCC so, dass.d-Abhängigkeitsdateien erzeugt werden- C- und C++-Quellen erzeugen jeweils mit separaten Pattern-Regeln
.o-Dateien unterbuild/; benötigte Verzeichnisse werden mitmkdir -p $(dir $@)erstellt cleanwird als.PHONYdeklariert und löscht mitrm -r $(BUILD_DIR)das Build-Verzeichnis-include $(DEPS)bindet.d-Dateien ein, unterdrückt aber Fehler, wenn sie wie beim ersten Build noch nicht existieren
1 Kommentare
Hacker-News-Kommentare
Es gibt einige wenig bekannte, aber nützliche
make-Flags. Output-Synchronisierung sorgt dafür, dass stdout/stderr erst ausgegeben werden, nachdem ein Target abgeschlossen ist, wodurch sich das Problem schwer lesbarer, bei parallelen Builds vermischter Logs verringertmake --output-sync=recurse -j10Auf stark ausgelasteten Mehrbenutzersystemen kann die Anzahl der
-j-Jobs allein nicht ausreichen, daher lässt sich die Parallelität auch anhand des Load Average begrenzen:make -j10 --load-average=10Auch
--shuffle, das die Reihenfolge der Target-Planung zufällig macht, ist in CI nützlich, um fehlende Abhängigkeiten in Makefiles aufzuspüren:make --shuffle # or --shuffle=seed/reverse-B, womit alles ohne Bedingungen komplett neu gebaut wirdmake -jschon mehrfach erlebt, wie es eine Maschine praktisch in einen DoS-Zustand versetzt, daher betrachte ich es fast als BugIch habe um 1985 im Graphics Lab der Boston University jemanden gesehen, der mit einem Makefile die Erzeugung eines 3D-Renderers für Animationen antrieb. Er kam aus der Lisp-Ecke und arbeitete an früher prozeduraler Generierung sowie einem 3D-Schauspielersystem, und das Makefile war mit insgesamt etwa 10 Zeilen ausgesprochen elegant
Nur mit einfachen Datei-Zeitstempel-Abhängigkeiten wurden Hunderte von Animationen erzeugt; Lisp erstellte die 3D-Form jedes Frames, und Make erzeugte dann die Frames
Das war 1985, also in einer Zeit, in der fast nichts von dem selbstverständlich war, was heute in 3D und Animation normal ist, und es verblüffte alle. Soweit ich mich erinnere, nutzte er später den 3D-Renderer für Iron Giant und spielte auch bei Coraline eine zentrale Rolle. Es war Brian Gardner
Make ist eines der Tools, bei denen ich sehr froh bin, es früh in meiner Laufbahn gelernt zu haben. Ich nutze es heute nicht mehr oft, aber es hat mir die Stärke deklarativer Systeme im Vergleich zu imperativen Systemen gezeigt
Mir wurde klar, dass sich diese Idee ganz natürlich auf andere Aufgaben ausdehnen lässt. Wenn man sich die Grafik oben auf dieser Seite ansieht, scheint der Autor ähnlich zu empfinden: Statt ein Kochrezept als skriptartige Abfolge traditioneller Anweisungen zu schreiben, kann man es als Makefile deklarativ betrachten und so besser verstehen
Verwandter Artikel: https://blog.gpkb.org/posts/cooking-with-make/
Ich schreibe Rezepte immer so auf, dass ich sie wie ein Makefile lesen und mit in die Küche nehmen kann, und frage mich, ob jemand versucht hat, sie auf diese Weise zu setzen oder darzustellen. Das würde viel Zeit sparen, weil man beim Lesen neuer Rezepte das Skript nicht erst im Kopf in ein Makefile umwandeln müsste
Der Preis dafür ist natürlich, dass man den Abhängigkeitsgraphen selbst auflösen muss. Wenn man einfach nur vorab serialisierten, sequenziellen Schritten folgen möchte, lädt man sich damit mehr Arbeit auf
Im Artikel heißt es, die meisten Rezepte würden nicht als
.PHONYmarkiert, und das wird so dargestellt, als sei das ein Grund, es im Tutorial nicht eigens zu behandeln, aber das ist eine schwache Ausrede. Ein Tool sollte die korrekte Verwendung lehrenIn meinem Team nutzen wir make als Task-Runner, und ich musste mir schon einiges anhören, weil ich darauf bestanden habe,
.PHONYzu allen Rezepten hinzuzufügen und das beizubehaltenDer Makefile-Styleguide von Clark Grubb ist gut: https://clarkgrubb.com/makefile-style-guide
Ich frage mich, ob jemand diesen Styleguide verwendet. Markiert ihr phony-Rezepte lieber direkt an der Deklarationsstelle oder als riesige Liste oben in der Datei? Ein Linter, der das erzwingt, wäre schön
-o pipefailreflexhaft zu verwenden, finde ich nicht gut. Pipefail ist nützlich, aber es zerschießt das Filtern von Ausgaben mitgrep, also genau das, was Leute in Pipelines am häufigsten tun. Es ist besser, es nur gezielt in den Rezepten zu setzen, die es brauchenNicht-Datei-Targets als
.PHONYzu markieren ist streng genommen korrekt, aber meistens nicht nötig. Wenn es viele Targets gibt, macht es das Makefile unnötig geschwätzig, daher würde ich es nur bei Bedarf hinzufügenBei Rezepten, die mehrere Output-Dateien erzeugen, waren früher Dummy-Dateien bzw. Flag-Dateien Standard, wenn Pattern Rules nicht passten, aber seit GNU Make 4.3 werden gruppierte Targets nativ unterstützt. Das ist auch in Ubuntu 22.04 LTS enthalten: https://www.gnu.org/software/make/manual/html_node/Multiple-...
Interessant ist in letzter Zeit, dass CMake offenbar entschieden hat, dass Makefiles für Projekte mit C++20-Modulen ungeeignet sind und stattdessen Ninja passend ist. https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
Offenbar geht man grundsätzlich davon aus, dass sich Zielabhängigkeiten statisch nur sehr schwer oder gar nicht definieren lassen. Inzwischen wird das dynamisch mit Tools wie
clang-scan-depsbehandelt: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...Tatsächlich verlangt Ninja, dass alle Abhängigkeiten statisch definiert werden, und hat in diesem Punkt also sogar weniger Funktionen als gewöhnliches Make
Make hat seinen Platz als Build-Tool für große C-Codebasen. Es wird aber oft wie ein allgemeiner „projektbezogener Task-Runner“ behandelt, und dafür eignet es sich nicht besonders gut. Schon einfache Bedingungen sind schwierig
Ich habe zum Beispiel mehrfach gut gemeinte Versuche gesehen, Terraform mit Make zu umhüllen, aber das endete nie gut
just ist ein hervorragendes modernes Werkzeug, um die Teile zu ersetzen, in denen Makefiles unordentlich werden: https://github.com/casey/just
Als völlig anderes Konzept gibt es auch Makedown, das vor 8 Monaten auf HN behandelt wurde: https://news.ycombinator.com/item?id=41825344
Makefiles sind großartig, aber man sollte sich nicht zu sehr darin verlieren. Vor ein paar Jahren wollte ich ein reines GNU-Make-Framework bauen und merkte dann, dass ich im Grunde autoconf neu erfand; erst da verstand ich, warum GNU autotools überhaupt geschaffen wurden
Makefiles sind auf seltsame Weise eine Lisp-artige Turing-Tarpit. GNU Make hat sogar Metaprogrammierungsfunktionen, sodass es schwerfällt, dem Drang zu widerstehen, innerhalb eines Makefiles irgendein gottloses System meta zu programmieren. Dass GNU Make so weit verbreitet installiert ist, verstärkt diese Versuchung noch
Ich bin eine der Personen, die Task als Alternative zu Make erstellt haben und pflegen. Es existiert seit über 8 Jahren und entwickelt sich weiter, also probiert es gern aus, wenn ihr nach etwas Neuem sucht. Fragen sind ebenfalls willkommen
https://taskfile.dev/
https://github.com/go-task/task
Ich frage mich, ob jemand tup ausprobiert hat: https://gittup.org/tup/ex_dependencies.html
Das ist ein Build-System, das Abhängigkeiten anhand von Dateisystemzugriffen automatisch erkennt, sodass es mit jedem Compiler oder Tool verwendet werden kann