6 Punkte von GN⁺ 2025-06-21 | 1 Kommentare | Auf WhatsApp teilen
  • 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: prerequisites und per Tab eingerückten Befehlen; Einrückung mit Leerzeichen kann dazu führen, dass make fehlschlägt
  • Variablen, Wildcards, automatische Variablen, Pattern-Regeln, Bedingungen, Funktionen, rekursives Make, include, .PHONY und .DELETE_ON_ERROR können den Build-Ablauf erweitern
  • Die Cookbook-Beispiele erzeugen aus C/C++-Quellen in src/ Build-Artefakte in build/ und richten mit -MMD -MP sowie -include eine 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 make eine Datei namens Makefile erstellt und dann in diesem Verzeichnis make ausfü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 make fehl

Zeitvergleich von Targets und Abhängigkeiten

  • make hello führt die mit dem Target hello verknüpften Befehle aus, aber nicht, wenn bereits eine Datei hello existiert
  • 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.c wird der Kompilierbefehl nur ausgeführt, wenn blah fehlt oder blah.c neuer ist als blah
  • 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 make kein Target angegeben wird, wird das erste Target im Makefile als Standard ausgeführt
  • Das Target all wird verwendet, um mehrere Targets auf einmal zu erstellen; steht es als erste Regel, baut schon ein einfaches make alle Targets
  • clean wird üblicherweise als Target zum Löschen von Build-Artefakten verwendet, ist aber kein spezielles reserviertes Wort in Make
  • Wenn clean weder das erste Target noch eine Abhängigkeit eines anderen Targets ist, muss es explizit wie make clean aufgerufen werden, damit es ausgeführt wird
  • Wenn tatsächlich eine Datei namens clean existiert, wird das Target clean möglicherweise nicht ausgeführt; das wird später mit .PHONY gelö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 der wildcard-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.o wird aus n.c in der Form $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@ erzeugt
    • n.o wird aus n.cc oder n.cpp in der Form $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@ erzeugt
    • n wird aus n.o per Linker-Befehl erzeugt
  • Wichtige Variablen in impliziten Regeln sind CC, CXX, CFLAGS, CXXFLAGS, CPPFLAGS und LDFLAGS
  • 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: %.c ein % 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 -s verhä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/sh und kann beispielsweise mit SHELL=/bin/bash geä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 Befehls
    • make -i: wendet Fehlerignorieren auf alle Befehle an
  • Wenn Make mit ctrl+c unterbrochen wird, löscht Make das gerade erstellte aktuelle Target

Rekursives Make und Umgebungsvariablen

  • Wenn innerhalb eines Makefiles erneut make aufgerufen wird, sollte statt make die 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 export setzt 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_VARIABLES können alle Variablen exportiert werden
  • Wenn mehrere Targets gleichzeitig angegeben werden, etwa make clean run test, werden sie in der Reihenfolge clean, run, test ausgeführt

Bedingungen und Funktionen

  • Bedingungen bestehen aus ifeq, ifneq, ifdef, ifndef, else und endif
  • ifdef expandiert keine Variablenreferenz, sondern prüft nur, ob eine Variable definiert ist
  • Mit $(MAKEFLAGS) und findstring kann geprüft werden, ob Flags wie make -i weitergegeben wurden
  • Make-Funktionen werden vor allem zur Textverarbeitung eingesetzt und in der Form $(fn, arguments) oder ${fn, arguments} aufgerufen
  • Wichtige Funktionen sind:
    • subst: String-Ersetzung
    • patsubst: Ersetzung von Wörtern nach Pattern
    • foreach: Transformation einer durch Leerzeichen getrennten Wortliste
    • if: nutzt das zweite Argument, wenn das erste nicht leer ist, sonst das dritte
    • call: 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 Leerzeichen
    • filter: wählt Elemente aus einer Liste aus, die einem bestimmten Pattern entsprechen
    • filter-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 einzulesen
  • include ist nützlich, wenn Makefiles gelesen werden sollen, die durch Optionen wie gcc’s -M erzeugt wurden
  • vpath <pattern> <directories> gibt Verzeichnisse an, in denen nach bestimmten Abhängigkeitsdateien gesucht wird
  • Mit der Variable VPATH kann ein ähnliches Suchpfadverhalten global festgelegt werden
  • Der Backslash \ wird verwendet, um lange Befehle auf mehrere Zeilen aufzuteilen
  • Wenn man ein Target zu .PHONY hinzufügt, verwechselt Make dieses Target nicht mit einer gleichnamigen Datei
  • .DELETE_ON_ERROR löscht das Target einer Regel, wenn während der Regelausführung ein Befehl einen Exit-Status ungleich 0 zurückgibt
  • .DELETE_ON_ERROR gilt nicht wie PHONY nur 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_program
    • BUILD_DIR := ./build
    • SRC_DIRS := ./src
  • SRCS findet mit find und $(shell ...) Dateien wie *.cpp, *.c und *.s
  • OBJS erzeugt Build-Artefaktpfade, indem jedem Quelldateinamen BUILD_DIR vorangestellt und .o angehängt wird
  • DEPS ersetzt .o durch .d und erzeugt so die Liste der Abhängigkeits-Makefiles
  • INC_DIRS findet alle Verzeichnisse unter src/, und INC_FLAGS stellt jedem Verzeichnis das Präfix -I voran
  • CPPFLAGS := $(INC_FLAGS) -MMD -MP konfiguriert GCC so, dass .d-Abhängigkeitsdateien erzeugt werden
  • C- und C++-Quellen erzeugen jeweils mit separaten Pattern-Regeln .o-Dateien unter build/; benötigte Verzeichnisse werden mit mkdir -p $(dir $@) erstellt
  • clean wird als .PHONY deklariert und löscht mit rm -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

 
