Hello; I’m Pete, and I’m a software developer working in internet TV using the .NET framework.

That out the way, I’m also the guy that looks after our CI at work. Which has meant that two years ago, I wrote My First MSBuild Project (tm), and it was truly a hacked-together nightmare that no-one else dared look at, let alone try to fiddle with. We so badly wanted automated deployment, but, well, it was just too hard. It was “good enough” for CI with Cruise Control.NET, and, well, there were other fish to fry.

Fast forward to now, or three months or so ago, and I used this in-depth guide to have a second go - we’re rebuilding our product suite, and, well, I’d like the sense of “new, done right” to be pervasive throughout. So; an improvement, but still no cigar - my colleagues didn’t see the need to care how it worked (which I suppose is a testament to frictionless, but it doesn’t exactly mitigate the bus factor… ). This time, I tried out Thoughtworks Studios’ new Cruise product (which is really nice, but fledgling compared to the feature-rich TeamCity, which I’m trying out now).

So, this week, I needed to fiddle with the build script (I know, MSBuild projects are not really “scripts”, and therein lies the pain… ) to make it more modular so that different parts can run against pulled-down artifacts rather than compiling from scratch each time (both Cruise and TeamCity use distributed build-agents, so if one wants to parallelise, one has to publish artifacts to the CI server for later stages to pull down and run against). Let’s just say that the change didn’t go well, and I threw in the towel (and, since it’s Christmas time, there’s no-one around to say “no, don’t spend time on that, it’s not worth the time”…).

Enter Fluent NHibernate. This is something I’ve looked at, and in so doing had to compile from source. It uses rake. Curious, I take a closer look. It looks really … simple. Not a pointy bracket in sight, for a start. Some googling yields some encouraging blogged about opinions and experiences. Let’s give it a whirl.

