6 Punkte von GN⁺ 2025-06-21 | 1 Kommentare | Auf WhatsApp teilen
  • 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 make im Terminal jedes Beispiel als Datei Makefile speichern und den Befehl make ausfü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 hello hat keine Abhängigkeiten und führt zwei Befehle aus
  • Bei make hello werden die Befehle ausgeführt, wenn die Datei hello nicht 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

  1. Datei blah.c anlegen (mit dem Inhalt int main() { return 0; })
  2. Danach folgendes Makefile schreiben
blah:  
	cc blah.c -o blah  
  • Bei make wird kompiliert, wenn das Target blah noch nicht existiert, und die Datei blah wird erzeugt
  • Auch nach Änderungen an blah.c erfolgt 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.c jetzt neu geändert wurde, wird das Target blah erneut 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_file nicht als echte Datei erzeugt wird, wird der Befehl für some_file bei jedem Lauf ausgeführt

Make clean

  • Das Target clean wird 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 clean trägt, kann das zu Verwirrung führen; daher wird die Verwendung von .PHONY empfohlen

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 wildcard einzubetten
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, LDFLAGS usw.
  • 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 filter lassen 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

 
GN⁺ 2025-06-21
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
    • Auf stark ausgelasteten Systemen oder in Multi-User-Umgebungen kann man statt -j auch --load-average nutzen, um die Systemlast bei paralleler Ausführung zu steuern (make -j10 --load-average=10)
    • Die Option --shuffle, die den Build-Target-Plan zufällig mischt, ist in CI-Umgebungen nützlich, um Abhängigkeitsprobleme im Makefile aufzudecken
    • Es 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 wird

    • Da make -j auf DOS-Maschinen häufig Probleme verursacht habe, wird dieses Verhalten als Bug wahrgenommen

    • Es 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, .PHONY in einem Tutorial nur deshalb auszulassen, weil es nicht verwendet wird. Die Meinung ist, man sollte lehren, wie man das Tool richtig benutzt

    • Im Team gab es Diskussionen, weil Make als Task-Runner verwendet wurde und man deshalb .PHONY für alle Rezepte ergänzt und gepflegt hat
    • Empfohlen wird Clark Grubbs Makefile Style Guide (clarkgrubb.com/makefile-style-guide)
    • Es werden verschiedene Stilvarianten geteilt, etwa .PHONY pro Rezept zu deklarieren oder gesammelt einmal am Anfang der Datei, verbunden mit dem Wunsch, das per Linter erzwingen zu können
    • Nach dem Lesen wird das Dokument als gut bewertet, aber es gibt einige Punkte, denen nicht zugestimmt wird
      • -o pipefail blind anzuwenden sei problematisch; bei Pipes mit grep usw. könne das kaputtgehen, daher werde eine situationsabhängige Nutzung empfohlen
      • Nicht-Datei-Targets mit .PHONY zu markieren sei zwar streng genommen korrekt, aber meist unnötig und mache Makefiles nur wortreicher; besser nur bei Bedarf einsetzen
      • Rezepte, die mehrere Output-Dateien erzeugen, verwendeten früher Dummy-Dateien, aber seit GNU Make 4.3 gibt es offizielle Unterstützung für gruppierte Targets (hier nachzulesen)
  • Es wird behauptet, Make sei ein Tool, das auf das Bauen großer C-Codebasen spezialisiert ist

    • Jemand nutzt es zwar gern als projektbezogenen Job-Runner, hält Make dafür aber nicht für geeignet, da schon Dinge wie Bedingungen umständlich werden
    • Es wird auch von gescheiterten Versuchen berichtet, Tools wie Terraform damit zu wrappen
    • 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)

    • In der Praxis sei es fast unmöglich, Target-Abhängigkeiten statisch zu definieren, weshalb dynamische Analyse mit Tools wie clang-scan-deps verwendet 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)

    • tup sei ein Build-System, das Abhängigkeiten automatisch über Dateisystemzugriffe erkennt und dadurch mit praktisch jedem Compiler oder Tool funktionieren könne
  • 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

    • Beim Parsen von Optionen aus MAKEFLAGS müsse man, wenn lange Optionen oder leere kurze Optionen behandelt werden sollen, so vorgehen
      ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))
    • Wenn Kompatibilität mit dem standardmäßig auf OS X mitgelieferten alten make nötig sei, fehlten viele Funktionen oder verhielten sich subtil anders
    • Andere Probleme seien meist Tippfehler oder Verstöße gegen Best Practices und würden daher ausgelassen
    • Zusätzlich der Hinweis, dass load portabler als guile sei und in Cross-Compile-Umgebungen der Compiler korrekt angegeben werden müsse
    • Empfohlen wird, Paul’s Rules of Makefiles (hier) sowie das GNU make Manual (hier) und verwandte Dokumentationen unbedingt zu lesen
    • Es wird auch ein einfaches Demo-Projekt für Makefiles betrieben (Demo auf GitHub)
  • Jemand hat die Gewohnheit, in jedes GitHub-Repository immer ein Makefile aufzunehmen

    • Da man Befehle leicht vergisst, lassen sie sich im Makefile speichern; außerdem können problemlos komplexe Schritte ergänzt werden, und mit einem einfachen make lässt sich sofort das projektspezifisch erwartete Verhalten ausführen, ohne sich alles merken zu müssen