Create a Build Script with Rake in Iron Ruby that Integrates StyleCop, Simian, FxCop, MSpec and NCover

With the release of Iron Ruby we are no more tide up with the xml based build script like NAnt and MSBuild, we can now use Rake with Iron Ruby to build our .NET based projects. In this post, I will show you a very basic build script in rake that will integrate StyleCop, Simian, FxCop, MSpec and NCover. I will use the same fund transfer project that I used in my previous post.

Before moving to the build script, let me give a brief description of the above tools:

  1. StyleCop: It works on source code and ensures that all of rules that you have defined earlier has been meet. These rules can include ode structure, naming Convention, documentation rules etc, you can even create your own custom rules if you want. It has been recently open sourced by Microsoft and can be download from http://stylecop.codeplex.com.
  2. Simian is another excellent tool which also works on source code. It detects the duplicate codes among the given source code files. Though it is a commercial product but it is free to use in non commercial or open source projects. It supports a lot of languages and you can download it from http://www.redhillconsulting.com.au/products/simian.
  3. FxCop is one of the tool that is available from the early days of .NET, similar to StyleCop but it enforce the rules on compiled outputs rather than source codes. The latest version of FxCop is included in the Windows 7 SDK which you can download from the Microsoft Download Center. In this post, I will be using the last standalone version 1.36.
  4. MSpec is my BDD Style Test Framework of choice. Currently there is no official binary version but you can download it and compile it yourself.
  5. NCover is my another favorite tool which reports the code coverage. Usually it is used with the test runner to capture the coverage. In this post I will be using the community edition of NCover which is free. You can download it from http://www.ncover.com/download/current.

Now we know the basics of the these tools, lets define the steps, since both StyleCop and Simian works on source codes we will invoke these before compiling our source codes and rest of the steps after the compilation:

  1. Run StyleCop
  2. Run Simian
  3. Compile
  4. Run FxCop
  5. Run Test And Coverage.
  6. Copy the build artifacts in the Drop location.

This is the skeleton of the rake script for the above steps:

task :style_cop do
	puts "Running StyleCop"
end

task :simian do
	puts "Running Simian"
end

task :compile do
	puts "Compiling projects"
end

task :fx_cop do
	puts "Running FxCop"
end

task :test_and_coverage do
	puts "Running Test and capturing the code coverage"
end

task :drop do
	puts "Preparing the build artifacts"
end

task :default => [:style_cop, :simian, :compile, :fx_cop, :test_and_coverage, :drop] do
	puts "Done"
end

Consider task as MSBuild Target and it has an associated name (ruby symbol) denoted as :name which is same as MSBuild Target name. To make a task depended on other tasks you have to specify the task names in square brackets like the last task of in above script. To invoke one or more specific tasks you have to pass the task name as argument, for multiple tasks use space as separator. For example:

rake simian fx_cop # The rake command by default executes rakefile.rb
will show output:
Running Simian
Running FxCop

If none of the task is passed, it will execute the default task.

Now, lets add the codes in the above skeleton, but before that lets see the directory structure of the Fund Transfer Project:

RakeFS

Though we have six main tasks but there few more things that we have to add. First the initialization

MSBUILD = File.join(ENV["windir"] || ENV["WINDIR"], "Microsoft.NET", "Framework", "v3.5", "msbuild.exe")

CONFIG = ENV["config"] || ENV["CONFIG"] || "Debug"

CURRENT_PATH = File.dirname(__FILE__)
ARTIFACT_PATH = File.join(CURRENT_PATH, "Artifacts")
REPORT_PATH = File.join(ARTIFACT_PATH, "Reports")
REFERENCES_PATH = File.join(CURRENT_PATH, "References")
TOOLS_PATH = File.join(CURRENT_PATH, "Tools")
TEST_SUFFIX = "Specs"

projects = []
app_projects = []
app_files = []
app_dependency_files = []
test_projects = []
test_files = []
test_dependency_files = []
referenced_files = []

