by Luis López de Quintana, Front End Architect
We at Cinchcast consider ourselves an āalt.NETā shop. This means we use C# and the .NET framework on Windows to do most of our heavy lifting, but we also rely on non-MS technologies extensively, because they are the right tool for the job. While we use .NET MVC on IIS for our web applications, we use REDIS for an ACID-compliant high-QPS datastore, and Rake for building our .NET projects.
When writing our Rake build system, we found that there was no āone true HOWTOā on the topic of writing Rakefiles, especially from the perspective of a complete Ruby n00b. Some articles covered some parts of the syntax, others other parts, and to make it worse, the Rake syntax changed over the iterations of Rake, and many high-PageRankād Rake tutorials are hopelessly out of date. Here we aim to provide the most conclusive introduction to Rakefile syntax for Rake/Ruby neophytes.
This HOWTO is current as of Ruby 1.9 and Rake 0.9.2.
Rake is the Ruby build system, meant to replace UNIX make. Buildmasters write Rakefiles, which define how the application is built. These Rakefiles are full-fledged Ruby scripts which can be run through a Ruby interpreter with no issues. This attribute of Rakefiles is the core reason why Rake is more attractive than the historical build systems which use a bespoke, declarative DSL such as make, ant, or MSBuild. Conditionals, loops and encapsulation are all much easier to implement and debug when using a first-class imperative language like Ruby for the build language.
Rake became popular for building .NET apps when Derick Bailey published his Rake tasks for .NET: the Albacore library. The flexibility of using Ruby in Rakefiles and the solid out-of-the-box functionality provided by Albacore makes for a very compelling build system for .NET projects.
We at Cinchcast use the excellent MinGW-based RubyInstaller Ruby distribution for Windows. RubyInstaller comes with a version of Rake installed, but itās not the most recent major version, so upgrade to the latest Rake fire by running:
$ gem install rake --version '0.9.2'
Linux users probably already have Ruby installed. Try ruby --version to see if youāre running Ruby 1.9. If not, use your package manager like apt-get install ruby to get the latest version.
Mac users can use the RubyOSX distribution of ruby and run the command above for installing the latest version of Rake.
Rakefiles contain Rake Tasks, which define units of work. One task can invoke MSBuild.exe on a .csproj, another can recursively traverse a directory tree and minify Javascript files. Tasks are arbitrary Ruby code, so the skyās the limit.
Each task can be individually executed on the command like like so:
rake mytask othertask task3
That will execute the tasks named āmytaskā, āothertaskā, ātask3ā which are defined in .\Rakefile.rb (an alternate name may be used and supplied to rake), in order from left to right.
Hereās a sample task definition found in a Rakefile:
desc "Minifies all JS files in the project. loren ipsum dolor sit amet loren\n" + "loren ipsum dolor sit amet." closurecompile :minify_js_working do |cc| cc.include_pattern = '.+\.js$' end
The first line defines a description for the task which enables Rake to display help about your task on the rake command like rake --describe minify_js_working. This is convenient for someone trying to run your Rakefile so they donāt have to open it up in an editor to get descriptions of your tasks. Note the line breaks, task description lines should be optimized for console display, so a line break every 73 characters is a good rule for descriptions.
āclosurecompileā is the type of task you are defining. In this case, closurecompile is a task type I wrote which is imported into the Rakefile using the ruby ārequireā statement.
ā:minify_js_workingā is the name of the task. The colon means that it is a Ruby symbol, all rake tasks are named using Ruby symbols. This task would be executed by running ārake minify_js_workingā on the command line.
ādo |cc| ⦠endā do .. end defines a Ruby code block. This code block is executed when the task is executed. You can put any ruby code in this block, which is the power of Rake. But the closurecompile task does all the heavy lifting for us. All we have to do is configure the closurecompile task object.
ā|cc|ā defines one parameter variable āccā to our block. This reference to the closurecompile task object allows us to configure it in the next statement. The first parameter to any Rake task block is the instance of the task object itself.
ācc.include_pattern = ā.+.js$ā is where we configure the closurecompile task object.
By now you might have noticed that the word ātaskā is overloaded. Unfortunately, this is a feature of rake lingo. When referring to a ātaskā, you could refer to:
A thing that has a name and is executed via the command line, such as the whole code sample above (Iāll call these tasks)
Type of task that does something so you donāt have to, such as ClosureCompile used via āclosurecompileā above (Iāll call these task types)
An instance of a task type that is passed into the task block, such as cc above (Iāll call these task objects)
Now that weāve got that out of the way, Iāll explain exactly what is happening from a Ruby perspective when you define a task. Hereās the example again:
desc "Minifies all JS files in the project." closurecompile :minify_js_working do |cc| cc.include_pattern = '.+\.js$' end
desc "Minifies all JS files in the project."
Passing a string to a method called desc. This desc method registers the description string with the Rake runtime for display when doing rake --describe taskname.
closurecompile :minify_js_working do...end
closurecompile is acually a method. Youāre passing the name of the task and the block to execute when the task is run to it. The closurecompile method is defined in a library. It registers with the Rake runtime two things: that a task called minify_js_working exists, and that when this task is executed, Rake shall instantiate a ClosureCompile Task object, pass it to the block given so the Rakefile author can configure it, and then execute the ClosureCompile fun.
task :print_hello do print 'hello ' end task :print_goodbye do print 'goodbye ' end task :dosomething => [:print_hello, :print_goodbye] do print 'doing something' end
Note that you can define a task using the basic Task task type that does nothing extra for you using ātaskā like above. Since Task does nothing on its own, thereās no reason to configure the task object, so thereās no block |parameter| after the do.
Executing dosomething executes print_hello, then print_goodbye, then dosomething itself:
$ rake dosomething hello goodbye doing something
Rake has no built-in mechanism for showing dependencies of tasks with --describe, so we follow this convention:
desc "A useful description for foo.\n" + "Dependencies: dep1, dep2" task :foo => [:dep1, :dep2] do end
Dependency Syntax details
:dosomething => [:print_hello, :print_goodbye]
This snippet defines a one-element Ruby hash, with the key being the symbol :dosomething, and the value being an array of two symbols: :print_hello, and :print_goodbye.
So we are passing a Ruby hash instead of a name symbol to the ātaskā method. This works because the dynamic nature of Ruby allows the ātaskā method (and the methods for any other task type) to inspect the type of the first parameter, and behave differently based on the type:
def task(name_sym) # symbol param means no dependencies def task({name_sym => [dep_sym,...]}) # hash param means dependencies
Note that a task with only one dependency doesnāt have to use an array:
task :dosomething => :print_hello do end
task :print_stuff, [:stuff1, :stuff2, :stuff3] do |t, args| print args[:stuff1] + "\n" print args[:stuff2] + "\n" print args[:stuff3] end
Hereās a sample run of print_stuff:
$ rake print_stuff[1,2,3] 1 2 3
So print_stuff that has three parameters: stuff1, stuff2 and stuff3. The task code accesses the parameters via the block parameter args, which is a hash containing the values passed in via the command line using the taskname[params] syntax.
The values are always strings.
Thereās no way to pass parameters by name on the command line. Set up default values and check for empty string like so:
task :print_stuff, [:stuff1, :stuff2, :stuff3] do |t, args| stuff1 = args[:stuff1] == "" ? "default1" : args[:stuff1] stuff2 = args[:stuff2] == "" ? "default2" : args[:stuff2] stuff3 = args[:stuff3] == "" ? "default3" : args[:stuff3] print stuff1 + "\n" print stuff2 + "\n" print stuff3 end
Then you can safely omit parameter values on the command line:
$ rake print_stuff[,,3] default1 default2 3
You can define a task with both dependencies and parameters:
task :foo, [:arg1, :arg2] => [:task1, :task2] do |t, args| end
Rake doesnāt provide a built-in way to provide parameter documentation for --describe, so we follow this convention:
desc "foo task description. This is a useful summary.\n" + "Param 'p1': Useful p1 description. loren ipsum dolor sit amet loren ipsum\n" + " More p1 useful information.\n" + "Param 'p2': Useful p2 description. loren ipsum dolor sit amet loren ipsum\n" + " More p2 useful information." task :foo do end
Note the two-space hanging indent for long descriptions.
task :foo, [:arg1, :arg2] do |t, args| end
We pass two arguments to the ātaskā method (besides the block): the name symbol and an array containing the name symbols of our arguments.
For a task with params and dependencies:
task :foo, [:arg1, :arg2] => [:task1, :task2] do |t, args| end
We still pass two arguments, but the second argument isnāt the array of dependency task names, itās a hash with a key of an array of argument name symbols, and the value is the array of dependency task names thatās passed in alone for a no-dependency task.
We can share the same ātaskā method with parameterless and parameterized task definitions because ātaskā is overloaded:
def task(name_sym_or_dep_hash) # Defines parameterless tasks def task(name_sym, args_definition) # Defines parameterized tasks
We can use the same overload of ātaskā for parameterized tasks with and without dependencies because the ātaskā method uses parameter type detection like so:
def task(name_sym, [arg_sym,...]) # 2 param args array def task(name_sym, {[arg_sym,...] => [dep_sym,...]}) # 2 param args hash
The first overload handles the dependency-less parameterized case, the second handles parameterized tasks with dependencies.
So thatās it for our Rake primer. Hope it helps!