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.

Kategorien: Ruby, Software

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:

  1. :hello
  2. :world
  3. :goodbye
  4. :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

  1. existiert und
  2. ihr Zeitstempel neuer ist als derjenige der Rakefile und
  3. 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:

1

Wohlgemerkt, nicht notwendigerweise auszuführen. Wir werden später Tasks kennenlernen, die auch erfüllt sind, obwohl sie nicht ausgeführt werden.