desc "Initilizing build"
task :init do

	# This will create the Artifacts directory if it does not exist
	FileUtils.mkdir_p ARTIFACT_PATH

	# Delete the existing files/folders of the Artifacts directory
	Dir.foreach(ARTIFACT_PATH) do | entry |
		FileUtils.remove_entry(File.join(ARTIFACT_PATH, entry)) if (entry != ".") && (entry != "..")
	end

	# Create the Report folder
	FileUtils.mkdir_p REPORT_PATH

	# Get all the CSharp Project files that exists under the current path
	projects = Dir.glob(File.join(CURRENT_PATH, "/**/*.csproj"))

	# Now we have to indentify the Test/Spec project and the regular project
	projects.each do | project |

		project_name = File.basename(project, ".csproj")

		# If Project has the special suffix we will treat it as Test/Spec Project
		# otherwise it is a regular project
		if /#{TEST_SUFFIX}$/.match(project_name)
			test_projects << project
		elsif
			app_projects << project
		end
	end

end

In the above we are first declaring few constants(please note that in Ruby Constants are started with capital letters) and few variables which we are going to use in the tasks. The File.Join is same as .NET Path.Combine. The ENV[YOUR_KEY] is used get the value of Environment variables. In the init task we are making sure the Artifacts and its subfolder Reports exists, next we are scanning all the projects and storing it in the two array’s for later usages. By default, the script assumes that the MSpec projects ends with “Specs”. Next, we will add one more helper task which will clear the build outputs:

desc "Cleaning up outputs"
task :clean do

	# Iterate all the project and pass to MSBuild for cleaning up the build outputs
	projects.each do | project |
		sh "\"#{MSBUILD}\" \"#{project}\" /p:Configuration=#{CONFIG} /t:Clean /tv:3.5"
	end

end

The sh stands for shell, we are using it to execute the MSBuild.exe and passing the required parameters.

Now, lets modify the default task’s dependencies so that the above two tasks are executed prior the others:

desc "Default"
task :default => [:init, :clean, :style_cop, :simian, :compile, :fx_cop, :test_and_coverage, :drop] do

	puts "Build Completed."

end

Among the tasks most of the tasks are invoking external processes, only the style_cop task requires bit of interaction with the clr types. Although StyleCop comes with MSBuild task but as we are not running in MSBuild environment, we cannot use it directly, instead we have to call the StyleCop directly from our rake script and this is the true power of Iron Ruby, we can easily call any clr type and vice versa. The following shows the code which runs the StyleCop analysis:

desc "Running StyleCop"
task :style_cop do

	# We will need the reference of StyleCop
	require File.join(TOOLS_PATH, "stylecop", "Microsoft.StyleCop.dll")

	# Create a List<StyleCopProject> to store the regular projects
	style_cop_projects = System::Collections::Generic::List[Microsoft::StyleCop::CodeProject].new
	
	# Build the File path of StyleCop Report
	report_file = File.join(REPORT_PATH, "StyleCop.xml")

	# Create a StyleCop console to apply the StyleCop rules
	style_cop_console = Microsoft::StyleCop::StyleCopConsole.new(nil, false, report_file, nil, true)
	
	# Create a StyleCop Configuration
	style_cop_configuration = Microsoft::StyleCop::Configuration.new(nil)

	# We will only invoke StyleCop for the regular project, not for the Test/Specs.
	app_projects.each do | project |

		style_cop_project = Microsoft::StyleCop::CodeProject.new(project.hash, project, style_cop_configuration)
		
		# Add all the .cs files that resides in this project
		source_files = Dir.glob(File.join(File.dirname(project), "/**/*.cs"))

		source_files.each do | source_file |
			style_cop_console.core.environment.add_source_code(style_cop_project, source_file, nil)
		end

		style_cop_projects.add(style_cop_project)
	end

	# Start applying the rules
	style_cop_console.start(style_cop_projects, true)
	
	# Now release all the associated resources of StyleCop
	style_cop_console.dispose()

end

The next two tasks are simple external program execution, it just prepares the required parameters and invokes the program:

