build automation evolution – Rake and .NET
When I first started with build automation, I started out with NAnt. I loved NAnt. NAnt loved me. We were happy. I could program anything with the NAnt XML goodness. If there wasn't a function or task to do what I wanted, I simply wrote one, compiled it, and wrote some more XML; it couldn't be any more simple! I think the part I liked the most was the instant gratification I had with being able to automate something that would/could not otherwise be automated [at least not in a simple manner] with a little bit of XML programming.
Soon after NAnt gained popularity, MSBuild was released from Microsoft, which eventually effectively squashed NAnt (IMHO, no stats to back this up). We never migrated our scripts over to MSBuild because we had significant investment in NAnt already, but it wasn't hard to shell off to MSBuild to compile our solutions. Eventually I worked on a new project (at a new company) and needed to learn how to use MSBuild since we were using TFS on that project.
Shortly after I started integrating MSBuild into NAnt, and then started learning MSBuild, I started feeling a twinge. Now that automation is a given, I need something more than programming in this extremely limited XML environment. Sure, I can write a new MSBuild task just like I did in NAnt, but is it worth it? My answer is an emphatic no. I need a great user experience. Something that feels nice AND is powerful.
Enter RAKE.
It sounds like MAKE; if it looks and feels like MAKE, I might vomit! No thanks!
Glad you brought that up, Dear Reader (If Hanselman can reference you like that, I can too). It's not really like MAKE. In fact, the things you do inside of a RAKE file, is write Ruby code! RAKE really gives you a nice [internal] DSL for automating tasks. If there is something you want to do that isn't built in, write a little ruby code to do it. No compilation and putting the dll in the write place, etc. etc. Programming in Ruby vs. XML... now that feels nice (requirement #1 above).
But wait! RAKE is for building Ruby and Rails apps, we can't possibly use it for .NET!
RAKE, just like Ant, NAnt or MSBuild, is a general purpose, task based automation tool. It may be written in Ruby, but it can build .NET solutions (with the help of MSBuild), Java projects (with the help of Ant or Maven), or Flex, or whatever. I call that powerful (requirement #2 above).
Please note I'm not claiming to be the first person to do this in .NET, I've found lots of other guys doing it too.
Here is an example of my first rake script for .NET (some pieces borrowed heavily from the Fluent NH guys... thanks!).
Enjoy/Discuss.
An always updated version of this file can be found here: http://jonfuller.googlecode.com/svn/trunk/code/CoreLib/RakeFile
require "BuildUtils.rb"
include FileTest
require 'rubygems'
gem 'rubyzip'
require 'zip/zip'
require 'zip/zipfilesystem'
#building stuff
COMPILE_TARGET = "debug"
CLR_VERSION = "v3.5"
SOLUTION = "src/CoreLib.sln"
MAIN_PROJECT = "CoreLib"
# versioning stuff
BUILD_NUMBER = "0.1.0."
PRODUCT = "CoreLib"
COPYRIGHT = "Copyright © 2009 Jon Fuller"
COMPANY = "Jon Fuller"
COMMON_ASSEMBLY_INFO = "src/CommonAssemblyInfo.cs"
desc "Compiles, tests"
task :all => [:default]
desc "Compiles, tests"
task :default => [:compile, :unit_test, :package]
desc "Update the version information for the build"
task :version do
builder = AsmInfoBuilder.new BUILD_NUMBER,
:product => PRODUCT,
:copyright => COPYRIGHT,
:company => COMPANY
builder.write COMMON_ASSEMBLY_INFO
end
desc "Prepares the working directory for a new build"
task :clean do
Dir.mkdir output_dir unless exists?(output_dir)
end
desc "Compiles the app"
task :compile => [:clean, :version] do
MSBuildRunner.compile :compilemode => COMPILE_TARGET,
:solutionfile => SOLUTION,
:clrversion => CLR_VERSION
end
desc "Runs unit tests"
task :unit_test => :compile do
runner = NUnitRunner.new :compilemode => COMPILE_TARGET,
:source => 'src',
:tools => 'tools',
:results_file => File.join(output_dir, "nunit.xml")
runner.executeTests Dir.glob("src/*Test*").map { |proj| proj.split('/').last }
end
desc "Displays a list of tasks"
task :help do
taskHash = Hash[*(`rake.cmd -T`.split(/\n/).collect { |l| l.match(/rake (\S+)\s+\#\s(.+)/).to_a }.collect { |l| [l[1], l[2]] }).flatten]
indent = " "
puts "rake #{indent}#Runs the 'default' task"
taskHash.each_pair do |key, value|
if key.nil?
next
end
puts "rake #{key}#{indent.slice(0, indent.length - key.length)}##{value}"
end
end
desc "Packages the binaries into a zip"
task :package => :compile do
source_files = Dir.glob("src/#{MAIN_PROJECT}/bin/#{COMPILE_TARGET}/**/*")
dest_files = source_files.map{ |f| f.sub("src/#{MAIN_PROJECT}/bin/#{COMPILE_TARGET}/", "#{MAIN_PROJECT}/")}
Zip::ZipFile.open(File.join(output_dir, "#{MAIN_PROJECT}.zip"), 'w') do |zipfile|
0.upto(source_files.size-1) do |i|
puts "Zipping #{source_files[i]} to #{dest_files[i]}"
zipfile.add(dest_files[i], source_files[i])
end
end
end
def output_dir
if ENV.keys.include?('CC_BUILD_ARTIFACTS')
return ENV['CC_BUILD_ARTIFACTS']
else
return 'results'
end
end

February 23rd, 2009 - 14:27
I’m definitely enjoying that! Very nice work. It might even be MORE enjoyable if you wrapped some of those helper classes in Tasks, similar to how rake’s GemTask and PackageTask are used.
February 24th, 2009 - 09:43
re: Shawn
Excellent point. I’ll see what I can do, and update accordingly.
April 2nd, 2009 - 02:09
This is great. If you’re interested, I have a question on StackOverflow with a bounty. You could probably have it uncontested. http://stackoverflow.com/questions/679009/anyone-have-experience-calling-rake-from-msbuild-for-code-gen-and-other-benefits
September 9th, 2009 - 04:49
Nice writeup.
I have a question though. I need to write incremental builds, where I only build and update the assemblyinfo-file for the actual projects being built.
I need to have a clean version built, specifically for testing (is everything still working if we have to build everything from scratch), but I also need my application’s different dll’s to have independent versioning. Do you know how to get this done?
I know that MSBuild can take a solution-file and then only build the projects that needs building, and in correct dependency-order. But, if I use your method and update all assemblyInfo-files surely MSBuild will consider all projects “touched”, wouldn’t it?
Thanks,
Ronny
September 9th, 2009 - 18:29
Thanks!
I’m not sure I understand exactly what you need, but I can at least take a stab at it.
What I’m hearing: You want to only version (and build) those projects that have actually changed.
I think that is doable. You can do something similar to what MSBuild is doing for determining if there are changes or not in a given project directory. You can check the file modified time (there are ruby file API’s for this), and then only version those specific projects… then shell out to MSBuild to build your solution, then MSBuild will only build those projects that you’ve versioned, rather than all projects.
Did I understand you correctly? Good luck