GN⁺ 2025-06-21
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 verringert
    make --output-sync=recurse -j10
    Auf 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=10
    Auch --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

    • Wenn es „wenig bekannt“ ist, wäre es schön, wenn die Autoren von make die Optionen irgendwo gesammelt mit dem Programm ausliefern würden. In Form einer für Menschen lesbaren Textdatei oder einer Satzsprache wäre der Zugang zu diesem Wissen viel besser
    • Am häufigsten nutze ich -B, womit alles ohne Bedingungen komplett neu gebaut wird
    • Ich habe make -j schon mehrfach erlebt, wie es eine Maschine praktisch in einen DoS-Zustand versetzt, daher betrachte ich es fast als Bug
    • Wenn es ein stark ausgelastetes Mehrbenutzersystem ist, sollte dann nicht der Betriebssystem-Scheduler damit klarkommen?
    • Nützlich, aber nicht portabel. Außerhalb privater Spielzeugprojekte, die man nicht verteilt, sollte man es besser nicht verwenden
  • Ich 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

    • Das Gute an diesem Ansatz ist, dass der Benutzer im Wesentlichen selbst den Abhängigkeitsgraphen interpretiert und zum Executor wird, also mehr Kontrolle hat. Im Kochbeispiel macht die deklarative Struktur besser sichtbar, an welchen Stellen man Wahlmöglichkeiten für den nächsten Schritt hat, und lässt einem die Freiheit, auch externe Einschränkungen zu berücksichtigen, die nicht formell im Makefile stehen, etwa eine Reihenfolge, in der das Abwaschen einfacher ist
      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 .PHONY markiert, 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 lehren
    In meinem Team nutzen wir make als Task-Runner, und ich musste mir schon einiges anhören, weil ich darauf bestanden habe, .PHONY zu allen Rezepten hinzuzufügen und das beizubehalten
    Der 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

    • Ich habe ihn gelesen, und insgesamt ist es ein gutes Dokument. Mit ein paar Punkten stimme ich aber nicht überein
      -o pipefail reflexhaft zu verwenden, finde ich nicht gut. Pipefail ist nützlich, aber es zerschießt das Filtern von Ausgaben mit grep, also genau das, was Leute in Pipelines am häufigsten tun. Es ist besser, es nur gezielt in den Rezepten zu setzen, die es brauchen
      Nicht-Datei-Targets als .PHONY zu 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ügen
      Bei 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-deps behandelt: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...

    • Falls das falsch ist, korrigiert mich bitte, aber diese Beschränkung bei Makefiles scheint vollständig eine Entscheidung von CMake zu sein oder zumindest das Ergebnis davon, dass es an Freiwilligen fehlte, die Unterstützung für den Makefile-Generator hinzufügen. Ninja selbst unterstützt C++-Module ebenfalls nicht: https://github.com/ninja-build/ninja/issues/2457
      Tatsächlich verlangt Ninja, dass alle Abhängigkeiten statisch definiert werden, und hat in diesem Punkt also sogar weniger Funktionen als gewöhnliches Make
    • Ehrlich gesagt sind Module fast schon eine Katastrophe
  • 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

    • Make ist kein allgemeiner Task-Runner. Es ist eher eine allgemeine Methode, lineare Shell-Skripte in deklarative Abhängigkeiten zu verwandeln. Ein allgemeines Werkzeug für die Shell
    • Ich finde sogar, dass die Aussage, es sei ein Build-Tool für große C-Codebasen, inzwischen nicht mehr stimmt. In den letzten 20 Jahren wurden robustere und besser definierte Build-Systeme geschaffen, also ist es Zeit für ein Update
    • Gibt es einen guten allgemeinen Task-Runner? Bearbeitung: Offenbar habe ich den Begriff „Task-Runner“ völlig missverstanden
  • just ist ein hervorragendes modernes Werkzeug, um die Teile zu ersetzen, in denen Makefiles unordentlich werden: https://github.com/casey/just

    • Es ersetzt den Aspekt von Make als „Liste kurzer Shell-Skripte“, ersetzt aber nicht den tatsächlich nützlichen Teil, nämlich „nur die Regeln ausführen, die erneut ausgeführt werden müssen“
    • Es gibt auch andere Optionen: Task (Go) https://github.com/go-task/task, Cake (C#) https://github.com/cake-build/cake, Rake (Ruby) https://github.com/ruby/rake
      Als völlig anderes Konzept gibt es auch Makedown, das vor 8 Monaten auf HN behandelt wurde: https://news.ycombinator.com/item?id=41825344
    • Diese Tools positionieren sich zwar selbst als Alternativen zu make, aber ich persönlich halte sie für völlig anders und nicht wirklich vergleichbar. make ist auf die Erzeugung von Artefakten und darauf ausgerichtet, bereits Erstelltes nicht erneut zu bauen, während just ein Befehls-Runner ist
    • Der Hauptvorteil von Make als Befehls-Runner ist, dass es als Standardwerkzeug „überall“ installiert ist. Auch wenn die Alternativen angenehmer zu benutzen wirken, habe ich nie das Gefühl gehabt, dass sie einen so großen Vorteil bieten, dass sich die Installation eines zusätzlichen Tools rechtfertigt
    • Task ist ebenfalls eine weitere Alternative. Ich persönlich habe es allerdings nur für einfache C-Hobbyprojekte verwendet, daher kann ich schwer sagen, ob es auch bei größerem Umfang gut durchhält: https://taskfile.dev/
  • 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