As one who is tired of all the xml in our NAnt based build process I've been pleased to see all the options out there sans xml. One that caught my attention is Rake. I've been interested in learning Ruby so I figured I could learn an xml-less build tool and at the same time get my feet wet with the Ruby language. With Albacore, a set of .NET focused Rake tasks (by Derick Bailey), Rake is even more of an attractive option for .NET developers. Another plus is that Rake can be run by both Ruby and IronRuby. We need to interact with some custom .NET code in our build process, so IronRuby allows us to easily leverage that code natively from the build scripts.

Ruby Quickstart

I wanted to immediately dive into creating Rake scripts but I could only seem to find very basic Ruby tutorials (Explaining what loops and conditionals are) or entire books. While I'd love to really dig into Ruby, I just don't have the time at the moment. So what I really wanted was a terse overview of Ruby and the syntax to quickly get me started writing Rake scripts. Hopefully the following quickstart will accomplish this. I'm by no means a Ruby expert, just a novice, so I'm sure that there is a lot I'm missing, but hopefully this will at least get you going.

Install

You will need to install either Ruby or IronRuby. After running the installers you should be good to go. I will be using IronRuby below.

http://www.ironruby.net

http://rubyforge.org

Hello World

Well start with the obligatory hello world script and sending some output to the console with the puts (Put string) method.

HelloWorld.rb:

# My first Ruby script 
puts "Hello World!"

Now save this and run this using the Ruby interpreter with the script path specified:

image

Including Files

You can also include other files by using the "require" keyword. You specify the filename without the extension.

HelloWorld.rb:

require "Common" 
puts "Hello World!"

Common.rb:

puts "This is from another file."

Now run HelloWorld.rb again:

image

If you want to include a file that is in a subfolder you can specify that with a relative path. You will need to use the forward slash as the folder delimiter, not the backslash as the backslash is the escape character:

HelloWorld.rb:

require "Libraries/Common" 
puts "Hello World!" 

In the above example the Common.rb file is in a subdirectory called "Libraries". Also, note that this is assuming that the current directory is the directory where HelloWorld.rb resides.

Working with .NET Assemblies

