Los Techies : Blogs about software and anything tech!

How A .NET Developer Hacked Out A Rake Task


I spent the last two days having my brain melted by Scott Bellware, in a crash course on Ruby, Rake, RSpec, Selenium, web UI test automation best practices, and all-around uber-normal-person-language-oriented development. It was great! After the first 7 hour day, I felt like I had been awake for 24 hours. After the second 8 hour day, I felt like I actually knew something about Ruby and good web UI test automation practices.

 

Will The Real Rake Task Please Stand Up

In the conversations and snappy banter that ensued during and after the sessions, Scott and I talked a little about Rake and building what he referred to as ‘real’ Rake tasks instead of the ‘not-Rakeish’ tasks that most people build. By ‘not-Rakeish’, Scott was talking about our propensity as .NET developers to make everything a class and make heavy use of the “system.Object of Rake: the ‘task’”. This point is illustrated by a google search of ‘build custom rake task’. Practically every result that comes up will show you how to use “task :somename do … end” and call “rake :somename”.

Whether or not you think using standard ruby objects in a standard rake ‘task’ is ‘Rakeish’ or not, isn’t really the point. Of course that works. Yes, many people do it that way, because it works. The point Scott was making is that professional tools like RSpec and Selenium don’t provide tasks in that way.  We don’t see these simple constructs being used. Rather, we see professional tasks being built so that we can call something more like this:

   1: Spec::Rake::SpecTask.new do |t|
   2:   t.ruby_opts = ['-rtest/unit']
   3:   t.spec_files = FileList['test/**/*_test.rb']
   4: end

That conversation with Scott and some other conversations around the idea of building .NET solutions with Rake got me thinking about how we could build a much more ‘professional’, ‘real’ Rake task to call out to MSBuild. While I have not yet put this solution together, I have spent some time learning how to build tasks such as the RSpec ‘SpecTask’ shown above. Not surprisingly, it’s a very simple endeavor. I honestly don’t claim to understand exactly why everything in my hacked-together custom task is set up the way it is, but I have a pretty good idea of what’s going on, at this point. So, I wanted to share my newfound knowledge and hopefully inspire some of the .NET Rake building people out there to create something a little more ‘Rakeish’, rather than just using the default ‘task :whatever’ syntax and calling into an ‘MSBuild’ object.

 

Defining A Custom Rake Task Using TaskLib

Since I was unable to find any good info via my google search, I started examining the RSpec rake task and the Selenium rake tasks that were installed via the gems. The first thing I found is that I needed to have the ‘rake’ and ‘rake/tasklib’ files, so that I could create a class that inherits from ‘TaskLib’. For example, I can create a task called “FooTask” like this:

   1: require 'rake'
   2: require 'rake/tasklib'
   3:  
   4: class FooTask < Rake::TaskLib
   5: end

This basic definition gives me a task that I can use via Rake. However, it doesn’t really do anything yet and doesn’t even have a name that we can call from rake.

 

Giving The Task A Name

