Sex, drugs and sausage rolls

Tech and Life… as seen by Tallmaris (il Gran Maestro)

Build and deploy .NET projects with Rake and Albacore

Hello. I wanted to share with you a small rake script that I use to build and deploy my .NET projects, be they web sites or simple DLLs that need consistent versioning and trackability in the wild.

If you don’t know rake, it is a task oriented ruby gem that allows to do a lot of things… you can get more info from this link, including installation details.

Of course before installing rake you will need to install Ruby on your machine, just head to the installation download page and get the version you prefer. I am using 1.9.3 but 2.0.0 should work perfectly.

Once Ruby and Rake are installed, you need albacore… Just open your DOS prompt and type gem install albacore and that will do. If you don’t know what albacore is and if you have problem installing just head to rubysource for a quick overview.

The script

Let’s start to write our script and analyse it as we go through. Open your favourite text editor and start typing.

require 'albacore'

load 'rakeconfig.rb'

$svn_revision = "WRONG"

desc "Default task: Update version, Cleans and Build in Debug"
task :default => [:config, :assemblyinfo, :build]

Save the file as rakefile.rb (one of the default names for rakefiles, this frees you from specifying the filename when running rake).
The first line here loads the albacore gem, while the second line will load the config file (which I like to keep separate since it will be different for each project).
I also like to have the $svn_revision initialised to a dummy value so we know if something went wrong later on, when we get this number from the repository.
The task keyword creates a task, named :default (meaning it will be run by default if you don’t specify anything on the command line), specifying that it depends on other three tasks, and gives it a description (meaning it will be shown when you request the list of tasks from the command line).

The rakeconfig.rb file is simply a container for all the config for your script and looks like this:

SVN_LOG_PATH = if (ENV['SVN_LOG_PATH'] == nil) then "log.xml" else ENV['SVN_LOG_PATH'] end
VERSION_MAJOR_MINOR_BUILD = if (ENV['VERSION_MAJOR_MINOR_BUILD'] == nil) then "1.0.0" else ENV['VERSION_MAJOR_MINOR_BUILD'] end
PUBLISH_DIR = if (ENV['PUBLISH_DIR'] == nil) then "C:\\Developments\\Deploy\\MySolution" else ENV['PUBLISH_DIR'] end
SOLUTION = if (ENV['SOLUTION'] == nil) then "MySolution.sln" else ENV['SOLUTION'] end
PROJECT_NAME = if (ENV['PROJECT_NAME'] == nil) then "MyProject" else ENV['PROJECT_NAME'] end
ASMINFO_PATH = if (ENV['ASMINFO_PATH'] == nil) then "MyProject\\Properties" else ENV['ASMINFO_PATH'] end
REPO_URL = if (ENV['REPO_URL'] == nil) then "https://svn.repo.url/path" else ENV['REPO_URL'] end

The properties are needed further along in the script. They are defined like this so that they can be overridden by a command line option (useful if you use this script both on your machine and your Continuous Integration Server).

The command line

Since we have been talking about the command line a couple of times now, let’s see what option we have available. First, though, let’s add a task to our rakefile so we can see the effects of our efforts. Add this to the rakefile and save:

desc "Shows Config Options"
task :config do
	puts "SVN_LOG_PATH = #{SVN_LOG_PATH}"
	puts "VERSION_MAJOR_MINOR_BUILD = #{VERSION_MAJOR_MINOR_BUILD}"
	puts "PUBLISH_DIR = #{PUBLISH_DIR}"
	puts "SOLUTION = #{SOLUTION}"
	puts "PROJECT_NAME = #{PROJECT_NAME}"
	puts "ASMINFO_PATH = #{ASMINFO_PATH}"
	puts "REPO_URL = #{REPO_URL}"
end

Open a command prompt and type rake -T. This option (uppercase T) will give you a list of tasks with their description. If a task does not have a desc line above it, it will not show in this list; in this way you can have sort-of “private” tasks in your rakefile. In your output you should see the two tasks default and config:

rake config   # Shows Config Options
rake default  # Default task: Update version, Cleans and Build in Debug

Go ahead and type now rake config. This should output the options coming from the rakeconfig.rb file:

SVN_LOG_PATH = log.xml
VERSION_MAJOR_MINOR_BUILD = 1.0.0
PUBLISH_DIR = C:\Developments\Deploy\MySolution
SOLUTION = MySolution.sln
PROJECT_NAME = MyProject
ASMINFO_PATH = MyProject\Properties
REPO_URL = https://svn.repo.url/path

Now try typing: rake config PROJECT_NAME="Another Project". This should output the configs as coming from the config file, except for PROJECT_NAME which should now be “Another Project”. You need to use the quotes if you want to put spaces, and you will need to escape backslashes using \\ of course, in case you are typing paths.

Ok let’s go ahead and try just simply rake. This will try to build the default task and since the deault task depends on the other tasks it will go ahead and try to run those:

rake aborted!
Don't know how to build task 'assemblyinfo'

Tasks: TOP => default
(See full trace by running task with --trace)

