Die Macht von Rake
Marvin Gülker · 12.11.2013
Jeder Rubyist kennt es, jeder benutzt es, jeder weiß, wie man eine Rakefile schreibt. Warum schreibe ich also einen Blogpost über dieses kleine Programm? Weil es aus meiner Sicht viel zu häufig unterschätzt wird. Nach einer Erklärung der Grundlagen befasst dieser Artikel sich dann mit den komplexeren Möglichkeiten des Tools.
Grundlagen
Rake ist ein von Programm zur Automation häufig wiederkehrender Aufgaben, dessen Anfänge weit in die Frühzeit von Ruby hereinreichen. Der erste zurückverfolgbare Commit des von Jim Weirich initiierten Programms datiert auf das Jahr 2003, doch legt dessen Commitnachricht nahe, dass Rake bereits länger existiert („New repository initialized by cvs2svn“). Nachdem mit Rake 0.8.7 2009 zunächst ein Release-Stillstand eingesetzt hatte, erwischte das unangekündigte Release von Rake 0.9.0 zwei Jahre später viele Rubyisten auf dem falschen Fuß, brach es doch Backward-Compatibility und führte einen neuen DSL-Namespace ein. Am 26. Oktober 2012 folgte dann die nächste Überraschung: Rake „übersprang“ ein paar Versionsnummern und wechselte von 0.9.3 direkt zu 10.0.0. In den Release Notes findet sich dazu folgende Begründung:
"Jim, when will Rake reach version 1.0?"
Over the past several years I've been asked that question at
conferences, panels and over twitter. Due to historical reasons (or
maybe just plain laziness) Rake has (incorrectly) been treating the
second digit of the version as the major release number. So in my head
Rake was already at version 9.
Well, it's time to fix things. This next version of Rake drops old,
crufty, backwards compatibility hacks such as top level constants, DSL
methods defined in Object and numerous other features that are just no
longer desired. It's also time to drop the leading zero from the
version number as well and call this new version of rake what it
really is: Version 10.
So, welcome to Rake 10.0!
Ursprünglich begann Rake als das Ruby-Pendant zum Unix-Urgestein
make
, d.h. zur Automation von Quellcodekompilation. Mittlerweile hat
es sich aber weit über dieses Anwendungsfeld hinaus verbreitet und
dient zur Automation von schier allem, was sich irgendwie auf der
Kommandozeile automatisieren lässt (lassen muss). Beispiele sind etwa
Website-Deployment, C-Extensions oder Gem-Erstellung.
Mittlerweile (seit Ruby 1.9) ist Rake Teil der Ruby-stdlib und muss als
solches nicht mehr nachinstalliert werden. Das Programm wird gesteuert
über eine Datei namens Rakefile
(alternativ auch Rakefile.rb
, auch
wenn dies nicht oft gesehen wird), in der die einzelnen Aufgaben
(Tasks) definiert werden, die Rake später ausführen können soll. Bei
der Rakefile selbst handelt es sich um gewöhnlichen Ruby-Code, sodass
eigens für Rake keine neue Sprache erlernt werden muss.
Beispielhaft will ich Ihnen nun zeigen, wie man das altbekannte Hello-World-Programm in Rake formuliert. Dafür legen Sie zunächst ein Verzeichnis an, in dem wir arbeiten werden:
$ mkdir ~/rake $ cd ~/rake
In diesem Verzeichnis legen Sie die Datei Rakefile
an:
task :hello do puts "Hello, World!" end
Die Methode DSL#task (in der Rakefile
wird das Modul Rake::DSL
bereits von Rake selbst in den Toplevel-Namespace eingebunden) ist die
einfachst mögliche Methode, um eine Aufgabe zu definieren. Als Argument
nimmt sie ein Symbol entgegen, das den Namen des Tasks angibt, unter dem
Sie ihn später aufrufen können, der ihr mitgegebene Block stellt den
eigentlichen Aufgabenkörper dar, welcher für den Moment nichts
besonderes darstellt. Speichern Sie die Datei ab und rufen Sie rake
mit dem Namen des gewünschten Tasks auf:
$ rake hello Hello, World!
Der Standardtask
Sie können Ihre Tasks nennen, wie Sie es für richtig halten, ohne dass
Ihnen Rake große Vorgaben macht. Von dieser Regel gibt es nur zwei
Ausnahmen: Tasknamen dürfen keine Doppelpunkte enthalten (:"my:task"
wäre ungültig) und der Task mit dem Namen :default
hat eine spezielle
Bedeutung: er wird ausgeführt, wenn rake
ohne weiteres Argument
aufgerufen wird. Folgende Rakefile
:
task :default do puts "Hello, World!" end
Erlaubt den Aufruf von Rake ohne Argument:
$ rake Hello, World!
Suchpfad
rake
sucht die Rakefile
bei Aufruf zunächst im aktuellen
Arbeitsverzeichnis. Kann es sie dort nicht finden, hangelt es
Verzeichnis für Verzeichnis nach oben, bis es in einem Elternverzeichnis
die Rakefile
gefunden hat. Bevor diese geladen wird, wechselt Rake in
das Verzeichnis der Rakefile (d.h. Dir.pwd
wird das Verzeichnis der
Rakefile zurückgeben) und speichert den ursprünglichen Aufrufort unter
Rake.original_dir
. Beispiel:
task :default do puts "Arbeitsverzeichnis: #{Dir.pwd}" puts "Aufrufverzeichnis: #{Rake.original_dir}" end
Aufruf:
$ rake Arbeitsverzeichnis: /home/quintus/rake Aufrufverzeichnis: /home/quintus/rake $ mkdir foo $ cd foo $ rake (in /home/quintus/rake) Arbeitsverzeichnis: /home/quintus/rake Aufrufverzeichnis: /home/quintus/rake/foo
Beachten Sie auch, wie Rake das Verzeichnis ausgibt, in dem es die
Rakefile
gefunden hat, wenn dieses nicht dem momentanen
Arbeitsverzeichnis entspricht.
Ein letzter Satz dazu: Sie können Rake mit der Option -f
vorschreiben,
welche Datei Rake zu benutzen hat. Bei Benutzung dieser Option aber
ändert Rake nicht sein Arbeitsverzeichnis!
$ rake -f ../Rakefile Arbeitsverzeichnis: /home/quintus/rake/foo Aufrufverzeichnis: /home/quintus/rake/foo $ cd .. && rmdir foo
Beschreibungen
Mittels #task erstellte Tasks können mithilfe von #desc mit einer Beschreibung versehen werden.
desc "Outputs Hello World" task :hello do puts "Hello, World!" end
Wird Rake mit dem Paraketer -T
aufgerufen, zeigt es diese
Beschreibungen an:
% rake -T rake hello # Outputs Hello World
Abhängigkeiten
Jeder Task kann von einem oder mehreren anderen Tasks abhängen. Dazu macht Rake regen Gebrauch von Rubys syntaktischem Zucker für Hash-Argumente:
task :hello do puts "Hello!" end task :world => :hello do puts "World!" end task :goodbye do puts "Goodbye World!" end task :default => [:world, :goodbye]
Im obigen Beispiel sehen wir verschiedene Tasks mit ihren
Abhängigkeiten. Während die Tasks :hello
und :goodbye
für sich
genommen keine Besonderheit darstellen, handelt es sich bei :world
um
einen Task mit einer Abhängigkeit. Dies weist Rake an, vor der
Ausführung von :world, den Task :hello zu erfüllen11
Wohlgemerkt, nicht notwendigerweise auszuführen. Wir werden
später Tasks kennenlernen, die auch erfüllt sind, obwohl sie
nicht ausgeführt werden.
. Ähnlich
verhält es sich mit dem Standardtask: Er hängt sowohl von :world
als
auch von :goodbye
ab, und zwar in genau der Reihenfolge. Da :world
seinerseits wieder von :hello
abhängt, entsteht für den Standardtask
folgende Abhängigkeitskette:
:default | +------ :world | | | +----- :hello | +------ :goodbye
Das mag etwas einschüchternd aussehen, ist im Grunde aber ganz einfach.
Bei der Ausführung wird zunächst die erste Abhängigkeit von :default
gefunden, :world
. Dieser Prozess wird dort wiederholt, sodass man
:hello
findet. :hello
hat keine Abhängigkeiten, also muss dieser
zunächst erfüllt werden. Da :world
keine weiteren Abhängigkeiten hat,
wird nun :world
erfüllt. :default
aber hat noch eine weitere
Abhängigkeit, :goodbye
, die nun ebenfalls geprüft (keine
Abhängigkeiten) und dann erfüllt wird. Zuletzt wird :default
erfüllt.
Kurz:
:hello
:world
:goodbye
:default
Ausgeführt schaut es so aus:
$ rake Hello! World! Goodbye World!
Wie dem aufmerksamen Leser aufgefallen sein mag, besaß der Task
:default
aus obigen Beispiel keinen eigenen Körper. Dies ist insoweit
zulässig, wie ein Task sich ausschließlich über seine Abhängigkeiten
definieren kann: :default
ist dann erfüllt, wenn :world
und
:goodbye
erfüllt sind. Das kann man sich wahlweise wie einen Task mit
leerem Körper oder wie ein Schiffstau, das ein Ladung zusammenbindet,
vorstellen. Ein leerer Task ohne Abhängigkeiten mag zwar möglich sein
(er wirft keine Exception), aber hat auch keinen Nutzen.
Befehle
An diesem Punkt nun haben wir die grundlegende Funktionalität von Rake abgedeckt. Selbst wenn Sie bisher kaum mit Rake zu tun hatten, dürfte das obige wenn vielleicht auch nicht selbstverständlich, so doch kaum neu gewesen sein. Dieser und die folgenden Abschnitte nun befassen sich mit der erweiterten Verwendung von Rake und den verschiedenen Arten von Tasks, die definiert und erfüllt werden können.
Zunächst wollen wir uns dem Befehlssatz von Rake widmen. Obwohl in einem
Task grundsätzlich beliebiger Ruby-Code zulässig ist, hält Rake im Sinne
der Lesbarkeit ein Reportoire an Methoden für häufig gebrauchte Aufgaben
bereit. So bindet Rake grundsätzlich das allseits bekannte
FileUtils
-Modul (oder genauer gesagt: FileUtils::Verbose
) in den
Task-Kontext ein, sodass dateibezogene Tasks aussehen wie eine
Shellsitzung:
task :default do rm "file.txt" mkdir "mydirectory" rm_r "other_directory" end
Die genauen Methoden können der FileUtils-Dokumentation entnommen werden.
Über diese Standardfunktionalität hinaus erweitert Rake das
FileUtils
-Modul eigenmächtig um einige weitere Methoden, die
demenstprechend auch in Ihren Tasks zur Verfügung stehen.
Da ist zum einen die Methode FileUtils#sh, die einen Shellout durchführt:
task :default do sh "dd if=/dev/zero of=/dev/sda" end
Sie ermöglicht ihnen, beliebige Programme von ihrem System auszuführen; im Grunde ist sie nur ein leichter Wrapper um Rubys eigene Methode #system.
In eine ähnliche Sparte fällt die Methode FileUtils#ruby. Anders als
FileUtils#sh konsultiert diese jedoch nicht die PATH-Umgebungsvariable,
sondern nutzt das RbConfig
-Modul aus Rubys stdlib, um den Pfad zum
aktiven Ruby-Interpreter festzustellen und diesen in einer neuen Instanz
auszuführen:
task :default do # Führt die Datei `foo.rb' unter einem neuen # Interpreter aus: ruby "foo.rb" end
Ebenfalls erwähnenswert ist FileUtils#safe\_ln, das versucht, einen Soft- bzw. Hardlink zu erstellen, aber bei Fehlschlag (unter Windows etwa) stattdessen eine Kopie der Datei anfertigt.
task :default do open("myfile.txt", "w"){|f| f.puts("Stuff")} safe_ln "myfile.txt", "alsomyfile.txt" end
Alle von Rake zu FileUtils hinzugefügten Methoden finden sich in dessen Dokumentation.
Parallele Tasks
Rake ist in der Lage, mehrere Tasks gleichzeitig auszuführen. Dazu bietet Rake die Methode DSL#multitask an, die die übliche Auswertungsreihenfolge der Abhängigkeiten eines Tasks aufhebt und sie stattdessen zeitgleich ausführt:
puts "Starting: #{Time.now.inspect}" task :dep1 do sleep 10 puts "Done 1: #{Time.now.inspect}" end task :dep2 do sleep 5 puts "Done 2: #{Time.now.inspect}" end multitask :default => [:dep1, :dep2] do puts "Done all: #{Time.now.inspect}" end
Ausführen:
$ rake Starting: 2013-11-12 11:41:16 +0100 Done 2: 2013-11-12 11:41:21 +0100 Done 1: 2013-11-12 11:41:26 +0100 Done all: 2013-11-12 11:41:26 +0100
Ein Hinweis dazu: Rake führt grundsätzlich alle Abhängigkeiten eines
Multitasks gleichzeitig aus. Das kann bei zahlreichen Abhängigkeiten
dazu führen, dass das System überlastet wird. Daher bieten neuere
Versionen von Rake die Option -j
an, mit der sich die Anzahl
gleichzeitig ausgeführter Tasks limitieren lässt. Jedenfalls
theoretisch, beim Testen konnte ich mit obigen Beispiel keinen
Unterschied feststellen, obwohl doch die Gesamtausführzeit bei -j 1
15
Sekunden hätte betragen müssen.
Dateitasks
Rake wurde wie eingangs erwähnt ursprünglich für die Kompilation von
Quellcode geschaffen. So ist es wenig verwunderlich, das Rake sich bei
der Behandlung von Dateien ganz am Vorbild, dem unixoiden make
orientiert. An dieser Stelle möchte ich jedoch einen entscheidenden
Unterschied zum Original-Make hervorheben: make
kennt nur
dateibasierte Tasks; die einzige Ausnahme sind solche, die explizit mit
.PHONY
als dateiunabhängig markiert wurden. Rake dagegen kennt eine
Menge verschiedener Tasks, die grundsätzlich alle phony sind. Die
einzige Ausnahme hiervon bilden explizit dateibasierte Tasks. Man könnte
also sagen, dass make
eine Whitelist bemüht, wohingegen Rake mit einer
Blacklist arbeitet.
Rake kennt zwei Arten von dateibasierten Tasks: FileTasks und DirectoryTasks. Erstere gelten als erfüllt, wenn die von ihnen referenzierte Datei
- existiert und
- ihr Zeitstempel neuer ist als derjenige der Rakefile und
- keine der Dateien, von denen der Task abhängt, einen neueren Zeitstempel trägt als die referenzierte Datei.
Trifft irgendeine dieser Bedingungen nicht zu, so muss die referenzierte Datei (neu) erstellt werden, indem ihr Task-Körper ausgeführt wird.
DirectoryTasks haben keinen Task-Körper. Sie gelten als erfüllt, wenn das von ihnen referenzierte Verzeichnis existiert, andernfalls wird es rekursiv erstellt.
FileTasks werden mithile der Methode DSL#file erstellt, DirectoryTasks mit DSL#directory:
directory "foo" file "foo/myfile.txt" => "foo" do puts "Creating myfile.txt" open("foo/myfile.txt", "w"){|f| f.write("Hi there")} end task :default => "foo/myfile.txt"
Mithilfe von DSL#directory und DSL#file erstellte Tasks können genau wie mit DSL#task und DSL#multitask erstellte Tasks als Abhängigkeiten anderer Tasks auftauchen und natürlich auch selbst Abhängigkeiten haben. Der einzige Unterschied zu „gewöhnlichen“ Tasks besteht darin, dass sie sich nicht über einen Namen (Symbol), sondern über einen Pfad (String) definieren und über einen solchen angesprochen werden.
Alle Dateipfade sind relativ zum Arbeitsverzeichnis von Rake. Wie bereits früher erwähnt, ändert Rake dieses in das Verzeichnis, in welchem sich die Rakefile befindet; möchte man dies verhindern und alle Dateipfade relativ zum ursprünglichen Aufrufsverzeichnis auflösen, empfiehlt sich folgender Code außerhalb einer Taskdefinition (solcher wird ganz normal vor Ausführung der Tasks abgearbeitet):
Dir.chdir(Rake.original_dir)
Führt man das obige Beispiel aus, ergibt sich folgendes:
$ rake mkdir -p foo Creating myfile.txt $ rake $ rm -r foo
Man beachte, das beim zweiten Aufruf weder das Verzeichnis foo/
noch
die Datei foo/myfile.txt
neu angelegt wurden.
Regeln
Regeln stellen ein besonders mächtiges Werkzeug für die Dateierzeugung
dar. Sie werden dann bedeutsam, wenn man einen Haufen ähnliche
Dateitasks schreiben müsste, die sich eigentlich nur im Dateinamen
unterscheiden. Das typische Beispiel sind Quellcodekompilationen.
Angenommen, Sie arbeiten an einem C++-Projekt mit drei .cpp
-Dateien.
Das Rakefile für die Kompilation könnte so aussehen:
CXX = ENV["CXX"] || "g++" LD = ENV["LD"] || "g++" CXXFLAGS = ENV["CXXFLAGS"] || "-Wall " LDFLAGS = ENV["LDFLAGS"] || "" file "file1.o" => "file1.cpp" do sh "#{CXX} -c #{CXXFLAGS} file1.cpp -o file1.o" end file "file2.o" => "file2.cpp" do sh "#{CXX} -c #{CXXFLAGS} file2.cpp -o file2.o" end file "file3.o" => "file3.cpp" do sh "#{CXX} -c #{CXXFLAGS} file3.cpp -o file3.o" end file "myexecutable" => ["file1.o", "file2.o", "file3.o"] do |t| sh "#{LD} #{LDFLAGS} -o #{t.name} #{t.prerequisites.join(' ')}" end task :default => "myexecutable"
Natürlich könnte man die drei Dateitasks auch mit Ruby-Bordmitteln automatisch erstellen, also etwa so:
# ... %w[file1 file2 file3].each do |name| file "#{name}.o" => "#{name}.cpp" do sh "#{CXX} -c #{CXXFLAGS} #{name}.cpp -o #{name}.o" end end # ...
Rake ist grundsätzlich offen für derartige Konstrukte, bietet mit Regeln aber eine sehr viel elegantere Möglichkeit an. Eine Regel definiert, wie Dateien, die auf ein bestimmtes Muster passen (typischerweise auf eine bestimmte Endung) generell erzeugt werden. Wann immer Rake einen Task findet, der von einer Datei abhängt, für die kein spezifischer Task definiert wurde, versucht es, eine passende Regel anzuwenden und den Task auf diese Weise dynamisch zu erstellen. Eine einfache Regel für unseren obigen Fall könnte so aussehen:
#... rule ".o" => [".cpp"] do |t| sh "#{CXX} -c #{CXXFLAGS} #{t.source}.cpp -o #{t.name}.o" end #...
Diese Regel ersetzt die drei file
-Tasks vollständig und erlaubt es
darüber hinaus, dynamisch auf neue Dateien mit demselben Muster zu
reagieren. t.source
stellt hierbei die erste Abhängigkeit des Targets
zur Verfügung, t.name
das Target selbst.
Regeln sind oft sperrig zu debuggen, da nicht immer vorhersehbar ist,
welche Dateien tatsächlich die Regel erfüllen und welche nicht. Es ist
für den Regelschreiber daher essentiell, sich mit der Option --rules
für rake
zu beschäftigen; diese Option veranlasst Rake, genau
aufzulisten, welche Dateien welche Regel erfüllen und welche nicht. Noch
mehr Details erhalten Sie, wenn Sie Rake wie folgt ausführen:
$ rake --verbose --trace --rules <task>
Pathmap
Regeln sind nicht auf Dateiendungen beschränkt, sondern können sehr viel komplexeren Mustern folgen. Dieses momentan zwar dokumentierte, aber kaum irgendwo erwähnte Feature nennt sich Pathmap-Substitution. Kurz gesagt funktioniert es ähnlich wie die bekannte Formatierungsfunktion #sprintf, bietet aber andere Substitutionsmuster an, die speziell auf Dateipfade zugeschnitten sind. Diese Muster können Sie ausschließlich in der Abhängigkeitenliste einer Regel verwenden, die sich ja immer auf die eigentliche Targetdatei bezieht. Für das Target können Sie, wenn Ihnen Dateiendungen nicht genügen, auf die weiter unten beschriebenen Regulären Ausdrücke zurückgreifen.
Ein Beispiel. Sie sind ein ordnungsliebender Mensch und haben ihren
Quellcode in das Verzeichnis src/
unterhalb Ihrer Projektwurzel
gepackt. Eine bloße Substitution der Dateiendung würde zur Kompilation
des Quellcodes nicht mehr genügen, denn Rake kann ja nicht wissen, dass
die Datei, von der ein Task abhängt, sich in einem anderen Verzeichnis
befindet. Leider erweist sich
rule ".o" => ["src/.cpp"] do |t| # ... end
als nicht funktional, Rake kann die Abhängigkeit nicht auffinden. Der
Slash suggeriert Rake, dass hier eine einzelne Datei als Abhängigkeit
angegeben wurde, und zwar fix die Datei .cpp
im Verzeichnis src/
,
die natürlich nicht existiert und von Rake auch nicht erstellt werden
kann. Abhilfe schafft eine Pathmap-Regel:
rule ".o" => ["src/%n.cpp"] do |t| # ... end
Wenn Rake ein Prozentzeichen % in einer Abhängigkeit findet, wendet es
automatisch Pathmap-Substitution an und versucht, die Datei ausfindig zu
machen. Konkret hieße das etwa, dass wenn die Datei file1.o
gebaut
werden soll, Rake auf diesen Namen das Muster src/%n.cpp
anwendet —
wie Sie der
Dokumentation
von String#pathmap entnehmen können, wird %n
durch den Basename der
Datei ersetzt, d.h. das Ergebnis hier lautet src/file1.cpp
. Oder
konkret als normale file
-Regel ausgedrückt:
file "file1.o" => ["src/file1.cpp"] do |t| #... end
String#pathmap, das von Rake definiert wird, bietet Ihnen viele weitere Möglichkeiten, die Abhängigkeiten einer Regel deutlich dynamischer zu fassen als eine bloße Substitution der Dateiendung. Um mit der Methode zu experimentieren, bevor Sie sie in einer Regel wie gezeigt ausnutzen, können Sie die IRB bemühen:
$ irb -rrake irb(main):001:0> "/tmp/myfile.o".pathmap("src/%n.cpp") => "src/myfile.cpp" irb(main):002:0> end
Regeln mit Regulären Ausdrücken
Wie schon weiter oben angesprochen, können Sie maximale Flexibilität erreichen, indem Sie nicht nur die Abhängigkeitenliste, sondern auch das Target möglichst dynamisch definieren. Rake erlaubt es, im Target einer Regel nicht nur auf Dateiendungen zu prüfen, sondern sogar Reguläre Ausdrücke zu verwenden. Rubys Parser allerdings ist von dieser Möglichkeit nicht ganz so begeistert und wird Code à la
rule /foo/ => ["foo"] do File.open("foo", "w"){...} end
möglicherweise mit
warning: ambiguous first argument; put parentheses or even spaces
quittieren und den ganzen Codeblock anders interpretieren als gewünscht. Das lässt sich umgehen, indem Sie entweder Klammern verwenden (was unästhetisch aussieht) oder die alternative Literalsyntax für Reguläre Ausdrücke in Ruby verwenden:
rule %r{foo} => ["foo"] do File.open("foo", "w"){...} end
Das %r{}
-Konstrukt schluckt der Parser ohne großes Meckern.
Um das Beispiel aus dem vorigen Abschnitt wieder aufzugreifen: Sie
wollen nicht nur, dass der Quellcode in einem separaten Verzeichnis
src/
liegt, sondern auch noch, dass der Objektcode in einem separaten
Verzeichnis build/
abgelegt wird. Indem Sie lediglich auf die
Dateiendung abstellen, lässt sich dies nicht mehr erreichen. Mit einem
Regulären Ausdruck aber ist es kein Problem mehr (t.prerequisites
gibt
die gesamte aufgelöste Abhängigkeitenliste zurück):
rule %r{build/.*\.o$} => ["src/%n.cpp"] do |t| sh "#{CXX} -c #{CXXFLAGS} #{t.source} -o #{t.name}" end file "myexecutable" => ["build/file1.o", "build/file2.o", "build/file3.o"] do |t| sh "#{LD} #{LDFLAGS} -o #{t.name} #{t.prerequisites.join(' ')}" end
Der Reguläre Ausdruck erfasst alle .o
-Dateien im Verzeichnis build/
und erfüllt so die Abhängigkeiten des myexecutable
-Dateitasks. Achten
Sie darauf, dass Sie bei der Verwendung eines Regulären Ausdrucks diesen
unbedingt an seinem Ende verankern sollten ($
), da er sonst
möglicherweise auf Dateien passt, die an ganz anderer Stelle der
Kompilation referenziert werden.
Namespaces
Ähnlich gelagerte bzw. zusammengehörige Tasks können in Namespaces
unterteilt werden. Ein Beispiel etwa aus dem Rails-Bereich ist der
Namespace db
, der Tasks wie db:migrate
(Datenbank-Migrations
durchführen) oder db:seed
(Datenbank mit Beispielwerten füllen)
enthält. Namespaces werden im Aufruf durch einen Doppelpunkt kenntlich
gemacht (daher ist der Doppelpunkt als Teil eines Tasknames nicht
zulässig) und mithilfe der Methode DSL#namespace definiert:
namespace :db do task :migrate do # ... end task :seed do # ... end end
Innerhalb von #namespace können alle Arten von Tasks außer dateibasierte Tasks verwendet werden (was auch logisch erscheint).
Rakelib
Ein kaum bekanntes Feature von Rake ist die Rakelib. Entgegen
landläufiger Meinung ist man nicht gezwungen, alle Tasks in einer
einzelnen Rakefile zu definieren oder ggf. andere Dateien mithilfe von
require
einzubinden. Zwar unterstützt Rake mittlerweile
#require\_relative (lange Zeit hieß es bei dessen Verwendung nur „Can't
determine base path“), doch bietet Rake eine interessante Alternative
dazu. Beim Start liest Rake nach der eigentlichen Rakefile
zusätzlich alle .rake
-Dateien in dem Verzeichnis rakelib/
, das sich
im selben Verzeichnis wie die Rakefile
befinden muss, ein. So können
umfangreiche Rakefiles leicht in mehrere Dateien aufgespalten werden,
ohne auch nur eine Zeile Code ändern zu müssen. Beispielsweise könnte
etwa der aufgrund der Gemspec sehr umfangreiche Gem::PackageTask
(siehe unten) in eine Datei rakelib/gem.rake
ausgelagert werden.
% tree . ├── Rakefile └── rakelib └── gem.rake 1 directory, 2 files
Weitere Tasks
Neben den vorgestellten standardmäßigen Möglichkeiten von Rake gibt es
zahlreiche Libs, die eigene Rake-Tasks mitbringen und die üblicherweise
durch ein einfaches require
eingebunden werden können. Ich möchte
einige davon im Folgenden vorstellen.
Clean und Clobber
Rake selbst kommt mit einer Taskkombination zum Aufräumen, den Clean- und Clobber-Tasks. Sie können mithilfe von
require "rake/clean"
eingebunden werden und dienen dazu, generierte Dateien sauber zu
entfernen. clean
ist dabei dazu gedacht, alle temporär generierten
Dateien zu entfernen, die nur Zwischenprodukt eines anderen Tasks waren
— typisches Beispiel dafür sind die von einer Kompilation übrig
gebliebenen Objektdateien. clobber
dient dazu, das gesamte Projekt in
den „Auslieferungszustand“ zurückzuversetzen. Um am Beispiel zu bleiben:
Während clean
nur die .o
-Dateien entfernt, löscht clobber
auch
sämtliche generierten Executables.
Zur Befüllung dieser zwei Tasks bietet die Lib zwei Konstanten CLEAN
und CLOBBER
an, bei denen es sich um Rake::FileList
-Instanzen
handelt, d.h. insbesondere, dass sie auf die Methode #include hören, die
einen Shell-Glob annimmt. Angewandt auf obiges Beispiel könnte man also
schreiben:
CLEAN.include("*.o") CLOBBER.include("myexecutable")
Da clobber
von clean
abhängt, würde ein rake clean
nur die
Objektdateien entfernen, rake clobber
dagegen sowohl diese als auch
zusäzlich die Executable myexecutable
.
Package-Task
Sehr praktisch ist der von
RubyGems
angebotene Gem::PackageTask
. Wer zu faul ist, Gems händisch mit
gem build
zu bauen, kann den Paketierprozess auch komplett von Rake
übernehmen lassen:
require "rubygems/package_task" GEMSPEC = Gem::Specification.new do |spec| spec.name = "ruby-xz" spec.summary = "XZ compression via liblzma for Ruby." spec.description =<<DESCRIPTION This is a basic binding for liblzma that allows you to create and extract XZ-compressed archives. It can cope with big files as well as small ones, but doesn't offer much of the possibilities liblzma itself has. DESCRIPTION spec.version = XZ::VERSION.gsub("-", ".") spec.author = "Marvin Gülker" spec.email = "quintus@quintilianus.eu" spec.license = "MIT" spec.homepage = "http://quintus.github.io/ruby-xz" spec.platform = Gem::Platform::RUBY spec.required_ruby_version = ">=1.9.3" spec.add_dependency("ffi") spec.add_dependency("io-like") spec.add_development_dependency("emerald") spec.add_development_dependency("turn") spec.files.concat(Dir["lib/**/*.rb"]) spec.files.concat(Dir["**/*.rdoc"]) spec.files << "COPYING" << "VERSION" spec.has_rdoc = true spec.extra_rdoc_files = %w[README.rdoc HISTORY.rdoc COPYING] spec.rdoc_options << "-t" << "ruby-xz RDocs" << "-m" << "README.rdoc" end Gem::PackageTask.new(GEMSPEC).define
Das Beispiel enstammt meinem Gem
ruby-xz, mit dem einzigen
Unterschied, dass ich die Gemspec dort in eine separate Datei
ausgelagert habe. rake gem
paketiert nun ein komplettes RubyGem und
platziert es im Verzeichnis pkg/
.
RDoc-Task
Rubys Dokumentationstool RDoc bietet ebenfalls einen
Task an. Dieser kann dazu
genutzt werden, die Dokumentation für das Projekt zu erstellen, sodass
man sie später etwa auf der eigenen Website veröffentlichen könnte. Ein
Beispiel ebenfalls aus ruby-xz
:
Rake::RDocTask.new do |rd| rd.rdoc_files.include("lib/**/*.rb", "**/*.rdoc", "COPYING") rd.title = "ruby-xz RDocs" rd.main = "README.rdoc" rd.generator = "emerald" rd.rdoc_dir = "doc" end
rdoc_files.include
akzeptiert Wildcards der zur dokumentieren Dateien,
main=
legt die Startseite der Dokumentation fest (diese Angabe bitte
niemals auslassen), title=
den Titel der Dokumentation und
rdoc_dir=
das Verzeichnis relativ zur Rakefile, in dem die
HTML-Dokumentation abgelegt wird. Wem RDocs Darkfish nicht gefällt, der
kann mit generator=
einen
vernünftigen Generator
festlegen.
Schlusswort
Dieser Beitrag sollte verdeutlichen, dass Rake mehr ist als einfach nur
ein grobes "mach-das-Werkzeug" und es weitaus mehr Möglichkeiten gibt
als nur per task
einfache Tasks zu definieren. Dennoch ist dieser
Artikel noch lange nicht erschöpfend. Für die weitere Auseinandersetzung
möchte ich Ihnen daher die
Dokumentation
von Rakefiles nahelegen.
Fußnoten:
Wohlgemerkt, nicht notwendigerweise auszuführen. Wir werden später Tasks kennenlernen, die auch erfüllt sind, obwohl sie nicht ausgeführt werden.