Working with .NET assemblies is simple with IronRuby. The "include" keyword also allows you to reference .NET namespaces of assemblies that are loaded (Just like the "using" directive in C#). Namespaces are separated by a double colon unlike C# which uses the period:

include System 
include System::IO 
puts "Hello World! It's #{DateTime.Now.ToString}." 

You can reference .NET assemblies in the same way you would another Ruby file, with the "require" keyword. Below I have copied the Zip Code Coords assembly (ZipCodes.dll) into the same directory as the Ruby script.

require "ZipCodes" 
include ZipCodes 
coords = Spatial.Search("81504") 
puts "Coords: #{coords.Latitude.ToString}, #{coords.Longitude.toString}" 

The require keyword can accept just an assembly name or a fully qualified assembly name if required. The assembly extension is omitted.

Operators

The operators are defined below. this information was taken from here.

Operator Description
[ ] [ ]= Element reference, element set
** Exponentiation
! ~ + - Not, complement, unary plus and minus (method names for the last two are +@ and -@)
* / % Multiply, divide, and modulo
+ - Plus and minus
>> << Right and left shift
& Bitwise 'and'
^ | Bitwise exclusive 'or' and regular 'or'
<= < > >= Comparison operators
<=> == === != =~ !~ Equality and pattern match operators (!= and !~ may not be defined as methods)
&& Logical 'and'
|| Logical 'or'
.. ... Range (inclusive and exclusive)
? : Ternary if-then-else
= %= { /= -= += |= &= >>= <<= *= &&= ||= **= Assignment
defined? Check if symbol defined
not Logical negation
or and Logical composition
if unless while until Expression modifiers
begin/end Block expression

Syntax Overview

The following is a quick overview of the Ruby syntax. BTW, I realize that you can use Linq in a lot of these C# examples, but that's not the point of the examples. Spontaneous Derivation also has a nice overview here. The Ruby Programming wikibook has nice coverage as well.

Name Ruby C# Notes
Comments
# This is a comment
// This is a comment
Variables
name = "Paul Dirac"
var name = "Paul Dirac"; 
or
string name = "Paul Dirac";
Variable names must start with a lowercase letter.
Constants
ElementaryCharge = 1.602
const double ElementaryCharge = 1.602;
Constant names must start with a capital letter.
Line continuation
story = "Once upon " \  
        "a time..."
The backslash character. Concatenating strings doesn't require the plus symbol with line continuations.
String formatting
puts "It's #{DateTime.Now.ToString}." 
Console.WriteLine(string.Format("It's {0}.",
DateTime.Now));
Code surrounded by #{...}.
Some string functions
.capitalize    
.center(integer,padstr)      # Centers test with padding 
.chomp                       # Removes carriage returns 
.chop                        # Removes the last character 
.count([other_str]+)         # Number of times a search string(s) exists 
.delete([other_str]+)        # Deleted the search strings 
.downcase                    # Lower cases the string 
.empty?                      # Determines if the string is empty 
.gsub(pattern,replacement)   # Replaces the regex pattern in the string 
.include? other_str          # Contains the search string 
.index(substring[,offset])   # Returns the index of the search string 
.length 
.ljust(integer,padstr='')    # Left justify 
.lstrip                      # Removes whitespace to the left 
.reverse 
.rindex(substring[,offset])  # Returns the index of the search string from the right 
.rjust(integer,padstr='')    # Right justify 
.rstrip                      # Removes whitespace to the right 
.split(pattern=' ', [limit]) # Splits a string 
.string                      # Removes whitespace to the left and right 
.swapcase                    # Inverts the casing 
.upcase                      # Upper cases the string
Conversion functions
.to_s 
.to_i 
.to_f
.ToString() 
.Parse("44") 
.Parse("44.5")
If/then/else
if length < 100 
    puts "Short"  
elsif length >= 100 && \ 
      length < 500 
    puts "Medium" 
else 
    puts "Long" 

end
var length = 100; 
if (length < 100) { 
    Console.WriteLine("Short"); 
} else if (length >= 100 && 
           length < 500) { 
    Console.WriteLine("Medium"); 
} else { 
    Console.WriteLine("Long"); 
}
Array declaration
names = ["Pablo Honey", \ 
         "OK Computer", \ 
         "Kid A", \ 
         "In Rainbows", \ 
         "The Bends"] 

or

names = Array.new 
names << "Pablo Honey" 
names << "OK Computer" 
...
var names = new string[] {"Pablo Honey", 
                          "OK Computer", 
                          "Kid A", 
                          "In Rainbows", 
                          "The Bends"};
Arrays are zero based. Arrays can contain mixed type elements.
Hash declaration
settings = { "length" => 25, \ 
             "color" => "red" }
Dictionary<string, object> settings = 
    new Dictionary<string, object>() 
    { { "length", 25 }, 
      { "color", "red" } }; 
Setting array elements
names[5] = "COM Lag" 
names[5] = "COM Lag"; 
Adding array elements
names << "COM Lag"
Function
def printHello 
    puts "Hello" 
end 

or

def printMessage(message, maxLen) 
    puts message[0..maxLen - 1] 
end 

or

def ToUpper(text)
    return text.upcase 
end

or

def printArgs(*args)
    puts args.join(", ")  
end
void PrintHello() { 
    Console.WriteLine("Hello"); 
}

or

void PrintMessage(string message, int maxLen) { 
    Console.WriteLine(message.Substring(0, maxLen)); 
}

or

string ToUpper(string text) { 
    return text.ToUpper(); 
}

or

void PrintArgs(object[] args) { 
    Console.WriteLine(string.Join(", ", args));  
}
Functions must begin with a lower case letter.
Do loop
10.times do 
    puts "hello" 
end
for (int index = 0; index < 10; index++ ) { 
    Console.WriteLine("hello"); 
}
While loop
while index < 5 
    puts index.to_s 
    index += 1 
end
int index = 0; 
do { 
    Console.WriteLine(index); 
    index += 1; 
} while (index < 5);
For loop
10.times do |index| 
    puts index.to_s 
end
for (int index = 0; index < 10; index++ ) { 
    Console.WriteLine(index); 
}
For each array
names = [...] 
names.each do |name| 
    puts name 
end        
var names = new string[] {...}; 
foreach (string name in names) { 
    Console.WriteLine(name); 
}
For each hash
settings = {...}
settings.each do |key, value| 
    puts key + ": " + value.to_s 
end
Dictionary<string, object> settings = 
    new Dictionary<string, object>() {...}; 
foreach (var setting in settings) { 
    Console.WriteLine(setting.Key + ": " +  
                      setting.Value); 
}
Iterators
def getHours 
    time = Time.new 
  
    time.hour.times do |hour| 
        yield(hour) 
    end 
end
IEnumerable<int> GetHours() { 
    for (int hour = 0;  
         hour < DateTime.Now.Hour;  
         hour++) yield return hour; 
}
Class
class Employee 
    def initialize(name)  
        @name = name 
    end 
end
class Employee { 
    string name; 
    public Employee(string name) { 
        this.name = name; 
    } 
}
Class names must be capitalized.
Accessors
class Employee 
     attr_reader :name 
     attr_accessor :firstName, :lastName 
     def PrintName 
         puts @name 
     end 
end
class Employee { 
    public void PrintName() { 
        Console.WriteLine(this.Name); 
    } 
    public string Name{get; private set;} 
    public string FirstName{get;set;}  
    public string LastName{get;set;} 
}
The @ symbol represents a class scoped variable.
Modules
module Domain 
    module HR 
        class Employee 
        end 
    end 
end
employee = Domain::HR::Employee.new
namespace Domain { 
    namespace HR { 
        class Employee { 
        } 
    } 
}
Domain.HR.Employee employee = 
            new Domain.HR.Employee();
The double colon is used as the module separator.
Symbols
:yada
Colon followed by a token. More on Ruby symbols here.
Closures
closure = lambda { puts "Hi, I'm a closure!" }
closure.call
closure = lambda { |name| puts "Hi, #{name}" \
" I'm a closure!" } closure.call "Johnny"
closure = lambda do puts "Hi I'm a closure!" end closure.call
closure = lambda do |name| puts "Hi, #{name} I'm a closure!" end closure.call "Johnny"
def callLambda(&closure) closure.call "Timmy" end
callLambda do |name| puts "Hi, #{name} I'm a closure!" end
Action closure = () => 
       Console.WriteLine("Hi, I'm a closure!");
closure();
Action<string> closure2 = (name) => 
Console.WriteLine("Hi, " + name + " I'm a closure!"); closure2("Johnny");
closure = () => { Console.WriteLine("Hi, I'm a closure!"); }; closure(); closure2 = (name) => { Console.WriteLine("Hi, " + name + " I'm a closure!"); }; closure2("Johnny");
void CallLambda(Action<string> closure) { closure("Timmy"); }
CallLambda((name) => { Console.WriteLine("Hi, " + name + " I'm a closure!"); });

This is an extremely simple introduction but hopefully this will give you enough information to dive right into writing Ruby for Rake scripts.

Rake Quickstart

Derick Bailey has written a great article for Code Magazine about setting up and using Rake & Albacore. The rest of this post will overlap somewhat with the information that Derick presents in his fine article (In fact that's where I got most of the following info from). He is the author of Albacore so he's a bit more qualified to talk about Rake & Albacore than myself. :) So I'd suggest reading that as well. Also Martin Fowler has a nice in-depth write up about Rake where I've gotten some information on the mechanics of the Rake DSL.

Installing Rake

Installing Rake is a matter of running (i)gem install rake:

image

Rake Hello World

Save the following script to a file:

desc "Rake hello world task"
task :default do
    puts "Hello world"
end

and run it using "rake –f [path]":

image

Understanding the Rake DSL

Below is a Rake task that depends on another task (test depends on compile):

task :compile do
    puts "Compiling..."
end
task :test => :compile do puts "Testing..." end

"task" is no more than a function being called with 2 parameters passed.

image

The first parameter you pass into the "task" function can be a few things. If there are no prerequisite tasks then only the task name is passed, which is a lone symbol (IE: task :build do...end). The task names are Ruby symbols. A Ruby symbol is a colon followed by a token; IE ":compile", ":test", etc. Now if there is one prerequisite task then a 1 element hash (Or dictionary in .NET) with the task as the key and the prerequisite as the value is passed in (IE: task :build => :test do...end). The "key => value" syntax is shorthand for defining a hash with one key/value pair. If multiple prerequisites are passed then its the same as the last scenario except the value is now an array of prerequisite tasks instead of just one (IE: task :build => [:test, :compile] do...end). The "[value1, value2, ...]" syntax is shorthand for creating an array.

The second parameter is a closure. It is a code block is surrounded by the "do" and "end" keywords. This closure is stored for later execution by Rake.

So the signature of this "task" function in Ruby could be as follows:

def task(nameAndDepends, &closure)

Or in C#, perhaps the following signatures:

void Task(Symbol name, Action closure) { }
void Task(Dictionary<Symbol, Symbol> nameAndDepends, Action closure) { }
void Task(Dictionary<Symbol, List<Symbol>> nameAndDepends, Action closure) { }

Lets take a look at a contrived example without Rake to further demonstrate:

def task(nameAndDepends, &closure)
    # Print the task name and dependencies
    if nameAndDepends.kind_of? Symbol then
        name = nameAndDepends.to_s
        depends = "nothing"
    else
        name = nameAndDepends.keys[0].to_s
        if nameAndDepends.values[0].kind_of? Symbol then
            depends = nameAndDepends.values[0].to_s
        else
            depends = nameAndDepends.values[0].join(", ")
        end
    end
	
    puts "The task name is '#{name}" \
         "' and it depends on " \
         "#{depends}."
	
    # Execute the closure
    closure.call
	
    puts
end

task :compile do
    puts "Compiling..."
end

task :test => :compile do
    puts "Testing..."
end

task :build => [:compile, :test] do
    puts "Building..."
end

Running this script produces the following output:

image

So the syntax of the Rake DSL may look a bit esoteric, especially to a .NET developer, but its actually pretty simple and much terser than C#.

Built-in Rake Tasks

Rake ships with a few primitive tasks; we will now look at a few.

Here we see the directory, file and file list tasks in action:

task :default => :deploy
	
# Create the directory "docs"
directory "docs"

# Creates the Readme.txt file by combining 
# the intro.txt and license.txt files.
file "docs\\Readme.txt" => ["Intro.txt", "License.txt"] do |f|
    open(f.name, "w") do |outs|
        f.prerequisites.each do |filename|
            contents = open(filename) do |ins| ins.collect \
                                      { |line| line.chomp } end
            contents.each do |line|
                outs.puts line
            end
        end
    end
end

# Creates the Files.lst file
file "docs\\Files.lst" => FileList['src/**/*.*'] do |f|
    open(f.name, "w") do |outs|
        f.prerequisites.each do |filename|
            outs.puts filename
        end
    end
end

desc "Deploys the application."
task :deploy => ["docs", "docs\\Readme.txt", "docs\\Files.lst"] do
    # Now we can deploy...
end

"directory" is just a task (Don't let the terse syntax fool you) as are the "file" tasks. A deploy depends on the existence of a "docs" folder and the "readme.txt" and "files.lst" files being in that folder. So those are specified as prerequisites of the deploy task (Where it says => ["docs", "docs\\Readme.txt", "docs\\Files.lst"]).

If your an OSS developer you may want to publish a Gem to distribute your library or application. Rake has built in functionality to generate a Gem. The Rake documentation has more info about this here. Below you can see how easy it is to generate the Gem:

require 'rubygems'
require 'rake/gempackagetask'

task :default => [:package]

spec = Gem::Specification.new do |spec|
    spec.platform = Gem::Platform::RUBY
    spec.summary = "xUnit.net - A Unit Testing Framework, Duh"
    spec.name = "xunitnet"
    spec.version = "1.6.1"
    spec.requirements << "none"
    spec.files = Dir["lib/**/*"]
    spec.authors = ["Brad Wilson","James Newkirk"]
    spec.homepage = "http://xunit.codeplex.com"
    spec.description = <<EOF
xUnit.net is a developer testing framework, built to support 
Test Driven Development, with a design goal of extreme simplicity 
and alignment with framework features.
EOF
end

Rake::GemPackageTask.new(spec) do |package|
end

This will produce a Gem file that can be published to rubygems.org. Rob Reynolds digs into the gory details here (Actually it's amazing simple). The Nu project, which brings Gems to .NET, makes this very compelling for .NET developers as well, definitely worth checking out.

Other primitive tasks include CLOBBER, CLEAN, RDoc, and Test. Spontaneous Derivation has a nice overview of these here.

User Defined Rake Tasks

Now we will look at defining our own Rake tasks. As an exercise, lets create a task to deploy files. We'll use the ever so popular robocopy (Formally known as xcopy):

task :default => :deploy

desc "Deploys the application."
task :deploy do

    # Options
    source = "D:/Development/ProjectEulerNet/src/ProjectEuler.UI"
    target = "D:/Websites/ProjecEulerNet/wwwroot"
    excludedDirectories = "obj"
    includeFiles = "*.dll *.pdb *.config *.asax *.ascx *.ashx " \
                   "*.aspx *.master *.htm *.html *.txt *.css " \
                   "*.gif *.jpg *.jpeg *.png *.xml *.js"
    logPath = "Robocopy.log"
	
    # Command
    robocopy = "robocopy " \
               "\"#{source}\" " \
               "\"#{target}\" " \
               "/MIR " \
               "/XD #{excludedDirectories} " \
               "/IF #{includeFiles} " \
               "/LOG+:\"#{logPath}\" " \
               "/TEE"

    # Execute
    sh robocopy do |ok,res|
                     raise "Robocopy failed with exit " \
                           "code #{res.exitstatus}." \
                     if res.exitstatus > 8
                end
end

A few things to note above. First the sh function. This function allows you to shell out and execute another program. Second, the sh command also takes an optional closure where you can explicitly handle the results of the command. Robocopy will return exit codes higher than zero for success, so the default Rake error handling will not work in this case. To compensate for this we add a closure that checks the exit code and only throws an exception if the status greater than eight (As 0-8 are success codes for robocopy). If the program returned zero for success and non zero for failure we could omit the custom error handling.

Now robocopy is really something that we could easily reuse in other places. So we could create a custom reusable task instead of the proprietary one above. Derick Bailey has a nice post which covers that in detail. Below we refactor a bit to create a reusable robocopy task.

robocopy.rb:

class Robocopy
    attr_accessor :source, :target, :excludeDirs, :includeFiles, :logPath
    
    def run()
        robocopy = "robocopy " \
                   "\"#{@source}\" " \
                   "\"#{@target}\" " \
                   "/MIR " \
                   "/XD #{@excludeDirs} " \
                   "/IF #{@includeFiles} " \
                   "/LOG+:\"#{@logPath}\" " \
                   "/TEE"
            
        errorHandler = \
            lambda do |ok, res|
                       raise "Robocopy failed with exit " \
                             "code #{res.exitstatus}." \
                       if res.exitstatus > 8
                   end

        sh robocopy, &errorHandler 
    end
end

def robocopy(*args, &block)
    body = lambda { |*args|
        rc = Robocopy.new
        block.call(rc)
        rc.run
    }
    Rake::Task.define_task(*args, &body)
end

rakefile.rb

require "robocopy"

task :default => :deploy

robocopy :deploy do |rc|
    rc.source = "D:/Development/ProjectEuler.Net/src/ProjectEuler.UI"
    rc.target = "D:/Websites/ProjecEulerNet/wwwroot"
    rc.excludeDirs = "obj"
    rc.includeFiles = "*.dll *.pdb *.config *.asax *.ascx *.ashx " \
                      "*.aspx *.master *.htm *.html *.txt *.css " \
                      "*.gif *.jpg *.jpeg *.png *.xml *.js"
    rc.logPath = "Robocopy.log"
end

Much better! Now we can reuse the robocopy task ad nauseum. As an aside, when we pass the args variable to the define_task function, we prefix it with the asterisk. This is an array and if we omit the asterisk it will passed into the function as an array parameter. Prefixing it with the asterisk tells Ruby to actually break the array elements out into separate function parameters. Neat, eh? Now when a function parameter is prefixed with an asterisk (In the function signature), that just means it can accept any number of parameters that will be passed into the function as an array (The same as the params keyword in C#).

In the examples above we statically specified some information. Rake scripts can obtain information from environment variables; a common way build servers will pass information to scripts. The following demonstrates getting information from the ThoughtWorks GO release management server:

buildServerUrl = ENV["CRUISE_SERVER_URL"] 
buildPipelineName = ENV["CRUISE_PIPELINE_NAME"]
buildPipelineLabel = ENV["CRUISE_PIPELINE_LABEL"]
buildStagename = ENV["CRUISE_STAGE_NAME"]
buildStageCounter = ENV["CRUISE_STAGE_COUNTER"]
buldJobName = ENV["CRUISE_JOB_NAME"]

Albacore Quickstart

As you can see, the number of primitive tasks that ship with Rake are very limited. Derick Bailey in his Albacore project has created a set Rake tasks that are geared towards .NET builds. These make it much easier to author Rake scripts by giving you precanned tasks (Much like NAnt and NAntContrib). At the time of writing the following tasks are available:

AssemblyInfoTask
ExecTask
ExpandTemplatesTask
MSBuildTask
MSpecTask
NantTask
NCoverConsoleTask
NCoverReportTask
NDependTask
NUnitTask
RenameTask
SftpTask
SQLCmdTask
SshTask
UnZipTask
XUnitTask
ZipTask

To use the Albacore tasks you will need to install the Gem as follows:

image

In the example below we will us the MSBuild and assembly info tasks to build our application. Note, you will have to add the require statement at the top of the file for Albacore.

require "albacore"
task :default => :build

desc "Generate assembly info."
assemblyinfo :assemblyinfo do |asm|
  asm.version = ENV["CRUISE_PIPELINE_LABEL"]
  asm.company_name = "Ultraviolet Catastrophe"
  asm.product_name = "ProjectEuler.NET"
  asm.title = "ProjectEuler.NET"
  asm.description = "A .NET implementation of the Project Euler site."
  asm.copyright = "Copyright (c) 2010 Ultraviolet Catastrophe"
  asm.output_file = "src/Properties/AssemblyInfo.cs"
end

desc "Builds the application."
msbuild :build => :assemblyinfo do |msb|
  msb.path_to_command = File.join(ENV["windir"], "Microsoft.NET", "Framework", "v4.0.30319", "MSBuild.exe")
  msb.properties :configuration => :Debug
  msb.targets :Clean, :Build
  msb.solution = "src/ProjectEuler.UI.csproj"
end

Doesn't get any simpler than that. One thing to note is that the "path_to_command" setting is temporary. .NET 4.0 support should be added soon to Albacore, as well as the ability to specify the framework version you want to use. Now compare this to some similar NAnt markup:

<asminfo output="${Project.AssemblyInfoFilePath}" >
  <attributes>
    <attribute type="ComVisibleAttribute" value="false" />
    <attribute type="AssemblyTitleAttribute" value="${Build.Component}" />
    <attribute type="AssemblyDescriptionAttribute" value="${Build.Component}" />
    <attribute type="AssemblyCompanyAttribute" value="Yada, Inc" />
    <attribute type="AssemblyProductAttribute" value="${Build.Component}" />
    <attribute type="AssemblyCopyrightAttribute" value="Copyright (c) 2010 Yada, Inc." />
    <attribute type="AssemblyVersionAttribute" value="${Build.Version}" />
    <attribute type="AssemblyFileVersionAttribute" value="${Build.Version}" />
  </attributes>
</asminfo>

<exec program="${App.MSBuild}" >
  <arg line="&quot;${Project.Folder}\${Project.ProjectFile}&quot;" />
  <arg line="/property:Configuration=Release" />
  <arg value="/target:Rebuild" />
  <arg value="/verbosity:normal" />
  <arg value="/nologo" />
</exec>

I don't know about you but the former is much cleaner and much more understandable. Plus with the former you get the power of Ruby right at your fingertips.

Conclusion

Hopefully this post gets you up and running quickly with Ruby/Rake/Albacore. Feedback is definitely welcome.