Skip to content

Upgrading from v1.0

haf edited this page Sep 13, 2014 · 5 revisions

I got a legacy codebase; how can I upgrade it to use the latest version of albacore?

You can see the work in progress at feature/albacore2.

Let's take this verbose Rakefile as an example:

COPYRIGHT = "Copyright 2012 Chris Patterson, Dru Sellers, Travis Smith, All rights reserved."

require File.dirname(__FILE__) + "/build_support/BuildUtils.rb"
require File.dirname(__FILE__) + "/build_support/util.rb"
include FileTest
require 'albacore'
require File.dirname(__FILE__) + "/build_support/versioning.rb"

PRODUCT = 'Topshelf'
CLR_TOOLS_VERSION = 'v4.0.30319'
OUTPUT_PATH = 'bin/Release'

props = {
  :src => File.expand_path("src"),
  :nuget => File.join(File.expand_path("src"), ".nuget", "nuget.exe"),
  :output => File.expand_path("build_output"),
  :artifacts => File.expand_path("build_artifacts"),
  :lib => File.expand_path("lib"),
  :projects => ["Topshelf"],
  :keyfile => File.expand_path("Topshelf.snk")
}

desc "Cleans, compiles, il-merges, unit tests, prepares examples, packages zip"
task :all => [:default, :package]

desc "**Default**, compiles and runs tests"
task :default => [:clean, :nuget_restore, :compile, :package]

desc "Update the common version information for the build. You can call this task without building."
assemblyinfo :global_version do |asm|
  # Assembly file config
  asm.product_name = PRODUCT
  asm.description = "Topshelf is an open source project for hosting services without friction. By referencing Topshelf, your console application *becomes* a service installer with a comprehensive set of command-line options for installing, configuring, and running your application as a service."
  asm.version = FORMAL_VERSION
  asm.file_version = FORMAL_VERSION
  asm.custom_attributes :AssemblyInformationalVersion => "#{BUILD_VERSION}",
	:ComVisibleAttribute => false,
	:CLSCompliantAttribute => true
  asm.copyright = COPYRIGHT
  asm.output_file = 'src/SolutionVersion.cs'
  asm.namespaces "System", "System.Reflection", "System.Runtime.InteropServices"
end

desc "Prepares the working directory for a new build"
task :clean do
	FileUtils.rm_rf props[:output]
	waitfor { !exists?(props[:output]) }

	FileUtils.rm_rf props[:artifacts]
	waitfor { !exists?(props[:artifacts]) }

	Dir.mkdir props[:output]
	Dir.mkdir props[:artifacts]
end

desc "Cleans, versions, compiles the application and generates build_output/."
task :compile => [:versioning, :global_version, :build4, :tests4, :copy4, :build35, :tests35, :copy35]

task :copy35 => [:build35] do
  copyOutputFiles File.join(props[:src], "Topshelf/bin/Release/v3.5"), "Topshelf.{dll,pdb,xml}", File.join(props[:output], 'net-3.5')
  copyOutputFiles File.join(props[:src], "Topshelf.Log4Net/bin/Release/v3.5"), "Topshelf.Log4Net.{dll,pdb,xml}", File.join(props[:output], 'net-3.5')
  copyOutputFiles File.join(props[:src], "Topshelf.NLog/bin/Release/v3.5"), "Topshelf.NLog.{dll,pdb,xml}", File.join(props[:output], 'net-3.5')
  copyOutputFiles File.join(props[:src], "Topshelf.Rehab/bin/Release/v3.5"), "Topshelf.Rehab.{dll,pdb,xml}", File.join(props[:output], 'net-3.5')
	copyOutputFiles File.join(props[:src], "Topshelf.Supervise/bin/Release/v3.5"), "Topshelf.Supervise.{dll,pdb,xml}", File.join(props[:output], 'net-3.5')
end

