make – der bessere Task-Runner für das Web Development

Fast im Wochentakt kann man das Erscheinen neuer Task-Runner-Systeme beobachten – sie alle versprechen weniger Bloat, leichteres Erstellen der Tasks und immer dreimal besser zu sein als die Konkurrenz. Heutzutage basieren diese System meist auf Node.js, da es hier mittlerweile einen riesigen Fundus an Tools gibt, die für die einzelnen Aufgaben eines Task-Runners für das Web Development benötigt werden.

Ich habe einige dieser Systeme (z. B. Grunt, Gulp) auch produktiv benutzt. Mittlerweile wird aber von mir wieder das selbe Werkzeug für diese Aufgaben eingesetzt wie bereits vor über zehn Jahren: make.

make ist ein Build-Management-Programm und kann beliebige Kommandos in Abhängigkeit von bestimmten Bedingungen ausführen. Der Ansatz unterscheidet sich grundlegend von dem der modernen Task-Runner.

Am einem Beispiel lässt sich das Konzept erklären: Ich möchte alle für eine Website benötigten Javascript-Dateien in eine einzige Datei zusammenfassen und davon dann eine minifizierte Version erstellen, die danach deployed werden kann.

Einem Task-Runner würde man in etwa sagen: „Nimm diese Liste von Dateien, hänge sie aneinander und speichere das Ergebnis als Datei. Danach nimm diese Datei, minifiziere sie und speichere sie ebenfalls ab.“

Mit make geht es eleganter: Statt einer Liste von abzuarbeitenden Kommandos gibt man einerseits die Voraussetzungen (Prerequisites) an, die erfüllt sein müssen, um die Zieldatei (Target) zu erzeugen. Zum anderen spezifiziert man die Kommandos, welche nach der Erfüllung der Voraussetzungen die Zieldatei erzeugen. Make wird dann zuerst rekursiv alle Voraussetzungen erfüllen, um zum Schluss die Zieldatei zu erzeugen. Klingt kompliziert, ist es aber nicht. Ein Beispiel:

assets/site.js: resources/jquery.js \
resources/app.ux.js \
resources/app.comments.js
	cat $^ > $@

Diese Datei nennt sich im make-Jargon Makefile und wird auch unter diesem Dateinamen gespeichert. Im Makefile ist ein sogenanntes Target samt seiner Prerequisites sowie die Kommandos zur Erstellung der Zieldatei definiert.

Das Target lautet hier assets/site.js und steht vor dem Doppelpunkt. Darauf folgend sind die Prerequisites in der selben Zeile aufgezählt (bzw. in aufeinanderfolgenden Zeilen die mit dem escapten Newline  \ enden, wie im Beispiel zu sehen). Das oder die Kommandos stehen mit jeweils einem Tabulator-Zeichen (!) eingerückt unter dem Target.

Die Voraussetzungen für das Erzeugen der Datei assets/site.js sind in Form von Dateinamen aufgezählt und lauten resources/jquery.js resources/app.ux.js resources/app.comments.js. Das Kommando erzeugt das Target durch einfaches Aneinanderhängen der Prerequisite-Dateien mit dem Unix-Kommando cat. Hierbei kommen zwei Variablen zum Einsatz, die von make automatisch gefüllt werden: $^ ist die Liste der Prerequisites und $@ ist das Target. Ausgeschrieben lautet das Kommando:

cat resources/jquery.js \
resources/app.ux.js \
resources/app.comments.js > assets/site.js

Der Aufruf von make assets/site.js baut jetzt diese Javascript-Datei. Clever dabei: make wird die Datei nur erzeugen, wenn sie noch nicht existiert oder mindestens eine der Prerequisites zwischenzeitlich geändert wurde. Ruft man das Kommando zweimal nacheinander auf, wird make beim zweiten Mal sagen, dass es nichts zu tun gibt, da die Zieldatei schon aktuell ist.

Ein zweites Target könnte dann die minifizierte Variante der Javascript-Datei erzeugen:

assets/site.min.js: assets/site.js
    uglifyjs $< --mangle --comments > $@

