private override

23Feb/095

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
Comments (5) Trackbacks (2)
  1. 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.

  2. re: Shawn
    Excellent point. I’ll see what I can do, and update accordingly.

  3. 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

  4. 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

  5. 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


Leave a comment