A day and a half later (having never touched Ruby or Rake before, but having wasted half a day trying to program by coincidence, before giving up and reading Justin Etheridge’s IronRuby for C# developers series), I have a clue about Ruby, and a Rakefile that:

  1. Cleans build artifacts (calling msbuild to do it against the solution’s projects, and calling Ruby code to delete other things)
  2. Generates AssemblyInfo.cs files with tokens replaced (like CI-label and svn-revision number, product name, build date, etc) (the C# projects include these via links against CommonAssemblyInfo.cs, and there’s a different one for the test projects)
  3. Runs msbuild to do a solution build (as in, all projects in the solution)
  4. Harvests the (configuration-specific) output (DLLs, PDBs, XML-doc) into an artifacts directory
  5. Runs and generates reports for xunit.net unit tests
  6. Runs and generates reports for xunit.net integration tests (ones that are not unit-tests; DB ones, and some others)

I like this. What’s more, it does all that in around 250 lines of Ruby (including whitespace and comments and desc headers for the tasks), compared to around 800 of XML-hell. To cap it off, I needed to learn a bit about a language that’s not C#, and it’s been a while since I messed with anything else other than JavaScript for UI work.

Compare. Here is what [2] looked like in msbuild:

<target Name="GetRevisionNumber">
  <!-- Get the revision number of the local working copy -->
  <svnInfo LocalPath="$(MSBuildProjectDirectory)" ToolPath="$(LIBRARYROOT)\svn\bin">
    <output TaskParameter="Revision" PropertyName="Revision"/>
  </svnInfo>
  <message Text="Running build for Revision = $(Revision)" Importance="high" />
</target>

<target Name="Version"
    DependsOnTargets="GetRevisionNumber">

  <propertyGroup>
    <appVersion>$(MajorVersion).$(MinorVersion).$(CRUISE_PIPELINE_LABEL).$(Revision)</appVersion>
  </propertyGroup>

  <message Text="AppVersion number generated: $(AppVersion)" Importance="high"/>

  <time Format="dd/MM/yyyy HH:mm:ss" Kind="Utc">
    <output TaskParameter="FormattedTime" PropertyName="BuiltOn" />
  </time>

  <message Text="BuiltOn: $(BuiltOn)" Importance="high"/>
  <message Text="Looking for: $(MSBuildProjectDirectory)\src\CommonAssemblyInfo.cs.template" />
  <message Text="Writing to: $(MSBuildProjectDirectory)\src\CommonAssemblyInfo.cs" />
  <message Text="Looking for: $(MSBuildProjectDirectory)\src\TestsAssemblyInfo.cs.template" />
  <message Text="Writing to: $(MSBuildProjectDirectory)\src\TestsAssemblyInfo.cs" />

  <itemGroup>
    <assemblyInfoTokens Include="AssemblyVersion">
      <replacementValue>$(AppVersion)</replacementValue>
    </assemblyInfoTokens>
    <assemblyInfoTokens Include="AssemblyConfiguration">
      <replacementValue>$(Configuration)</replacementValue>
    </assemblyInfoTokens>
    <assemblyInfoTokens Include="Company">
      <replacementValue>$(Company)</replacementValue>
    </assemblyInfoTokens>
    <assemblyInfoTokens Include="ProductName">
      <replacementValue>$(ProductName)</replacementValue>
    </assemblyInfoTokens>
    <assemblyInfoTokens Include="BuiltOn">
      <replacementValue>$(BuiltOn)</replacementValue>
    </assemblyInfoTokens>
  </itemGroup>

  <templateFile Template="$(MSBuildProjectDirectory)\src\CommonAssemblyInfo.cs.template"
          OutputFilename="$(MSBuildProjectDirectory)\src\CommonAssemblyInfo.cs"
          Tokens="@(AssemblyInfoTokens)"/>

  <templateFile Template="$(MSBuildProjectDirectory)\tests\TestsAssemblyInfo.cs.template"
          OutputFilename="$(MSBuildProjectDirectory)\tests\TestsAssemblyInfo.cs"
          Tokens="@(AssemblyInfoTokens)"/>
</target>

And here is what it looks like in rake:

desc "Figure out what version (major.minor.build.svn-revision) we're building and store it for other tasks"
task :version => [:setup] do
  build = 0
  if ENV['build.number']
    build = ENV['build.number']
  end

  svn = "\"#{@conf[:paths][:lib]}/svn/bin/svn.exe\""
  svn_revision = get_svn_revision(svn, Dir.pwd)
  @conf[:version] = "#{@conf.fetch(:major, 0)}.#{@conf.fetch(:minor, 0)}.#{build}.#{svn_revision}"
  @conf[:asm_info_replacements][:version] = @conf[:version]
end

desc "Generate the AssemblyInfo.cs files in each project based on build &amp; revision numbers, product name, etc"
task :assembly_info => [:setup, :version] do
  ai = AsmInfo.new(@conf[:paths][:lib], @conf[:paths][:src_asm_info], @conf[:asm_info_replacements])
  ai.run
  ai = AsmInfo.new(@conf[:paths][:lib], @conf[:paths][:tests_asm_info], @conf[:asm_info_replacements])
  ai.run
end

class AsmInfo
  def initialize(lib_path, asm_info, replacements)
    @asm_info = asm_info
    @template = asm_info + '.template'
    @replacements = replacements
  end

  def run
    content = File.read_to_end(@template)
    @replacements.each do |key,value|
      content = content.gsub(/(\$\{#{key}\})/, value.to_s)
    end

    File.delete(@asm_info) if File.exists?(@asm_info)
    File.write(@asm_info, content)
  end
end

class File
  def self.read_to_end(path)
    content = ''
    File.open(path, 'r') do |file|
      while (line = file.gets)
        content += line
      end
    end
    return content
  end

  def self.write(path, content)
    File.open(path, 'w') do |file|
      file.puts content
    end
  end
end

(Both snippets have the setup stuff omitted - this in both cases is about the same amount of config-value setting sort of thing). Here’s the full download#1. I’ll post about experiences with TeamCity later (like when I’ve ascertained whether it’s possible to die from having eaten too much turkey… ).

I love empty offices ;-)