To set up a good task name to use, we need to include a ‘:name’ accessor (think auto property in c#) and parameter in the initializer. This name will be used later, to tell the TaskLib base class what the name of the task is, at runtime.

   1: class FooTask < Rake::TaskLib
   2:   attr_accessor :name
   3:   def initialize(name = :some_default_name)
   4:     @name = name
   5:   end
   6: end

Notice the ‘:some_default_name"’ – this does exactly what it sounds like… sets the default name of the task. In this case, we are literally giving the name a default of ‘some_default_name’. We are then setting the @name (which has been automatically generated by the ‘attr_accessor :name’ usage… again, this is like an auto property in C#).

 

Letting The Task Do Custom Work

If we want to provide the ability for the user of the task to actually do some work, we need to include the lambda block evaluation that ruby has built in. This is done by yielding self if a lambda block is present, in the constructor.

   1: def initialize(name = :test)
   2:   @name = name
   3:   yield self if block_given?
   4: end

this block of code let’s us call our custom task with a ‘do … end lambda’ block

   1: FooTask.new do |t|
   2:  #do stuff with t, here
   3: end

The ‘|t|’ in this code refers to the actual FooTask instance that we yielded (‘self’), and will let us have access to all of the methods in the FooTask object, once we add some.

 

Defining The Default Work To Do

In all of the custom rake tasks that I have examined so far, I have found a method called ‘define’ that appears to be a standard convention for rake tasks as the location of code that gets executed by the custom task that we are creating. This method calls the TaskLib method ‘task’ with a name parameter (the name we supplied in the initializer) and then does the work. For example, RSpec does all of it’s spec parsing and running from here, and Selenium does all of it’s server initializing or shutdown here. For this example, we’re just going to print some junk out to the screen.

   1: def define
   2:   task name do
   3:     puts 'some test being printed'
   4:   end
   5: end

The ‘task name’ code is important here, as it provide rake with the knowledge of the task’s name, so that we can call it from the rake command line.

 

Running What We Have

At this point, we have enough code in place to run the sample rake task. Create a ‘rakefile’ with all of the above code, then add code similar to how we called the ‘SpecTask’ earlier. The contents of the rakefile should be:

   1: require 'rake'
   2: require 'rake/tasklib'
   3:  
   4: class FooTask < Rake::TaskLib
   5:  
   6:   attr_accessor :name
   7:  
   8:   def initialize(name = :test)
   9:     @name = name
  10:     yield self if block_given?
  11:     define
  12:   end
  13:  
  14:   def define
  15:     task name do
  16:       puts 'some test being printed'
  17:     end
  18:   end
  19: end
  20:  
  21: FooTask.new :my_task do
  22: end

Notice that we are giving the task a new name when we call ‘FooTask.new :my_task’. The name is being set to ‘my_task’. We can now run the task from rake via a shell prompt (command prompt in windows):

image

So we now have a custom rake task that is much more ‘Rakeish’. However, it’s still not terribly useful. There is no way to get data into the task and there is no way to call any dependencies before this task.

 

Adding Data To The Task

To add data, methods, or do something useful with the task, we can use all the usual ruby ways – methods with parameters, with ‘=’ assignment, attr_accessors, etc etc. For simplicity, let’s use an attr_accessor to create a ‘property’ (I know, it’s not really a property in Ruby, but it sure behaves like one). We’ll add an accessor call ‘:data’ and we’ll have the define method write that data out instead of our hard coded string.

   1: class FooTask < Rake::TaskLib
   2:  
   3:   attr_accessor :name, :data
   4:  
   5:   def initialize(name = :test)
   6:     @name = name
   7:     yield self if block_given?
   8:     define
   9:   end
  10:  
  11:   def define
  12:     task name do
  13:       puts @data
  14:     end
  15:   end
  16: end

Adding the data accessor will let us use the ‘|t|’ yielded to our task at runtime, like this:

   1: FooTask.new :my_task do |t|
   2:   t.data = "I'm using the data accessor!"
   3: end

Now when we run the task again, we get this output:

image

Using this basic pattern, along with other methods, accessors, etc. we can begin to create a task that can do much more for us by allowing the user of the task to provide configuration of the task at runtime.

 

Calling Dependency Tasks

The last thing I want to show for now, is how to add the ability to call task dependencies when calling our custom task.

In the ‘task :somename’ style of usage, we may want to call other tasks as dependencies of ‘somename’. This is done by using a lambda with the task names. For example:

   1: task :somename => [:somedepdency, :anotherdependency, :whatever, :etc] do
   2:  # do stuff here
   3: end

This code would call the ‘somedependency’, ‘anotherdependency’, ‘whatever’, and ‘etc’ tasks before running the ‘somename’ task.

To provide this same dependency execution in our custom task, we first need to provide a way to specify the dependencies. We can do this by adding a second parameter to the initializer, with a default value that shows we have no dependencies, and then calling the that parameter as a method or hash of methods after storing it in another accessor.

   1: class FooTask < Rake::TaskLib
   2:  
   3:   attr_accessor :name, :task_dependencies, :data
   4:  
   5:   def initialize(name = :test, task_dependencies = {})
   6:     @name = name
   7:     yield self if block_given?
   8:     @task_dependencies = task_dependencies
   9:     define
  10:   end
  11:  
  12:   def define
  13:     task name => task_dependencies do
  14:       puts @data
  15:     end
  16:   end
  17: end

Notice the additional ‘task_dependencies’ accessor, the assignment of an empty hash as the default value in the initializer, storage of the parameter in the ‘task_dependencies’ accessor variable, and then the ‘ => task_dependencies’ lambda execution in the define method. This basic code set allows us to specify a number of dependent tasks and have them executed prior to our custom task executing.

To set a dependency for this task, we only need to add the task names as the second parameter to the constructor in our rakefile. Here’s example rakefile with all of our code, a simple task to use as a dependency, and our custom task being called with that dependency.

   1: require 'rake'
   2: require 'rake/tasklib'
   3:  
   4: class FooTask < Rake::TaskLib
   5:  
   6:   attr_accessor :name, :task_dependencies, :data
   7:  
   8:   def initialize(name = :test, task_dependencies = {})
   9:     @name = name
  10:     yield self if block_given?
  11:     @task_dependencies = task_dependencies
  12:     define
  13:   end
  14:  
  15:   def define
  16:     task name => task_dependencies do
  17:       puts @data
  18:     end
  19:   end
  20: end
  21:  
  22: FooTask.new :my_task, :call_me_first do |t|
  23:   t.data = "I'm using the data accessor!"
  24: end
  25:  
  26: task :call_me_first do
  27:   puts "I'm called first, because I'm the dependency!"
  28: end

The output of this task with its dependency is this:

image

 

Moving On

Now that we have all of the core parts of a real, ‘Rakeish’ custom task in place, we can easily move our FooTask out into it’s own ‘FooTask.rb’ file and have it be a required file in our Rakefile. We could even package this up as a gem and have it be available for installation via the gem system. However, this is not a professional quality task, at this point. There are no tests written for this task, and it doesn’t really do anything valuable other than show how to hack together a custom rake task.

I hope that this little tutorial will at least provide some inspiration to the rest of my Rake using .NET developing colleagues out there in the world, though. I’d really like to see (and will probably build and distribute) a much more professional ‘MSBuild’ rake task, based on this new found knowledge. There are plenty of MSBuild ruby objects out there… it shouldn’t be that difficult to convert it into a rake task and put some unit tests around it.

Kick It on DotNetKicks.com
Posted Sep 17 2009, 11:19 AM by derick.bailey

Comments

DotNetShoutout wrote How A .NET Developer Hacked Out A Rake Task - Derick Bailey - Los Techies
on 09-17-2009 6:47 PM

Thank you for submitting this cool story - Trackback from DotNetShoutout

new ThoughtStream("Derick Bailey"); wrote An Alternate Way To Handle Task Dependencies In Custom Rake Tasks
on 09-17-2009 8:30 PM

Earlier today, I showed how to create a custom Rake task from the base TaskLib , so that we can use more

Reflective Perspective - Chris Alcock » The Morning Brew #436 wrote Reflective Perspective - Chris Alcock &raquo; The Morning Brew #436
on 09-18-2009 3:36 AM

Pingback from  Reflective Perspective - Chris Alcock  » The Morning Brew #436

mendicant wrote re: How A .NET Developer Hacked Out A Rake Task
on 09-18-2009 1:33 PM

I started out using this form for my project, Rubicant. After a while, I realized that the issues that I have to go through in order to have a custom MsBuild far outweighed the much easier solution of having an MsBuild class.

For example:

MsBuildTask.new (:compile, [:call_first, :call_second], {:opt => 'opt1', :opt2 => 'opt2'}) do

end

doesn't feel as nice to me as eventually moving towards:

task :compile => [:call_first, :call_second] do

 MsBuild.new(:opt1 => 'opt1', :opt2 => 'opt2')

end

In the end, I decided that letting Rake handle the tasks and just creating custom classes (or custom methods which wrap those classes) feels and looks much nicer than custom tasks a la NAnt.

Remember, once you get into Rake, you have the full power of an entire language behind you and it's easy to take advantage of.

mendicant wrote re: How A .NET Developer Hacked Out A Rake Task
on 09-18-2009 1:52 PM

To try and be a little more clear...

In the end, I found that once you start having to pass information into your custom task, such as the sln file, build properties like Output dir and such, that having everything in a custom task just got too messy.

In your define, you just end up wrapping what is essentially a custom class with:

task :name => :dependencies do

 #custom code

end

So why do all the extra work, violate SRP, etc just to end up with less clean and expressive code that could just be avoided by having the person using the task just write: write the task :name => :dependencies themselves.

Also, using your footask example, what if they do:

FooTask.new :name, [:dependencies] do

 #some more stuff here

end

Where do you do the #some more stuff here part... before or after? Even better, since you've made this a custom task, will you ever really need to do anything inside the block?

These aren't all answers that I have. Just things that I've been thinking about a lot.

derick.bailey wrote re: How A .NET Developer Hacked Out A Rake Task
on 09-20-2009 10:59 PM

@mendicant,

You bring up some good questions that I'm going to have to think through while playing with this idea.

In terms of any extra work that it would take to maintain a custom task and an msbuild object - they are separate objects. I am calling 'yield @msbuild if block_given?' where @msbuild is a var pointing to an MSBuild object.

with this in play, my custom task is all of 27 lines of code, and there's not much else that I'll have to do with it, if anything. I should be able to continue expanding the core MSBuild object without changing the task. this should let people have the choice of using the custom task or the generick task w/ the MSBuild class directly.

If anyone's interested in seeing what I'm doing, it's up on GitHub: github.com/.../Albacore - it's still very much a work in progress, though... don't expect it to be a great solution, yet. :)

derick.bailey wrote re: How A .NET Developer Hacked Out A Rake Task
on 09-20-2009 11:09 PM

@mendicant,

just checked out your 'rubicant' project on github... looks like great stuff! i'm probably just going to throw out what i'm playing with after a bit, and fork your solution so I can add on to it.

new ThoughtStream("Derick Bailey"); wrote Albacore: A Suite Of Rake Build Tasks For .NET Solutions
on 09-23-2009 11:14 AM

After my previous post on building a “real” rake task , I decided to dive in head first and learn how

StevenHarman.net wrote OMG, Better Rake (for .net)!
on 11-23-2009 6:31 PM

OMG, Better Rake (for .net)!

Copyright Los Techies 2008, 2009. All rights reserved.
Powered by Community Server (Commercial Edition), by Telligent Systems