task :copy4 => [:build4] do
  copyOutputFiles File.join(props[:src], "Topshelf/bin/Release"), "Topshelf.{dll,pdb,xml}", File.join(props[:output], 'net-4.0-full')
  copyOutputFiles File.join(props[:src], "Topshelf.Log4Net/bin/Release"), "Topshelf.Log4Net.{dll,pdb,xml}", File.join(props[:output], 'net-4.0-full')
  copyOutputFiles File.join(props[:src], "Topshelf.NLog/bin/Release"), "Topshelf.NLog.{dll,pdb,xml}", File.join(props[:output], 'net-4.0-full')
  copyOutputFiles File.join(props[:src], "Topshelf.Rehab/bin/Release"), "Topshelf.Rehab.{dll,pdb,xml}", File.join(props[:output], 'net-4.0-full')
	copyOutputFiles File.join(props[:src], "Topshelf.Supervise/bin/Release"), "Topshelf.Supervise.{dll,pdb,xml}", File.join(props[:output], 'net-4.0-full')
end

desc "Only compiles the application."
msbuild :build35 do |msb|
	msb.properties :Configuration => "Release",
		:Platform => 'Any CPU',
                :TargetFrameworkVersion => "v3.5"
	msb.use :net4
	msb.targets :Clean, :Build
  msb.properties[:SignAssembly] = 'true'
  msb.properties[:AssemblyOriginatorKeyFile] = props[:keyfile]
	msb.solution = 'src/Topshelf.sln'
end

desc "Only compiles the application."
msbuild :build4 do |msb|
	msb.properties :Configuration => "Release",
		:Platform => 'Any CPU'
	msb.use :net4
	msb.targets :Clean, :Build
  msb.properties[:SignAssembly] = 'true'
  msb.properties[:AssemblyOriginatorKeyFile] = props[:keyfile]
	msb.solution = 'src/Topshelf.sln'
end

def copyOutputFiles(fromDir, filePattern, outDir)
	FileUtils.mkdir_p outDir unless exists?(outDir)
	Dir.glob(File.join(fromDir, filePattern)){|file|
		copy(file, outDir) if File.file?(file)
	}
end

desc "Runs unit tests"
nunit :tests35 => [:build35] do |nunit|
          nunit.command = File.join('src', 'packages','NUnit.Runners.2.6.3', 'tools', 'nunit-console.exe')
          nunit.options = "/framework=#{CLR_TOOLS_VERSION}", '/nothread', '/nologo', '/labels', "\"/xml=#{File.join(props[:artifacts], 'nunit-test-results-net-3.5.xml')}\""
          nunit.assemblies = FileList[File.join(props[:src], "Topshelf.Tests/bin/Release", "Topshelf.Tests.dll")]
end

desc "Runs unit tests"
nunit :tests4 => [:build4] do |nunit|
          nunit.command = File.join('src', 'packages','NUnit.Runners.2.6.3', 'tools', 'nunit-console.exe')
          nunit.options = "/framework=#{CLR_TOOLS_VERSION}", '/nothread', '/nologo', '/labels', "\"/xml=#{File.join(props[:artifacts], 'nunit-test-results-net-4.0.xml')}\""
          nunit.assemblies = FileList[File.join(props[:src], "Topshelf.Tests/bin/Release", "Topshelf.Tests.dll")]
end

task :package => [:nuget, :zip_output]

desc "ZIPs up the build results."
zip :zip_output => [:versioning] do |zip|
	zip.directories_to_zip = [props[:output]]
	zip.output_file = "Topshelf-#{NUGET_VERSION}.zip"
	zip.output_path = props[:artifacts]
end

desc "restores missing packages"
msbuild :nuget_restore do |msb|
  msb.use :net4
  msb.targets :RestorePackages
  msb.solution = File.join(props[:src], "Topshelf.Tests", "Topshelf.Tests.csproj")
end
desc "restores missing packages"
msbuild :nuget_restore do |msb|
  msb.use :net4
  msb.targets :RestorePackages
  msb.solution = File.join(props[:src], "Topshelf.Log4Net", "Topshelf.Log4Net.csproj")