desc "Running Simian"
task :simian do

	simian_path = File.join(TOOLS_PATH, "simian")
	simian_exe = File.join(simian_path, "simian-2.2.24.exe")
	simian_report = File.join(REPORT_PATH, "Simian.xml")

	app_projects_paths = app_projects.map { | project | "\"" + File.dirname(project) + "\""}.join(" ")

	# Copy the xsl file in the report folder so that we can view the xml as html
	FileUtils.copy(File.join(simian_path, "Simian.xsl"), REPORT_PATH)

	sh "\"#{simian_exe}\" #{app_projects_paths} -formatter=xml:\"#{simian_report}\" -failOnDuplication- -reportDuplicateText+ -includes=\"**/*.cs\""

end

desc "Compiling source"
task :compile do

	projects.each do | project |
		sh "\"#{MSBUILD}\" \"#{project}\" /p:Configuration=#{CONFIG} /t:Build /tv:3.5"
	end

end

Now we add one last helper task, this will ensure that all the project outputs and referenced components are copied into the build Artifacts folder, so that we can use the folder as Working Directory to run the FxCop and MSpec specifications.

desc "Copy compiled files"
task :copy_files do
	
	app_projects.each do | project |

		project_directory = File.dirname(project)
		project_name = File.basename(project, ".csproj")

		project_outputs = Dir.glob(File.join(File.join(project_directory, "bin", CONFIG), "/**/*.*"))

		project_outputs.each do | file |
		
			base_name = File.basename(file)

			if File.basename(base_name, File.extname(file)).eql?(project_name)
				app_files << base_name
			else
				app_dependency_files << base_name
			end
			
			FileUtils.copy(file, ARTIFACT_PATH);

		end

	end

	test_projects.each do | project |

		project_directory = File.dirname(project)
		project_name = File.basename(project, ".csproj")

		project_outputs = Dir.glob(File.join(File.join(project_directory, "bin", CONFIG), "/**/*.*"))

		project_outputs.each do | file |

			base_name = File.basename(file)

			if File.basename(base_name, File.extname(file)).eql?(project_name)
				test_files << base_name
			else
				test_dependency_files << base_name if !app_files.include?(base_name) && !app_dependency_files.include?(base_name)
			end

			FileUtils.copy(file, ARTIFACT_PATH);

		end

	end

	Dir.glob(File.join(REFERENCES_PATH, "/**/*.*")).each do | file |

		referenced_files << File.basename(file)

		FileUtils.copy(file, ARTIFACT_PATH);

	end

end

Depending upon the configuration (usually Debug/Release) we are copying the build outputs from the bin directory of each project and putting it into the Artifacts folder, when copying we are also populating few file list variables so that we can later use it in the FxCop and MSpec tasks. This tasks will be invoked after the compiling the projects. So we will again modify the dependencies of the default task.

desc "Default"
task :default => [:init, :clean, :style_cop, :simian, :compile, :copy_files, :fx_cop, :test_and_coverage, :drop] do

	puts "Build Completed."

end

The next two tasks are also invoking the external processes and building the required parameters:

desc "Running FxCop"
task :fx_cop do

	fxcop_path = File.join(TOOLS_PATH, "fxcop")
	fxcop_exe = File.join(fxcop_path, "FxCopCmd.exe")
	fxcop_report = File.join(REPORT_PATH, "FxCop.xml")

	dlls = "/f:" + app_files.select{ |file| File.extname(file).eql?(".dll")}.map { | dll | "\"" + File.join(ARTIFACT_PATH, dll) + "\""}.join(" /f:")

	FileUtils.copy(File.join(fxcop_path, "Xml", "FxCopReport.xsl"), REPORT_PATH)
	
	sh "\"#{fxcop_exe}\" #{dlls} /d:\"#{ARTIFACT_PATH}\" /o:\"#{fxcop_report}\" /oxsl:\"FxCopReport.xsl\" /to:0 /fo /gac /igc /q"

end

