Ich mag Makefiles
(switowski.com)- Auch wenn ständig neue Build-Tools erscheinen, bleibt das Makefile als leichtgewichtiges Standardwerkzeug nützlich, um wiederkehrende Aufgaben in Projekten zu bündeln
- Dank konventioneller Befehle wie
make,make build,make installundmake devkann man auch bei unbekannten Projekten schnell Build, Installation und Start des Entwicklungsservers ausprobieren - Unabhängig vom Stack – etwa Jekyll, Hugo, 11ty, Python-Web-Frameworks, Docker oder gulp – dient das Makefile als gemeinsame Ausführungsschnittstelle
- In persönlichen Projekten reichen oft ein paar einfache Targets wie
dev,buildunddeploystatt Bedingungen oder Flags - GNU Make ist weit verbreitet, und selbst wenn es fehlt, lassen sich die Shell-Befehle aus dem Makefile direkt ausführen, wodurch es auch in eingeschränkten Umgebungen wenig Hürden gibt
Unterschiedliche Projektbefehle in einem Einstiegspunkt bündeln
- Ein Makefile wird genutzt, um Projekte mit unterschiedlichen Build-, Ausführungs- und Deployment-Methoden in eine vertraute Befehlsmenge zu verpacken
- Wenn ein unbekanntes Projekt ein
Makefilehat, kann man meist zuerst diese Befehle ausprobierenmakemake buildmake install
- Wenn man ein altes persönliches Projekt wieder öffnet, kann man es so einrichten, dass schon
make devalle nötigen Build-Schritte und den Start des Entwicklungsservers auf einmal erledigt - Auch bei unterschiedlichen Technologie-Stacks wie Jekyll, Hugo, 11ty oder verschiedenen Python-Web-Frameworks kann derselbe Einstiegspunkt erhalten bleiben
- Selbst wenn Docker oder gulp verwendet werden, ersetzt das Makefile diese Tools nicht, sondern fungiert als Orchestrierungsschicht
- Beispiel:
make buildbaut die nötigen Docker-Images und übergibt dabei projektspezifische Zusatzargumente
- Beispiel:
Einfache Nutzung in persönlichen Projekten
- Beim Einsatz von Makefiles liegt der Fokus eher auf Bündeln von Shell-Befehlen als auf komplexen Funktionen
- Funktionen wie Bedingungen oder Flags werden nicht verwendet
- Die Arbeitseinheit von make ist technisch gesehen ein Target, wird praktisch aber wie ein Task behandelt
- Man könnte das auch als Bash-Skriptfunktion bauen, aber ein Makefile wird als einfacher und schneller angesehen
- In persönlichen Projekten gibt es meist nur 2 bis 3 häufig genutzte Aufgaben
dev: Entwicklungsserver startenbuild: Projekt bei Bedarf bauendeploy: Projekt deployen oder veröffentlichenwatch: zusätzliche Aufgabe, um Build-Schritte bei Änderungen an Quelldateien automatisch erneut auszuführen
- Das aktuelle Blog-Makefile hat nur ein einziges Target:
dev- Unter
dev:wirdnpm run devausgeführt
- Unter
- In komplexeren persönlichen Projekten werden folgende Aufgaben in einem Makefile gebündelt
dev: Entwicklungsserver mitbundle exec jekyll servestartenbuild: Assets mitnpm run gulp buildbauenwatch: bestimmtes Verzeichnis mitnpm run gulp watchüberwachendeploy: Jekyll für die Produktionsumgebung bauen und danachmake encrypt,netlify deploy --prodausführenencrypt: HTML-Dateien im Ordner_sitemitnpx staticryptverschlüsseln
Wichtige Einschränkungen und Vorteile
- Im Beispiel wurden phony targets ausgelassen, aber wenn es Dateien mit Namen wie
dev,build,watch,deployoderencryptgibt, funktioniert das Makefile möglicherweise nicht wie erwartet- Leser auf Hacker News empfehlen in solchen Fällen, phony targets hinzuzufügen
- GNU Make ist die Software, die makefiles ausführt; unter Linux ist sie vermutlich bereits installiert, und auch auf dem MacBook ist sie so verbreitet, dass man sich oft nicht einmal an eine separate Installation erinnert
- Make verlangt nicht viele zusätzliche Abhängigkeiten und kann deshalb in eingeschränkten Umgebungen nützlich sein, in denen die Installation von Paketen aus Sicherheitsgründen schwierig oder unmöglich ist
- Selbst wenn Make nicht vorhanden ist, lassen sich die Befehle aus dem Makefile direkt in die Shell kopieren und ausführen
- Im Gegensatz dazu ist es schwierig, JavaScript-Code einfach direkt ins Terminal einzufügen und auszuführen, wenn gulp auf dem Server nicht vorhanden ist
- Selbst wenn man ein besseres und schnelleres neues Build-Tool findet, kann Make weiterhin als gemeinsame Befehlsschicht genutzt werden, um unterschiedliche Tools und Konfigurationen zu verwalten
2 Kommentare
Wenn keine Abhängigkeiten definiert sind, bietet ein Ersatz des
makefiledurchjustfileeine bessere Benutzerfreundlichkeit.Hacker-News-Meinungen
Man muss sich in diesem Thread nicht davon entmutigen lassen, dass einem gesagt wird, man „benutze make falsch“. Einer der Gründe, warum Make ein gutes Tool ist: Man kann damit einfacher anfangen, als es auf den ersten Blick wirkt.
Wenn man
.PHONYnicht verwendet, kann das Probleme verursachen, aber bei einem kleinen Projekt ist das vielleicht eine Falle, in die man erst ein Jahr später tappt; und selbst dann ist es wahrscheinlich mit etwa einer Stunde Kopfkratzen erledigt. In 99 % der Fälle kann man einfach loslegen, ohne sich um die „Lehrbuchlösung“ zu kümmern, und Make verlangt nur so viel Komplexität, dass es nicht auseinanderfällt.Make nimmt Eingabedateien und Abhängigkeiten und erzeugt genau eine Ausgabedatei. Regeln, die keine Ausgabe erzeugen, wie
install,alloderclean, sowie alle Targets des Artikels müssen von einem speziellen magischen Target wie.PHONYabhängen;.PHONYgab es in POSIX von 2017 (IEEE Std 1003.1-2017, https://pubs.opengroup.org/onlinepubs/9699919799/utilities/m...) noch nicht und ist erst im aktuellen Standard (IEEE Std 1003.1-2024, https://pubs.opengroup.org/onlinepubs/9799919799/utilities/m...) enthalten. Wenn man mehrere Dateien erzeugen will, etwa Objektdateien und Module oder vorkompilierte Header, muss man sich selbst fragile Workarounds bauen. Nicht jedes Make ist GNU Make; auch BSD, Solaris/Illumos und andere nix-artige Systeme existieren noch.Das heißt nicht, dass Make nutzlos wäre. Es ist nützlich für Projekte, die komplex genug sind, aber noch nicht so komplex, dass sie ein „besseres“ Build-System brauchen. Das Problem ist, dass solche Projekte mit wachsendem Code komplexer werden und am Ende Skripte/Programme haben, die ein Makefile oder Teile davon erzeugen – womit ein provisorisches Meta-Build-System entsteht.
Das größere Problem ist nicht, dass man es verwendet, sondern dass es „allen“ als Lösung vorgeschlagen wird. Und sobald im Projekt-Root ein Verzeichnis oder eine Datei namens
buildoderdevauftaucht, funktioniert dieses Makefile nicht mehr.Heutzutage würde ich zwar auf pnpm umstellen, aber besonders komplex finde ich es trotzdem nicht. Man muss die Syntax zwar ein Stück weit kennen, aber es ist weniger schwierig, als andere Tools von Anfang an korrekt zum Laufen zu bringen.
.PHONY-Tasks, um die sich die Leute so viele Gedanken machen.Es ist einfach, gut lesbar, änderbar, kombinierbar und praktisch überall bereits installiert. Es tut, was auf der Oberfläche steht, und kaum mehr.
Nebenbei: Wenn ich in anderen Projekten mit modischen oder albtraumhaften Build-Systemen umgehen muss, wickle ich sie ebenfalls mit make ein.
Jeder muss irgendwo anfangen, und sobald etwas erst einmal läuft, lassen sich schrittweise Verbesserungen leicht ergänzen.
$@. Das gesamte Makefile kann beliebig viele Ausgaben erzeugen, weil Regeln von anderen Regeln abhängen können.Daraus ergibt sich eine saubere Faustregel für phony Targets: Wenn ein Rezept nicht nur
$@anfasst, sollte$@als phony markiert werden. Es ist viel einfacher, phony Targets als Liste zu verwalten.phonies :=phonies += somethingsomething:./do-somethingphonies += something-elsesomething-else: something./do-something-else# touches $@ and thus does not need to be phonycreate-file:./generate-some-output > $@.PHONY: $(phonies)Makefiles sind eine miserable Technik. Nur sind sie etwas weniger schlecht als die meisten anderen Build-Systeme, die wir gebaut haben, und werden dadurch in selbstquälerischem Sinne nützlich.
Build-Systeme sind meist entweder so grundlegend, dass sie beim Bauen von mehr als Spielzeugprojekten im Chaos enden, oder so komplex, dass Vorwissen, Bürokratie, Synchronisierung und Boilerplate überhandnehmen, oder sie haben keine Standardbibliothek, sodass sich dieselben Build-Muster auf 10.000 inkompatible Implementierungen verteilen, oder sie sind so restriktiv, dass man bei wachsenden Anforderungen migrieren muss, oder sie enthalten zu viel Magie, oder die Syntax ist schwer verständlich und inkonsistent.
Mehr als die Hälfte der Software, die ich nutze, verwendet nur ein schlichtes Makefile und gelegentlich ein configure-Skript. Ohne autotools optional
./configureausführen und dannmake,make install– und es funktioniert einfach. Das ist keineswegs eine Spielzeug-Konfiguration, sondern besteht aus kleinen Programmen, die jeweils eine Sache gut machen.Make und seine Funktionsweise sind zumindest für mich leicht zu verstehen. Ich glaube, die schlechten Erfahrungen, die Leute gemacht haben, liegen oft eher an schlechten Build-Konfigurationen komplexer Projekte oder an fehlendem nötigem Wissen als an Make selbst.
Dass es keine Standardbibliothek gibt, ist als Werkzeugunabhängigkeit gedacht, und genau deshalb ist es universell auf verschiedene Werkzeuge, Sprachen und Einsatzzwecke anwendbar. Ich sehe das nicht als Fehler, sondern als Feature. Die einfache Ebene ist Dependency-Tracking, und Targets und Abhängigkeiten sind ebenfalls klar, daher wirkt es kaum magisch. Die Syntax könnte allerdings definitiv besser sein.
Die einzigen allgemeinen Build-Systeme, die mehrere Sprachen abdecken, sind
makeoder Systeme wie CMake, die aufmakeabzielen. Der Grund, warum das nicht ideal ist, liegt darin, dassmakenicht ideal ist.makerichtig zu verwenden ist wirklich schwierig; es gibt Probleme wie rekursives Make und nicht-rekursives Make, viele inkompatible Varianten wie Unix/POSIX make, BSD make, GNU make und Windowsnmake, und es ist ziemlich hässlich.Trotzdem ist korrekt eingesetztes
makeziemlich gut. Es ist wirklich ein Glück, dass esmakeals kleinsten gemeinsamen Nenner gibt.Dass Make nicht einmal Unterverzeichnisse sauber handhaben kann, ist ziemlich absurd. Ich frage mich, ob es etwas Besseres als Make gibt. Ninja existiert zwar, ist aber nicht dafür entworfen, direkt von Menschen geschrieben zu werden.
Wie hoch würdest du den Anteil einschätzen? 20 %, 40 %, vielleicht 70 % trifft es doch ungefähr?
Ich mag Make wirklich sehr. In den letzten 20 Jahren habe ich für jede Aufgabe oder jedes Projekt, an dem ich gearbeitet habe, mindestens eines geschrieben.
Da ist nichts Cleveres dran, nur eine Sammlung von Schnipseln mit ein paar Variablen. Etwa
make run,make test,make lint.make recentführt nach dem Linting das zuletzt geänderte Skript aus. Das ginge auch mit Bash oder einer anderen Shell, aber dann wird man in die Entwicklerwelt hineingezogen, die Komplexität steigt, und es entsteht kein zusätzlicher Wert. Make ist lediglich eine domänenspezifische Sprache für „diese Datei wird aus diesen Dateien erzeugt, und dabei werden diese ein oder zwei Befehle ausgeführt“. Genau deshalb ist es enorm mächtig.Vor Jahrzehnten habe ich in Java ein Test-Framework gebaut, in dem man Tests und abhängige Klassen mit einer make-ähnlichen Syntax angeben konnte. Es gab eine Menge von Testklassen, die ein „Basis-Testbündel“ definierten, und andere Ebenen von Testklassen, die davon abhingen und nur ausgeführt wurden, wenn die vorherige Stufe erfolgreich war.
Ich weiß nicht, warum heutzutage alles so kompliziert gemacht wird. Ich habe immer geraten, bei den Standard-Unix-Werkzeugen und ihrer Arbeitsweise zu bleiben, also bei dem, was sich über die Zeit bewährt hat – sofern es nicht eine Situation ist, in der das absolut unmöglich ist. Zeit ist endlich, daher möchte ich sie für das Wesentliche der Problemlösung nutzen: System-/Programmdesign, Modellierung, Struktur, Patterns usw.; der Rest ist zweitrangig.
Es ist eine große Erleichterung zu wissen, dass
make testfunktioniert – unabhängig von Stack, Sprache oder Repository.Im Makefile meines letzten Projekts gab es vier Hauptbefehle:
build,test,frontend,deploy. build und test riefen maven auf, frontend rief npm auf, deploy rief docker und aws auf.Diese Werkzeuge haben alle eigenes State-Tracking, Caching und inkrementelle Verarbeitung, berichten aber nicht, was sie getan haben. Deshalb kann man keine atomaren Regeln wie „nur deployen, wenn der Build aktualisiert wurde“ schreiben. Denn maven/cargo/dotnet/npm/go legen diese Information nicht offen.
Der Autor verwendet nicht einmal mtime-basierte Abhängigkeitsverfolgung. Die Targets müssten außerdem
.PHONYsein, sind aber nicht entsprechend markiert. Man hätte das auch durch ein Shell-Skript ersetzen können, das$1liest, abgleicht und entscheidet, was zu tun ist.https://just.systems/
.PHONY: $(MAKECMDGOALS)./build.sh devist zwar weniger eindeutig alsmake dev. Ein weiterer Grund, Make zu verwenden, selbst wenn es gar keine nicht-phony Schritte gibt, ist aber, dass man solche Schritte später hinzufügen kann, falls man sie braucht. Natürlich stimme ich zu, dass der Autor{dev,build,deploy}als phony markieren sollte.Das eigene Makefile ließe sich technisch verbessern, aber wichtiger ist, dass die „Getting started“-Erfahrung beim Einstieg in ein Projekt konsistent ist.
Die Leute wollen, dass es viele andere Dinge tut – Skriptsprache, Dependency-Tracker usw. – und ziehen dafür aufgeblähte Tools heran. Neu heißt nicht automatisch besser. Autoconf und automake sind nicht make.
Technisch gesehen suchen all diese make-Targets nach Dateien mit demselben Namen wie das Target. Jedes Target sollte tatsächlich als
.PHONYdefiniert werden.Früher habe ich solche Makefiles ständig verwendet, aber in den letzten Jahren bin ich zu just und justfiles gewechselt. Dieses Verhalten ist dort der Standard, und insgesamt ist es einfacher zu benutzen. Dinge wie Parameter sind ebenfalls einfacher.
https://github.com/casey/just
:beginnen sollen, sodass man sie schon am Namen unterscheiden kann.Dafür ist es jetzt natürlich zu spät.
Es scheint an der Zeit für eine neue Art von coreutils-Distribution zu sein, die neue und nützliche Tools wie just und ripgrep bündelt und überall dort installiert wird, wo man sie verwenden möchte.
Es ist interessant, dass make so heftige Debatten auslöst. Das erinnert an die früheren, halb religiösen vi-gegen-emacs-Debatten.
Ich stimme dem ursprünglichen Beitrag voll zu und halte insbesondere den Ansatz für klug, auch bei ausgefeilteren Build-Tools einen Top-Level-Makefile-Wrapper zu verwenden. Der Vorteil ist Standardisierung. Ich muss mir nichts merken: Wenn es ein Projekt von mir ist, tippe ich einfach
makeein und weiß, dass es funktioniert.Wenn zum Beispiel jemand aus der C-Welt ein Rust-Projekt kompilieren will, muss er nicht nachschlagen, wie cargo benutzt wird, sondern kann einfach
makeodergmakeeingeben. Ich versuche, keine GNU-spezifischen Features zu verwenden und POSIX-konform zu bleiben, aber es stimmt auch, dass fast 100 % der realen make-Installationen gmake sind.Der Vorschlag, das zeitlose
makeals eine Art Top-Level-Treiber für Build-Systeme zu verwenden, dürfte wahrscheinlich auch in 250 Jahren noch funktionieren.cargo buildverwendet wird.Make selbst ist großartig, aber in den meisten C-Projekten, an denen ich gearbeitet habe, war es eine Kombination aus cmake/autotools und globaler Paketinstallation, und solche Dinge musste ich häufig nachschlagen.
Ich stimme der Stimmung hier zu, schreibe aber vieles auf Justfile um.
https://github.com/casey/just
Damit lassen sich viele der Makefile-typischen Eigenheiten vermeiden.
Ja, Make ist großartig. Ich benutze es für unglaublich viele Dinge. Es ist eine gute Art, Aufgaben zu automatisieren.
Meine persönliche Website zum Beispiel wird über ein Makefile betrieben, das Bash-Skripte aufruft, um aktualisierte Webseiten neu zu erstellen, dann per git push auf den Server überträgt, woraufhin ein Git-Hook auf dem Server Make aufruft und deployt. Allerdings möchte ich häufig geänderte binäre Brocken wie PDFs mit Vorlesungsmaterial nicht im Git-Repository haben. Kein Problem: Im Makefile gibt es ein Target
uploads, das nur geänderte PDFs auf den Server hochlädt, und dieses Target ist eine Abhängigkeit desdeploy-Targets, das den git push ausführt, sodass ich mich nicht darum kümmern muss.Außerdem legt ein anderes Makefile aktualisierte PDFs für Vorlesungsmaterial automatisch in den Quellbaum der Website. Mit diesem Makefile kann ich aus LaTeX-Quellen die PDFs erzeugen, die ich benutze, oder aus denselben Quellen alternative Versionen für Studierende, zum Beispiel ohne Lösungen zu Übungsaufgaben, und sie automatisch in der lokalen Website-Version veröffentlichen, um sie dann bei Bedarf auf die aktualisierte Website hochzuladen.
Es ist eine Art endlose Kette von Makefiles. Ich mag Makefiles.
Was der Autor in diesem Fall wirklich braucht, ist nicht make, sondern just.
https://just.systems/man/en/
Als Task Runner ist make nicht schlecht, aber heutzutage gibt es, wie in anderen Kommentaren erwähnt, bessere Alternativen wie just.
Als Build-System ist make in Ordnung, bis man auf seine Nachteile stößt. make/Makefile ist nicht standardisiert, deshalb gibt es automake. Man schreibt also nicht direkt ein Makefile, sondern verwendet Templates und lässt das eigentliche makefile erzeugen. Wenn man die gesamte Toolchain selbst kontrolliert, ist das egal, aber die meisten tun das nicht; deshalb macht man es so, um portable Makefiles zu gewährleisten.
make führt keinerlei Dependency Resolution durch und geht davon aus, dass alles Benötigte genau dort vorhanden ist. Deshalb gibt es configure-Skripte; und weil auch configure-Skripte kein Standard sind, erzeugt man das configure-Skript mit autoconf/autoreconf, bevor man make-Targets ausführt.
make und benachbarte Tools wie automake/autoconf/autoreconf verwenden mtime, um zu entscheiden, ob Eingaben veraltet sind. Es kann passieren, dass man trotz Ausführung von autoconf/autoreconf/automake/configure in einen Zustand gerät, in dem die Eingaben dauerhaft als veraltet gelten und sich nichts mehr bauen lässt. Zur Einordnung: Viele Build-Systeme kommen mit mtime gut zurecht, sofern es ein ordentliches Dependency Tracking gibt.
Insgesamt liegt der grundlegende Designfehler von make darin, dass es an der Unix-Philosophie ausgerichtet ist: „Tu eine Sache und tu sie gut.“ Diese eine Sache ist: „Wenn Eingaben veraltet sind, baue das Target neu.“ Das ist ein sehr eingeschränktes Tool, sodass moderne Build-Systeme viel darüberstapeln müssen, um es über einen einfachen Task Runner hinaus nützlich zu machen.
makekönne keine Dependencies auflösen?dependency:...target: dependency...make configure. Das richtet die Dinge ein, die fürmake [build]nötig sind.Deshalb verwendet man touchfiles; das ist zwar unsauber, funktioniert aber immer noch besser als vieles andere. Ich meine damit insbesondere das Caching von docker build.