end
desc "restores missing packages"
msbuild :nuget_restore do |msb|
  msb.use :net4
  msb.targets :RestorePackages
  msb.solution = File.join(props[:src], "Topshelf.NLog", "Topshelf.NLog.csproj")
end

desc "Builds the nuget package"
task :nuget => [:versioning, :create_nuspec] do
  sh "#{props[:nuget]} pack #{props[:artifacts]}/Topshelf.nuspec /Symbols /OutputDirectory #{props[:artifacts]}"
  sh "#{props[:nuget]} pack #{props[:artifacts]}/Topshelf.Log4Net.nuspec /Symbols /OutputDirectory #{props[:artifacts]}"
  sh "#{props[:nuget]} pack #{props[:artifacts]}/Topshelf.NLog.nuspec /Symbols /OutputDirectory #{props[:artifacts]}"
  sh "#{props[:nuget]} pack #{props[:artifacts]}/Topshelf.Rehab.nuspec /Symbols /OutputDirectory #{props[:artifacts]}"
	sh "#{props[:nuget]} pack #{props[:artifacts]}/Topshelf.Supervise.nuspec /Symbols /OutputDirectory #{props[:artifacts]}"
end

nuspec :create_nuspec do |nuspec|
  nuspec.id = 'Topshelf'
  nuspec.version = NUGET_VERSION
  nuspec.authors = 'Chris Patterson, Dru Sellers, Travis Smith'
  nuspec.summary = 'Topshelf, Friction-free Windows Services'
  nuspec.description = 'Topshelf is an open source project for hosting services without friction. By referencing Topshelf, your console application *becomes* a service installer with a comprehensive set of command-line options for installing, configuring, and running your application as a service.'
  nuspec.title = 'Topshelf'
  nuspec.projectUrl = 'http://github.com/Topshelf/Topshelf'
  nuspec.iconUrl = 'http://topshelf-project.com/wp-content/themes/pandora/slide.1.png'
  nuspec.language = "en-US"
  nuspec.licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0"
  nuspec.requireLicenseAcceptance = "false"
  nuspec.output_file = File.join(props[:artifacts], 'Topshelf.nuspec')
  add_files props[:output], 'Topshelf.{dll,pdb,xml}', nuspec
  nuspec.file(File.join(props[:src], "Topshelf\\**\\*.cs").gsub("/","\\"), "src")
end

nuspec :create_nuspec do |nuspec|
  nuspec.id = 'Topshelf.Log4Net'
  nuspec.version = NUGET_VERSION
  nuspec.authors = 'Chris Patterson, Dru Sellers, Travis Smith'
  nuspec.summary = 'Topshelf, Friction-free Windows Services'
  nuspec.description = 'Log4Net Logging Integration for Topshelf. Topshelf is an open source project for hosting services without friction. By referencing Topshelf, your console application *becomes* a service installer with a comprehensive set of command-line options for installing, configuring, and running your application as a service.'
  nuspec.title = 'Topshelf.Log4Net'
  nuspec.projectUrl = 'http://github.com/Topshelf/Topshelf'
  nuspec.iconUrl = 'http://topshelf-project.com/wp-content/themes/pandora/slide.1.png'
  nuspec.language = "en-US"
  nuspec.licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0"
  nuspec.requireLicenseAcceptance = "false"
  nuspec.dependency "Topshelf", NUGET_VERSION
  nuspec.dependency "Log4Net", "2.0.3"
  nuspec.output_file = File.join(props[:artifacts], 'Topshelf.Log4Net.nuspec')
  add_files props[:output], 'Topshelf.Log4Net.{dll,pdb,xml}', nuspec
  nuspec.file(File.join(props[:src], "Topshelf.Log4Net\\**\\*.cs").gsub("/","\\"), "src")
end