desc "Running tests"
task :test_and_coverage do

	test_runner_exe = File.join(TOOLS_PATH, "mspec", "mspec.exe")

	app_dlls = app_files.select{ |file| File.extname(file).eql?(".dll")}.map { | dll | File.basename(File.basename(dll), File.extname(dll)) }.join(";")
	test_dlls = test_files.select{ |file| File.extname(file).eql?(".dll")}.map { | dll | File.join(ARTIFACT_PATH, dll) }.join(" ")

	test_runner_argument = "\"#{test_runner_exe}\" --html \"#{REPORT_PATH}\" \"#{test_dlls}\""

	exclude_attributes = "System.Runtime.CompilerServices.CompilerGeneratedAttribute;System.CodeDom.Compiler.GeneratedCodeAttribute;System.Diagnostics.DebuggerNonUserCodeAttribute"

	ncover_coverage = File.join(REPORT_PATH, "NCover.Console.xml")
	ncover_path = File.join(TOOLS_PATH, "ncover")
	ncover_console_exe = File.join(ncover_path, "NCover.Console.exe")
	ncover_console_argument = "#{test_runner_argument} //a \"#{app_dlls}\" //x \"#{ncover_coverage}\" //ea \"#{exclude_attributes}\" //w \"#{ARTIFACT_PATH}\" //q"

	FileUtils.copy(File.join(ncover_path, "Coverage.xsl"), REPORT_PATH)

	sh "RegSvr32 \"#{File.join(ncover_path, "CoverLib.dll")}\" /s"

	sh "\"#{ncover_console_exe}\" #{ncover_console_argument}"

	sh "RegSvr32 \"#{File.join(ncover_path, "CoverLib.dll")}\" -u /s"
	
	ncover_explorer_exe = File.join(ncover_path, "NCoverExplorer.Console.exe")
	ncover_report = File.join(REPORT_PATH, "NCoverExplorer.Console.xml")

	FileUtils.copy(File.join(ncover_path, "CoverageReport.xsl"), REPORT_PATH)

	sh "\"#{ncover_explorer_exe}\" \"#{ncover_coverage}\" /r:ModuleClassFunctionSummary /x:\"#{ncover_report}\" /q"

end

And in the last task we will do some cleanup so that the artifacts directory only contains the application dll/pdb/xml docs and all the xml/html reports of the above tools.

desc "Preparing drop"
task :drop do

	app_dependency_files.concat(test_files).concat(test_dependency_files).concat(referenced_files).uniq().each do | file |
		FileUtils.remove_file(File.join(ARTIFACT_PATH, file), true)
	end
	
end

If you open the Artifacts and the Reports folder after running the rake scripts, you will see:

artifacts

As mentioned that it is a very basic rake example which you can use for building relatively simple applications as it does not have any customization support (though you can always modify the source code). But the good news is there is already rake task library for the .NET applications called Albacore which I highly recommend you to check.

That’s it for today. You can download the complete source code of this post from the following link.

Download: RakeIntegration.zip

 

Shout it

4 Comments

  • Excuse my ignorance, but I was kinda curious about comparing msbuild with rake...are there some big differences which immediately struck you ?

  • @varun: The most important advantage is _I am writing code_. Defining your custom logic in Xml is not an option, though you can write simple loops, if/else/switch condition, but when it comes to beyond that, you always have to create a MSBuild Task. Just take the example of StyleCop, to use it in MSBuild there is a specific task for it, but in this case I an easily call it form my build script. And the good thing of Iron Ruby is I can have the power of both .NET and Ruby world.

    Make sense?

  • Yes it does... I mean there were loads of things I had to do, shell script/batch files maybe poweshell to comeup with similar things... i even had friends in past who have written custom " hacked" unit test cases (inside which they write code or trigger batch files to integrate with CI servers ), So I suppose this one is by far the most simple/clean and easy way to do all of the above mentioned stuff.

    Great Post....

  • @Varun: Yes Powershell can be an option, but it was never created for running build script, also I am not sure how can define the dependencies among the tasks in Powershell.

    Anyway thanks for the comments.

Comments have been disabled for this content.