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!
Advanced Bundling and Minification of Coffeescripts in MVC4 Continuous Integration with a Custom NuGet server