Ruby/Rake/Albacore Quickstart Guide for .NET Developers July, 2010
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.
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:
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:
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}.", |
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" |
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(", ") |
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 |
Action closure = () => Console.WriteLine("Hi, I'm a closure!"); closure(); Action<string> closure2 = (name) => |
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:
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]":
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.
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:
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:
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=""${Project.Folder}\${Project.ProjectFile}"" /> <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.