Update: Steffen wies mich per E-Mail darauf hin, dass obiges Make-Target unter Umständen zu Problemen führen kann, nämlich dann, wenn in dem Beispiel `uglifyjs` fehlschlägt. Die Ausgabedatei wird sofort durch die Shell erzeugt – und zwar unabhängig davon, ob das Kommando erfolgreich ist oder mit einem Fehler beendet wird. Im Fehlerfall liegt dann eine leere Zieldatei vor ohne dass irgendeine Fehlerbehandlung durchgeführt wurde oder man den Fehler überhaupt bemerkt. Besser ist es also, das Target folgendermaßen zu definieren:

assets/site.min.js: assets/site.js
    uglifyjs $< --mangle --comments > $@ || { rm -f $@ ; exit 2 ; }

So bekommt man sofort mit, wenn irgendetwas schief läuft und verarbeitet nicht aus Versehen eine leere Datei weiter.

Man sieht, dass jetzt die eben erzeugte Datei assets/site.js als Prerequisite für die minifizierte Datei dient. Die Variable $< steht dabei für die erste Prerequisite aus der Liste; in diesem Fall wird sie durch assets/site.js ersetzt.

Wenn jetzt make assets/site.min.js aufgerufen wird, stellt make sicher, dass zuerst assets/site.js erzeugt wird (siehe oben, dort ist das Target assets/site.js definiert) und führt dann das Kommando zur Minifizierung aus. Auch hier wird make nur tätig, wenn das Ziel noch nicht existiert oder sich eine der Quelldateien zwischenzeitlich geändert hat.

Mit diesem Prinzip kann man jetzt alle benötigten Assets für eine Website erzeugen, von Image-Sprites über robots.txt-Dateien bis hin zu Konfigurations-Dateien und vieles andere mehr, von sehr simpel bis beliebig komplex.

Die größten Vorteile von make sind meines Erachtens:

  • die Geschwindigkeit: bei meinen Websites ist make im Durchschnitt dreimal schneller als Gulp (inkl. der langsamen Tasks wie JavaScript Minifying usw.). Der Unterschied zwischen zehn Sekunden und drei Sekunden Build-Dauer ist nicht zu vernachlässigen. Vor allem wenn man das zig Mal pro Tag macht.
  • die Verfügbarkeit: make ist auf so ziemlich jedem unixoiden Betriebssystem dabei bzw. lässt sich aus Paketquellen sofort installieren
  • die Einfachheit: make ist eine einzige ausführbare Datei vom kaum 200 KiB Größe (Linux, x86_64). Moderne JavaScript-Task-Runner haben viele Abhängigkeiten, die man mit npm installieren muss – was aber erst geht, wenn man npm selbst installiert hat, für welches man wiederum erst mal Node.js installieren muss…[1]
  • make folgt der Unix-Philosophie: anstatt das Rad jedes Mal neu zu erfinden, nutzt man mit make die Möglichkeiten, die Unix bietet, z. B. Pipes und Redirections, mit denen simple Werkzeuge verknüpft werden um komplexe Aufgaben zu lösen.
  • die klarere Strukturierung der Anweisungen: ein Makefile ist üblicherweise leichter lesbar als die Anweisungen für einen JavaScript-Task-Runner, da make selbst auf komplexe Funktionialitäten, wie zum Beispiel Closures verzichtet.
  • make macht nicht mehr als es wirklich tun muss: anstatt z. B. die kompletten Assets bei jedem Aufruf neu zu bauen, wird make nur die Targets ausführen, deren Prerequisites sich auch wirklich geändert haben. Ändert man etwas am CSS, wird make nicht auch die JavaScript-Assets bauen, da es weiß, dass dieses bereits aktuell sind. Sind alle Targets aktuell, wird make das auch direkt so mitteilen: `make: Nothing to be done for ‚assets’`.

make gibt es in verschiedenen Versionen, ich setze GNU make ein: Homepage.


[1] Zugegeben, das ist nicht ganz fair, da Programme wie uglifyjs üblicherweise ebenfalls per npm installiert werden. npm und Node.js sind also unter Umständen doch Voraussetzung.

Veröffentlicht von

Marcus Jaschen

Ich bin selbstständiger Webentwickler und Systemadministrator und bin unter anderem für MTB-News.de, Europas größte Mountainbike-Website tätig. Meine freie Zeit verbringe ich mit Radsport und Fotografie.