Of course it will abort immediately since it does not know what the other tasks are! So let’s get started on those.

The AssemblyInfo Task

Until now, in case you were wondering, we haven’t used anything that comes from Albacore: all of the above was pure rake stuff. Now we are going to use the albacore gem to create a task that updates the AssemblyInfo.cs file in our project so that the revision version matches the SVN revision. The task looks like this:

desc "Sets the version number to the latest SVN revision"    
assemblyinfo :assemblyinfo do |asm| 
	sh "copy #{ASMINFO_PATH}\\AssemblyInfo.template #{ASMINFO_PATH}\\AssemblyInfo.cs"
	asm.input_file = "#{ASMINFO_PATH}/AssemblyInfo.cs"
	asm.output_file = "#{ASMINFO_PATH}/AssemblyInfo.cs"
	version = get_version()
	asm.version = version
	asm.file_version = version
end

def get_version()
	sh "svn info #{REPO_URL} --xml > #{SVN_LOG_PATH}"
	log = REXML::Document.new File.new(SVN_LOG_PATH)
	logEntry = log.root.elements["entry"]
	$svn_revision = logEntry.attributes["revision"]
	"#{VERSION_MAJOR_MINOR_BUILD}.#{$svn_revision}"
end

Here, the first assemblyinfo is the albacore task we are invoking, while :assemblyinfo is the task name; it can be anything you like and it must match the name used above in the :default task dependecies: => [:config, :assemblyinfo, :build].
So, first of all I copy my AssemblyInfo.template as the AssemblyInfo.cs. I do this because I don’t want to commit my real file everytime I change the version number, but I like to somehow have the file in SVN, so I keep the template file versioned and the .cs file unversioned.

The assemblyinfo task has a lot more options that you can read in the Albacore Wiki. We are using the input and output file options so we don’t have to create a new file from scratch but simply edit the existing one.

To get the SVN version, the get_version() method runs the svn info command (the svn command line client should be in your PATH of course) and gets the output into an xml file (see how we use the config options specified in the rakeconfig file?). The xml file will look something like this:

<info>
  <entry kind="dir" path="website" revision="48">
    <url>https://svn.url/path</url>
    <repository>
      <root>https://sv.url/svn</root>
      <uuid>29d03e39-d19d-054e-b529-9ccdbb0ca05a</uuid>
    </repository>
    <commit revision="48">
      <author>ltramma</author>
      <date>2013-04-30T13:42:31.618491Z</date>
    </commit>
  </entry>
</info>

So we use REXML (which is built into ruby) to get the entry element and read the revision attribute… nothing too fancy here. We end up with a number than in this case will be 1.0.0.48 and we stick it in the Version and FileVersion attributes of your Assembly Info File. If you open it, at the bottom you should see:

[assembly: AssemblyVersion("1.0.0.78")]
[assembly: AssemblyFileVersion("1.0.0.78")]

Building

Now for the third missing task, let’s still leverage albacore and the built-in msbuild task to build our solution. Add this task to your rakefile:

msbuild :build do |msb|
	msb.properties = { :configuration => :Debug }
	msb.targets = [ :Clean, :Build ]      
	msb.solution = SOLUTION
end

That’s it, it could not be any easier could it? I have omitted the description here since I would like people to use the default (or the other “public”) task, following a series of predefined steps rather than cherry-picking the task to run.

Now your command line rake command should work without problems and your solution will build. If you go into the bin\Debug folder and right click on your project DLL, selecting Properties and then the Details tab should show the FileVersion as being what we wanted. Cool, isn’t it?

Unit testing

As a bonus, albacore also has nunit and mstest tasks to run your tests. You simply need to create a task like this:

nunit :test do |nunit|
	nunit.command = "nunit-console.exe"
	nunit.assemblies "path/to/MyProject.Tests.dll", "path/to/OtherTests.dll"
end

And since I did not put the description here as well, let’s add this task as part of the default:

task :default => [:config, :assemblyinfo, :build, :test]

And that’s it.

Publish

At this point it shold be pretty easy to create a task for publishing your website:

desc "Publish website into the #{PUBLISH_DIR}/ folder"
msbuild :publish do |msb|
	msb.targets [ :Clean, :Build ]
	msb.properties = {
		:configuration => :Release,
		:UseWPP_CopyWebApplication => true,
		:PipelineDependsOnBuild => false,
		:webprojectoutputdir => "#{PUBLISH_DIR}/", 
		:outdir => "#{PUBLISH_DIR}/bin/" 
	}
	msb.solution = "#{PROJECT_NAME}.csproj"
end

The properties just set up everything for deploying. The :UseWPP_CopyWebApplication => true will do your Web.Config transforms, this will require the :PipelineDependsOnBuild => false, or your build will fail.

That is all for now. You may notice there is some inconsistent use of the config properties (some have the extension, some don’t, we need to put trailing slashes etc.), but I am sure it is something you can easily make more consistent yourself.

Thanks for reading and happy coding!

, , , , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.