nuspec :create_nuspec do |nuspec|
  nuspec.id = 'Topshelf.NLog'
  nuspec.version = NUGET_VERSION
  nuspec.authors = 'Chris Patterson, Dru Sellers, Travis Smith'
  nuspec.summary = 'Topshelf, Friction-free Windows Services'
  nuspec.description = 'NLog Logging Integration for Topshelf. Topshelf is an open source project for hosting services without friction. By referencing Topshelf, your console application *becomes* a service installer with a comprehensive set of command-line options for installing, configuring, and running your application as a service.'
  nuspec.title = 'Topshelf.NLog'
  nuspec.projectUrl = 'http://github.com/Topshelf/Topshelf'
  nuspec.iconUrl = 'http://topshelf-project.com/wp-content/themes/pandora/slide.1.png'
  nuspec.language = "en-US"
  nuspec.licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0"
  nuspec.requireLicenseAcceptance = "false"
  nuspec.dependency "Topshelf", NUGET_VERSION
  nuspec.dependency "NLog", "2.1.0"
  nuspec.output_file = File.join(props[:artifacts], 'Topshelf.NLog.nuspec')
  add_files props[:output], 'Topshelf.NLog.{dll,pdb,xml}', nuspec
  nuspec.file(File.join(props[:src], "Topshelf.NLog\\**\\*.cs").gsub("/","\\"), "src")
end

nuspec :create_nuspec do |nuspec|
  nuspec.id = 'Topshelf.Rehab'
  nuspec.version = NUGET_VERSION
  nuspec.authors = 'Chris Patterson, Dru Sellers, Travis Smith'
  nuspec.summary = 'Topshelf, Friction-free Windows Services'
  nuspec.description = 'Rehab provides automatic updates to services. Topshelf is an open source project for hosting services without friction. By referencing Topshelf, your console application *becomes* a service installer with a comprehensive set of command-line options for installing, configuring, and running your application as a service.'
  nuspec.title = 'Topshelf.Rehab'
  nuspec.projectUrl = 'http://github.com/Topshelf/Topshelf'
  nuspec.iconUrl = 'http://topshelf-project.com/wp-content/themes/pandora/slide.1.png'
  nuspec.language = "en-US"
  nuspec.licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0"
  nuspec.requireLicenseAcceptance = "false"
  nuspec.dependency "Topshelf", NUGET_VERSION
  nuspec.output_file = File.join(props[:artifacts], 'Topshelf.Rehab.nuspec')
  add_files props[:output], 'Topshelf.Rehab.{dll,pdb,xml}', nuspec
  nuspec.file(File.join(props[:src], "Topshelf.Rehab\\**\\*.cs").gsub("/","\\"), "src")
end

nuspec :create_nuspec do |nuspec|
  nuspec.id = 'Topshelf.Supervise'
  nuspec.version = NUGET_VERSION
  nuspec.authors = 'Chris Patterson, Dru Sellers, Travis Smith'
  nuspec.summary = 'Topshelf, Supervised Services'
  nuspec.description = 'Supervise provides automatic recovery, memory and CPU monitoring, and scheduled restarting to services. Topshelf is an open source project for hosting services without friction. By referencing Topshelf, your console application *becomes* a service installer with a comprehensive set of command-line options for installing, configuring, and running your application as a service.'
  nuspec.title = 'Topshelf.Supervise'
  nuspec.projectUrl = 'http://github.com/Topshelf/Topshelf'
  nuspec.iconUrl = 'http://topshelf-project.com/wp-content/themes/pandora/slide.1.png'
  nuspec.language = "en-US"
  nuspec.licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0"
  nuspec.requireLicenseAcceptance = "false"
  nuspec.dependency "Topshelf", NUGET_VERSION
  nuspec.output_file = File.join(props[:artifacts], 'Topshelf.Supervise.nuspec')
  add_files props[:output], 'Topshelf.Supervise.{dll,pdb,xml}', nuspec
  nuspec.file(File.join(props[:src], "Topshelf.Supervise\\**\\*.cs").gsub("/","\\"), "src")
end

def project_outputs(props)
	props[:projects].map{ |p| "src/#{p}/bin/#{BUILD_CONFIG}/#{p}.dll" }.
		concat( props[:projects].map{ |p| "src/#{p}/bin/#{BUILD_CONFIG}/#{p}.exe" } ).
		find_all{ |path| exists?(path) }
