Makefile lernen mit den besten Beispielen
(makefiletutorial.com)- Makefile ist ein Werkzeug zur Vereinfachung der Build-Automatisierung und des Abhängigkeitsmanagements für C/C++
- Es erkennt geänderte Dateien über Zeitstempel und führt Kompilierungsschritte nur dann aus, wenn sie nötig sind
- Kernstrukturen wie Regeln (rules), Befehle (commands) und Abhängigkeiten (prerequisites) werden anhand von Beispielen erklärt
- Auch fortgeschrittene Funktionen wie automatische Variablen, Pattern Rules und Variablenerweiterung werden praxisnah behandelt
- Mit einer Makefile-Vorlage für mittelgroße Projekte wird die Bedeutung von Skalierbarkeit und Wartbarkeit vorgestellt
Einführung in den Makefile-Tutorial-Guide
- Makefile ist ein zentrales Werkzeug für die Build-Automatisierung und das Abhängigkeitsmanagement in Projekten
- Wegen verschiedener versteckter Regeln und Symbole kann es beim ersten Kontakt komplex wirken, aber dieser Guide fasst die wichtigsten Inhalte mit knappen, direkt ausführbaren Beispielen zusammen
- Jeder Abschnitt lässt sich anhand praxisorientierter Beispiele nachvollziehen
Erste Schritte
Wozu es Makefiles gibt
- Makefiles werden in großen Programmen verwendet, um nur geänderte Teile neu zu kompilieren
- Neben C/C++ gibt es auch für viele andere Sprachen eigene Build-Tools, aber Make wird weiterhin in allgemeinen Build-Szenarien breit eingesetzt
- Der Kern ist die Logik, geänderte Dateien zu erkennen und nur die nötigen Schritte auszuführen
Alternative Build-Systeme zu Make
- Für C/C++: SCons, CMake, Bazel, Ninja und weitere Optionen
- Für Java: Ant, Maven, Gradle usw.
- Auch Go, Rust und TypeScript bringen eigene Build-Tools mit
- Interpretierte Sprachen wie Python, Ruby und JavaScript müssen nicht kompiliert werden und brauchen daher seltener eine separate Verwaltung wie mit einem Makefile
Versionen und Varianten von Make
- Es gibt verschiedene Make-Implementierungen, dieser Guide ist jedoch auf GNU Make optimiert, das vor allem unter Linux und MacOS verwendet wird
- Die Beispiele sind sowohl mit GNU Make 3 als auch 4 kompatibel
So führt man die Beispiele aus
- Nach der Installation von
makeim Terminal jedes Beispiel als DateiMakefilespeichern und den Befehlmakeausführen - Befehlszeilen in einem Makefile müssen zwingend mit einem Tab-Zeichen eingerückt werden
Grundsyntax eines Makefiles
Aufbau einer Regel (Rule)
-
Target: Abhängigkeit(en)- Befehl
- Befehl
-
Target: der Name der Build-Ausgabedatei, meist genau eine
-
Befehl: das tatsächlich ausgeführte Shell-Skript, beginnend mit einem Tab
-
Abhängigkeit: Liste von Dateien, die vor dem Build des Targets vorhanden sein müssen
Das Wesen von Make
Hello-World-Beispiel
hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."
- Das Target
hellohat keine Abhängigkeiten und führt zwei Befehle aus - Bei
make hellowerden die Befehle ausgeführt, wenn die Dateihellonicht existiert. Existiert sie bereits, wird nichts ausgeführt - Üblicherweise wird ein Target so geschrieben, dass es mit einem Dateinamen übereinstimmt
Grundlegendes Beispiel zum Kompilieren einer C-Datei
- Datei
blah.canlegen (mit dem Inhaltint main() { return 0; }) - Danach folgendes Makefile schreiben
blah:
cc blah.c -o blah
- Bei
makewird kompiliert, wenn das Targetblahnoch nicht existiert, und die Dateiblahwird erzeugt - Auch nach Änderungen an
blah.cerfolgt keine automatische Neukompilierung → es muss eine Abhängigkeit ergänzt werden
So fügt man Abhängigkeiten hinzu
blah: blah.c
cc blah.c -o blah
- Wenn
blah.cjetzt neu geändert wurde, wird das Targetblaherneut gebaut - Zum Erkennen von Änderungen werden Zeitstempel von Dateien verwendet
- Werden Zeitstempel manuell verändert, kann das zu unerwartetem Verhalten führen
Weitere Beispiele
Beispiel für verknüpfte Targets und Abhängigkeiten
blah: blah.o
cc blah.o -o blah
blah.o: blah.c
cc -c blah.c -o blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
- Make folgt den Abhängigkeiten in einer Baumstruktur und automatisiert die Erzeugung in jedem Schritt
Beispiel für ein immer ausgeführtes Target
some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
- Da
other_filenicht als echte Datei erzeugt wird, wird der Befehl fürsome_filebei jedem Lauf ausgeführt
Make clean
- Das Target
cleanwird häufig verwendet, um Build-Artefakte zu löschen - Es ist in Make kein spezielles reserviertes Wort und muss als eigener Befehl definiert werden
- Wenn eine Datei den Namen
cleanträgt, kann das zu Verwirrung führen; daher wird die Verwendung von.PHONYempfohlen
Beispiel:
some_file:
touch some_file
clean:
rm -f some_file
Arbeit mit Variablen
- Variablen sind immer Strings.
- Üblicherweise wird
:=empfohlen; daneben gibt es verschiedene Zuweisungsarten wie=,?=und+= - Verwendungsbeispiel:
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file1 file2 some_file
- Variablenreferenzen werden als
$(variable)oder${variable}geschrieben - Anführungszeichen im Makefile haben für Make selbst keine Bedeutung, wohl aber in Shell-Befehlen
Verwaltung von Targets
Das all-Target
- Um mehrere Targets auf einmal auszuführen, versieht man das erste Target, also das Standard-Target, entsprechend
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
Mehrere Targets und automatische Variablen
- Für mehrere Targets lassen sich jeweils eigene Befehle ausführen.
$@steht für den aktuellen Target-Namen
all: f1.o f2.o
f1.o f2.o:
echo $@
Automatische Variablen und Wildcards
Wildcard *
*durchsucht Dateinamen direkt im Dateisystem- Es wird empfohlen, es immer in die Funktion
wildcardeinzubetten
print: $(wildcard *.c)
ls -la $?
*sollte in Variablendefinitionen nicht direkt verwendet werden
thing_wrong := *.o
thing_right := $(wildcard *.o)
Wildcard %
- Wird meist in Pattern Rules verwendet und kann das angegebene Muster extrahieren und erweitern
Fancy Rules
Implizite Regeln (Implicit Rules)
- Make enthält mehrere versteckte Standardregeln für C/C++-Builds
- Typische Variablen sind
CC,CXX,CFLAGS,CPPFLAGS,LDFLAGSusw. - C-Beispiel:
CC = gcc
CFLAGS = -g
blah: blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
clean:
rm -f blah*
Static Pattern Rules
- Mehrere Regeln mit demselben Muster lassen sich kompakt formulieren
objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all
$(objects): %.o: %.c
$(CC) -c $^ -o $@
all.c:
echo "int main() { return 0; }" > all.c
%.c:
touch $@
clean:
rm -f *.c *.o all
Static Pattern Rules + Funktion filter
- Mit
filterlassen sich nur Ziele auswählen, die zu einem bestimmten Erweiterungsmuster passen
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
.PHONY: all
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $
1 Kommentare
Hacker-News-Kommentare
Jemand berichtet, dass er 1985 im Graphics-Labor der Boston University persönlich gesehen hat, wie jemand mit einem Makefile einen 3D-Renderer für Animationen baute. Die Person war Lisp-Programmierer und arbeitete an früher prozeduraler Generierung sowie einem 3D-Actor-System; dabei entstand ein wirklich elegantes Makefile mit etwa 10 Zeilen. Die Struktur erzeugte anhand einfacher Dateizeitstempel-Abhängigkeiten automatisch Hunderte von Animationen. Die 3D-Form jedes Frames wurde in Lisp erzeugt, und Make generierte die Frames. 1985 war 3D und Animation noch keineswegs selbstverständlich wie heute, daher waren alle erstaunt. In Erinnerung geblieben ist auch, dass es sich um Brian Gardner handelte, der später an den 3D-Renderern für Iron Giant und Coraline arbeitete
Es wird gefragt, ob es sich vielleicht um die Person auf 3d-consultant.com/bio.html handelt
Es wird nachgefragt, ob tatsächlich der Film Coraline gemeint war
Es werden einige wenig bekannte, nützliche Flags für Make vorgestellt
--output-sync=recurse -j10: Dieses Flag sorgt dafür, dass stdout/stderr bis zum Abschluss jedes Target-Jobs gesammelt und dann ausgegeben wird; andernfalls vermischen sich die Logs und sind schwer auszuwerten-jauch--load-averagenutzen, um die Systemlast bei paralleler Ausführung zu steuern (make -j10 --load-average=10)--shuffle, die den Build-Target-Plan zufällig mischt, ist in CI-Umgebungen nützlich, um Abhängigkeitsprobleme im Makefile aufzudeckenEs wird die Idee erwähnt, die verschiedenen Optionen von make offiziell zu bündeln und als Text oder Dokumentation im Programm mitzuliefern, um die Zugänglichkeit zu erhöhen
Ein häufig genutztes Flag sei
-B, das für einen vollständigen erzwungenen Rebuild verwendet wirdDa
make -jauf DOS-Maschinen häufig Probleme verursacht habe, wird dieses Verhalten als Bug wahrgenommenEs wird gefragt, ob Parallelisierungsprobleme auf ausgelasteten Systemen oder in Multi-User-Umgebungen nicht eigentlich vom OS-Scheduler gelöst werden sollten
Es seien nützliche Flags, aber da diese Optionen nicht portabel sind, werde empfohlen, sie außerhalb privater Projekte für den Eigengebrauch nicht zu verwenden
Es wird als schwache Ausrede betrachtet,
.PHONYin einem Tutorial nur deshalb auszulassen, weil es nicht verwendet wird. Die Meinung ist, man sollte lehren, wie man das Tool richtig benutzt.PHONYfür alle Rezepte ergänzt und gepflegt hat.PHONYpro Rezept zu deklarieren oder gesammelt einmal am Anfang der Datei, verbunden mit dem Wunsch, das per Linter erzwingen zu können-o pipefailblind anzuwenden sei problematisch; bei Pipes mitgrepusw. könne das kaputtgehen, daher werde eine situationsabhängige Nutzung empfohlen.PHONYzu markieren sei zwar streng genommen korrekt, aber meist unnötig und mache Makefiles nur wortreicher; besser nur bei Bedarf einsetzenEs wird behauptet, Make sei ein Tool, das auf das Bauen großer C-Codebasen spezialisiert ist
Eine andere Meinung lautet, Make sei weniger ein Job-Runner als vielmehr ein universelles Shell-Tool, das lineare Shell-Skripte in deklarative Abhängigkeitsformen überführt
Die Sicht auf Make als reines Build-Tool für C-Codebasen wird als überholt angesehen. Es wird darauf verwiesen, dass in den letzten 20 Jahren robustere und klarere Build-Systeme entwickelt wurden, und eine Aktualisierung dieser Sichtweise angeregt
Es wird nach einem guten Job-Runner gefragt. (Später folgt eine Entschuldigung dafür, dass der Begriff missverstanden wurde.)
Als modernes Tool, das die Komplexität von Makefiles in manchen Bereichen ersetzt, wird just empfohlen
just eigne sich gut als Ersatz für eine Liste von Shell-Skripten, könne aber die eigentliche Kernfunktion von Make — nur Regeln auszuführen, die erneut ausgeführt werden müssen — nicht ersetzen
Weitere Alternativen seien
Alternative Tools bezeichneten sich selbst als Make-Ersatz, aber nach Ansicht des Kommentators seien sie völlig anders und kaum direkt vergleichbar. Der Kern von Make liege in der Erzeugung von Artefakten und darin, bereits Gebautes nicht erneut zu bauen. just dagegen sei eher ein einfacher Kommando-Ausführer
Ein Vorteil von Make als Kommando-Ausführer sei seine Verlässlichkeit als Standardtool, das fast überall installiert ist. Auch wenn Alternativen besser gebaut sein mögen, lohne sich ihr Einsatz wegen des zusätzlichen Installationsaufwands oft nicht
Task werde für kleine Hobbyprojekte in C gut genutzt, aber ob es auch für große Projekte taugt, lasse sich noch schwer beurteilen (offizielle Task-Website)
Interessant sei, dass CMake in jüngerer Zeit ninja als Standard gewählt habe, weil Makefiles für die Unterstützung von C++20-Modulen ungeeignet seien (CMake-Leitfaden)
clang-scan-depsverwendet werde (technische Folien)Dem wird entgegengehalten, dass diese Einschränkung eher eine Entscheidung von CMake oder ein Mangel an Mitwirkenden für den Makefile-Generator sei. Auch ninja unterstütze C++-Module nicht direkt (zugehöriges Issue); zudem habe ninja sogar weniger Funktionen als Make und verlange, dass alle Abhängigkeiten statisch angegeben werden
Es wird die Meinung geäußert, dass schon die Einführung von Modulen selbst komplex und verwirrend sei
Es wird gefragt, ob jemand Erfahrungen mit tup hat. (offizielle Dokumentation)
Jemand stellt sich als Gründer und Maintainer von Task vor, einem alternativen Tool zu Make. Es werde seit über 8 Jahren entwickelt und stetig verbessert
just wird ebenfalls als weitere Make-Alternative empfohlen (just auf GitHub)
Als interessante Koinzidenz wird erwähnt, dass der Kommentator Task häufig nutzt und heute Morgen sogar ein Issue eingereicht hat
In diesem Tutorial gebe es gefährliche und subtile Probleme
ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))Jemand hat die Gewohnheit, in jedes GitHub-Repository immer ein Makefile aufzunehmen
makelässt sich sofort das projektspezifisch erwartete Verhalten ausführen, ohne sich alles merken zu müssen