end

def get_commit_hash_and_date
	begin
		commit = `git log -1 --pretty=format:%H`
		git_date = `git log -1 --date=iso --pretty=format:%ad`
		commit_date = DateTime.parse( git_date ).strftime("%Y-%m-%d %H%M%S")
	rescue
		commit = "git unavailable"
	end

	[commit, commit_date]
end

def add_files stage, what_dlls, nuspec
  [['net35', 'net-3.5'], ['net40', 'net-4.0'], ['net40-full', 'net-4.0-full']].each{|fw|
    takeFrom = File.join(stage, fw[1], what_dlls)
    Dir.glob(takeFrom).each do |f|
      nuspec.file(f.gsub("/", "\\"), "lib\\#{fw[0]}")
    end
  }
end

def waitfor(&block)
	checks = 0

	until block.call || checks >10
		sleep 0.5
		checks += 1
	end

	raise 'Waitfor timeout expired. Make sure that you aren\'t running something from the build output folders, or that you have browsed to it through Explorer.' if checks > 10
end

source

It contains these concepts, in order:

  • the versioning helpers
  • strong naming
  • assembly info generation
  • a clean task
  • a compile task for .Net 4.0 and .Net 3.5
  • a task that copies files for .Net 3.5
  • a task that copies files for .Net 4.5
  • a task that builds .Net 3.5
  • a task that builds .Net 4.5
  • a task that runs tests for .Net 3.5
  • a task that runs tests for .Net 4.5
  • a task that zips the output
  • tasks to restore nugets
  • tasks to create nuspecs
  • tasks to package those into nupkgs
  • inline functions that handle getting details from git

We're going to improve on it a bit.

The first part of the file is:

COPYRIGHT = "Copyright 2012 Chris Patterson, Dru Sellers, Travis Smith, All rights reserved."

require File.dirname(__FILE__) + "/build_support/BuildUtils.rb"
require File.dirname(__FILE__) + "/build_support/util.rb"
include FileTest
require 'albacore'
require File.dirname(__FILE__) + "/build_support/versioning.rb"

This is a bad way of writing it because a) it's unidiomatic - instead require_relative should be used: require_relative 'build_support/build_utils' (and snake_case should be used for the file names instead of PascalCase).

That's not as interesting as how the functionality can be made. I'm going to ignore utils, as that's mostly helper functions, which are better suited as pull-requests towards this repository or as a piece of the 'Albacore::Tools' namespace.

versioning.rb is my own creation, by an early self. It's been incorporated into v2.0 with require 'albacore/tasks/versionizer' -- and you can create a Task with:

Albacore::Tasks::Versionizer.new :versioning

which you now can depend on.

Continuing:

PRODUCT = 'Topshelf'
CLR_TOOLS_VERSION = 'v4.0.30319'
OUTPUT_PATH = 'bin/Release'

These ruby constants are better defined in the appspecs; with a loop to avoid repetitions (after all all rake files are pure ruby with some helper methods thrown into the global scope). And output path is better specified in the XXproj files of your project, having more semantic sounding environment variables defined with build.prop 'CI_SIGN_AUTHENTICODE', '\\share\corp.pfx' or similar - letting the individual project files handle the output. This is especially important as MsBuild/XBuild copies the DLLs around from the original output folder, and hence it is very hard to get the full copying + signing + modifying + ilmerging + whatever done after the project has finished building. Have a look at how latest Logary handles signing with Authenticode to get an idea of this.

props = {
  :src => File.expand_path("src"),
  :nuget => File.join(File.expand_path("src"), ".nuget", "nuget.exe"),
  :output => File.expand_path("build_output"),
  :artifacts => File.expand_path("build_artifacts"),
  :lib => File.expand_path("lib"),
  :projects => ["Topshelf"],
  :keyfile => File.expand_path("Topshelf.snk")
}

Having 'global vars collected in a hash map' is something I played with initially, because it 'feels good to have it in a single place' - but again, the cohesion you lose isn't worth it, and you can avoid repeating yourself through looping and the new .appspec and reading of XXproj file support instead.

Note how we now have two confusing concepts of output path. In general, you need to keep your outputs immutable; this means that you should absolutely not build everything into a single folder through MsBuild, but rather, after the execution has finished, move everything once!, no overwriting of identical files, to its final resting place. While Albacore doesn't provide similar functionality as Haskell's Shake, it helps thinking about files as immutable and writing code to the same extent.

The keyfile should be specified in the XXproj files, not in this has map, unless there are automated facilities in place for replacing that keyfile during build.

The next:

desc "Cleans, compiles, il-merges, unit tests, prepares examples, packages zip"
task :all => [:default, :package]

desc "**Default**, compiles and runs tests"
task :default => [:clean, :nuget_restore, :compile, :package]

is identical in v2. So far we have:

Rakefile:

require 'bundler/setup'

require 'albacore'
require 'albacore/tasks/versionizer'

Albacore::Tasks::Versionizer.new :versioning

Gemfile:

source 'https://rubygems.org
gem 'albacore', '~> 2.0.0'

Next follows a solution version file, but before that, let's have a look at the project files. All of them contain this crap that doesn't work cross-platform:

     <FileAlignment>512</FileAlignment>
     <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
     <RestorePackages>true</RestorePackages>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
     <PlatformTarget>x86</PlatformTarget>
@@ -83,12 +81,4 @@
     <Folder Include="Properties\" />
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Import Project="$(SolutionDir)\.nuget\nuget.targets" />

Remove anything nuget, and also <SolutionDir ... and <RestorePackages..., because we're going to do that in a better way with albacore:

desc 'restore all nugets as per the packages.config files'
nugets_restore :restore do |p|
  p.out = 'src/packages'
  p.exe = 'tools/NuGet.exe'
end

And then that's done for all projects.

Now, the solution version file:

desc "Update the common version information for the build. You can call this task without building."
assemblyinfo :global_version do |asm|
  # Assembly file config
  asm.product_name = PRODUCT
  asm.description = "Topshelf is an open source project for hosting services without friction. By referencing Topshelf, your console application *becomes* a service installer with a comprehensive set of command-line options for installing, configuring, and running your application as a service."
  asm.version = FORMAL_VERSION
  asm.file_version = FORMAL_VERSION
  asm.custom_attributes :AssemblyInformationalVersion => "#{BUILD_VERSION}",
    :ComVisibleAttribute => false,
    :CLSCompliantAttribute => true
  asm.copyright = COPYRIGHT
  asm.output_file = 'src/SolutionVersion.cs'
  asm.namespaces "System", "System.Reflection", "System.Runtime.InteropServices"
end

We'll ignore the asmver_files task type, and use the equivalent, asmver task type:

asmver :asmver => :versioning do |a|
  a.file_path  = 'src/SolutionVersion.cs'
  a.namespace  = 'Topshelf'
  a.attributes assembly_title: 'Topshelf',
               assembly_version: ENV['LONG_VERSION'],
               assembly_file_version: ENV['LONG_VERSION'],
               assembly_informational_version: ENV['BUILD_VERSION'],
               assembly_copyright: "Copyright #{Time.now.year} Chris Patterson, Dru Sellers, Travis Smith, All rights reserved.",
               assembly_description: 'Topshelf is an open source project for hosting services without friction. By referencing Topshelf, your console application *becomes* a service installer with a comprehensive set of command-line options for installing, configuring, and running your application as a service.',
               com_visible: false
end

As you can see I haven't written the CLSCompliant attribute as I have never in my 11 years of doing .Net seen a use-case for it and different languages routinely stamp all over its semantics - the virtual machine is the definition of what's compliant and what's not; there are two of them: .Net 4.5 (CLR 4.special) and Mono 3.x with now and they are the yardsticks.

TBD

Clone this wiki locally