Wednesday, August 24, 2005

Ruby C# Project File parser (CSProjFile)

In response to my blogs about using rake with .net and how to add an embedded resource, Jeremy Miller asks:

Isn't there an equivalent to the "solution" task in NAnt for Rake?
I don't see how it would be better if I had to manually specify things
like including resource files in the build script.

As far as I know, there is no built in support for .net in rake. However, a .csproj file is just an XML file and should therefore be easy to parse.

I've never done any XML parsing in Ruby so I contacted Jeremy Stell-Smith for a recommendation. His answer was REXML. REXML must have come standard in my version of Ruby because I didn't need to install anything. After about a minute looking at the tutorial I was set.

Of course, being test-driven, I started out with the tests. I didn't feel like creating a Foo.csproj, so I used the NanoContainer.csproj to create a XML string I would use for testing.
require 'test/unit'
require 'csprojfile'

class CSProjFileTest < Test::Unit::TestCase
def CSProjFileTest.NanoConfig
%q!
<VisualStudioProject>
<CSHARP
ProjectType = "Local"
ProductVersion = "7.10.3077"
SchemaVersion = "2.0"
ProjectGuid = "{10C07279-0C4B-49AC-8DA5-54062116C2ED}"
>
<Build>
<Settings
ApplicationIcon = ""
AssemblyKeyContainerName = ""
AssemblyName = "NanoContainer"
AssemblyOriginatorKeyFile = ""
DefaultClientScript = "JScript"
DefaultHTMLPageLayout = "Grid"
DefaultTargetSchema = "IE50"
DelaySign = "false"
OutputType = "Library"
PreBuildEvent = ""
PostBuildEvent = ""
RootNamespace = "NanoContainer"
RunPostBuildEvent = "OnBuildSuccess"
StartupObject = ""
>
<Config
Name = "Debug"
AllowUnsafeBlocks = "false"
BaseAddress = "285212672"
CheckForOverflowUnderflow = "false"
ConfigurationOverrideFile = ""
DefineConstants = "DEBUG;TRACE"
DocumentationFile = ""
DebugSymbols = "true"
FileAlignment = "4096"
IncrementalBuild = "false"
NoStdLib = "false"
NoWarn = ""
Optimize = "false"
OutputPath = "bin\Debug\"
RegisterForComInterop = "false"
RemoveIntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "4"
/>
<Config
Name = "Release"
AllowUnsafeBlocks = "false"
BaseAddress = "285212672"
CheckForOverflowUnderflow = "false"
ConfigurationOverrideFile = ""
DefineConstants = "TRACE"
DocumentationFile = ""
DebugSymbols = "false"
FileAlignment = "4096"
IncrementalBuild = "false"
NoStdLib = "false"
NoWarn = ""
Optimize = "true"
OutputPath = "bin\Release\"
RegisterForComInterop = "false"
RemoveIntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "4"
/>
</Settings>
<References>
<Reference
Name = "System"
AssemblyName = "System"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.dll"
/>
<Reference
Name = "System.Data"
AssemblyName = "System.Data"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.Data.dll"
/>
<Reference
Name = "System.XML"
AssemblyName = "System.Xml"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.XML.dll"
/>
<Reference
Name = "PicoContainer"
AssemblyName = "PicoContainer"
HintPath = "..\..\lib\PicoContainer.dll"
/>
<Reference
Name = "Microsoft.JScript"
AssemblyName = "Microsoft.JScript"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\Microsoft.JScript.dll"
/>
<Reference
Name = "VJSharpCodeProvider"
AssemblyName = "VJSharpCodeProvider"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\VJSharpCodeProvider.DLL"
/>
</References>
</Build>
<Files>
<Include>
<File
RelPath = "AssemblyInfo.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "DefaultNanoContainer.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\AssemblyUtil.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\AttributeBasedContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\ComponentAdapterType.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\DependencyInjectionType.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\RegisterWithContainerAttribute.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "IntegrationKit\ContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "IntegrationKit\LifeCycleContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "IntegrationKit\PicoCompositionException.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\AbstractFrameworkContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\FrameworkCompiler.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\ScriptedContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\CSharp\CSharpBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\JS\JSBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\JSharp\JSharpBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\VB\VBBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\ComposeMethodBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\Constants.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\ContainerStatementBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\XmlContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "TestScripts\test.js"
BuildAction = "EmbeddedResource"
/>
<File
RelPath = "TestScripts\test.vb"
BuildAction = "EmbeddedResource"
/>
</Include>
</Files>
</CSHARP>
</VisualStudioProject>!
end

def testOutputType
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
assert_equal(projFile.output_type, "library")
end

def testAssemblyName
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
assert_equal(projFile.assembly_name, "NanoContainer")
end

def testFiles
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedFiles = %w(AssemblyInfo.cs DefaultNanoContainer.cs Attributes\\AssemblyUtil.cs Attributes\\AttributeBasedContainerBuilder.cs Attributes\\ComponentAdapterType.cs Attributes\\DependencyInjectionType.cs Attributes\\RegisterWithContainerAttribute.cs IntegrationKit\\ContainerBuilder.cs IntegrationKit\\LifeCycleContainerBuilder.cs IntegrationKit\\PicoCompositionException.cs Script\\AbstractFrameworkContainerBuilder.cs Script\\FrameworkCompiler.cs Script\\ScriptedContainerBuilder.cs Script\\CSharp\\CSharpBuilder.cs Script\\JS\\JSBuilder.cs Script\\JSharp\\JSharpBuilder.cs Script\\VB\\VBBuilder.cs Script\\Xml\\ComposeMethodBuilder.cs Script\\Xml\\Constants.cs Script\\Xml\\ContainerStatementBuilder.cs Script\\Xml\\XmlContainerBuilder.cs)
assert_equal(projFile.files, expectedFiles)
end

def testReferences
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedRefs = %w(System System.Data System.XML PicoContainer Microsoft.JScript VJSharpCodeProvider)
assert_equal(projFile.references, expectedRefs)
end

def testCscOutput
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedCsc = "csc /out:../build/NanoContainer.dll /target:library /lib:../build /r:'System.dll;System.Data.dll;System.XML.dll;PicoContainer.dll;Microsoft.JScript.dll;VJSharpCodeProvider.dll' /res:'TestScripts\\test.js,NanoContainer.TestScripts.test.js' /res:'TestScripts\\test.vb,NanoContainer.TestScripts.test.vb' AssemblyInfo.cs DefaultNanoContainer.cs /recurse:AssemblyUtil.cs /recurse:AttributeBasedContainerBuilder.cs /recurse:ComponentAdapterType.cs /recurse:DependencyInjectionType.cs /recurse:RegisterWithContainerAttribute.cs /recurse:ContainerBuilder.cs /recurse:LifeCycleContainerBuilder.cs /recurse:PicoCompositionException.cs /recurse:AbstractFrameworkContainerBuilder.cs /recurse:FrameworkCompiler.cs /recurse:ScriptedContainerBuilder.cs /recurse:CSharpBuilder.cs /recurse:JSBuilder.cs /recurse:JSharpBuilder.cs /recurse:VBBuilder.cs /recurse:ComposeMethodBuilder.cs /recurse:Constants.cs /recurse:ContainerStatementBuilder.cs /recurse:XmlContainerBuilder.cs"
assert_equal(projFile.create_csc("../build"), expectedCsc)
end

def testEmbeddedResource
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedResources = %w(TestScripts\test.js TestScripts\test.vb)
assert_equal(projFile.embedded_resources, expectedResources)
end

def testConvertToResource
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
assert_equal(projFile.convert_to_resource("a\\b.c"),"/res:'a\\b.c,NanoContainer.a.b.c'")
end
end
After getting those tests to pass I felt like my CSProjFile class was well enough tested.
require 'rexml/document'

class CSProjFile
def initialize(projFile)
@projXml = REXML::Document.new projFile
@extensions = {"library"=>"dll"}
end

def output_type
@projXml.elements["VisualStudioProject/CSHARP/Build/Settings"].attributes["OutputType"].downcase
end

def assembly_name
@projXml.elements["VisualStudioProject/CSHARP/Build/Settings"].attributes["AssemblyName"]
end

def files
result = []
path = "VisualStudioProject/CSHARP/Files/Include/File"
@projXml.elements.each(path) { |element| result << element.attributes["RelPath"] if compiledFile?(element) }
result
end

def compiledFile?(element)
element.attributes["BuildAction"]=="Compile"
end

def embedded_resources
result = []
path = "VisualStudioProject/CSHARP/Files/Include/File"
@projXml.elements.each(path) { |element| result << element.attributes["RelPath"] if embeddedResourceFile?(element) }
result
end

def embeddedResourceFile?(element)
element.attributes["BuildAction"]=="EmbeddedResource"
end

def references
result = []
@projXml.elements.each("VisualStudioProject/CSHARP/Build/References/Reference") { |element| result << element.attributes["Name"] }
result
end

def create_csc(outDir)
result = []
result << "csc"
result << "/out:#{outDir}/#{assembly_name}.#{@extensions[output_type]}"
result << "/target:#{output_type}"
result << "/lib:#{outDir}"
refs = references.collect {|ref| ref + ".dll"}
result << "/r:'#{refs.join(";")}'"
embeddedFiles = embedded_resources.collect { |item| convert_to_resource(item)}
result << embeddedFiles.join(" ")
recurseFiles = files.each { |item| item.sub!(/.+\\/,"/recurse:") }
result << recurseFiles.join(" ")
result.join(" ")
end

def convert_to_resource(item)
file = item.to_s
id = "#{assembly_name}.#{item.to_s.sub(/\\/,".")}"
"/res:'#{file},#{id}'"
end
end
So, the final (successful) test was changing my NanoContainer rakefile to use the CSProjFile class.
require 'CSProjFile.rb'

buildDir = "src/build"
nanoDll = "build/NanoContainer.dll"
nanoTestsDll = "build/NanoContainer.Tests.dll"
testCompDll = "build/TestComp.dll"
testComp2Dll = "build/TestComp2.dll"
notStartableDll = "build/NotStartable.dll"
nanoTestVsBinDir = "src/NanoContainer.Tests/bin"
nanoTestVsBinDebugDir = "src/NanoContainer.Tests/bin/Debug"
testCompBinDir = "src/TestComp/bin"
testCompBinDebugDir = "src/TestComp/bin/Debug"

task :default => [:compileInit, :compile, :test]
task :all => [:clear, :removeBuildDir, :removeVsDirs, :default]

task :clear do sh "clear" end

task :removeBuildDir => :clear do rm_rf(buildDir) end
task :removeVsDirs => :clear do
rm_rf(nanoTestVsBinDir)
rm_rf(testCompBinDir)
end

directory buildDir

task :compileInit => [buildDir] do
copyToDir(%w(lib/NUnit.Framework.dll lib/PicoContainer.dll lib/Castle.DynamicProxy.dll),buildDir)
copyToDir(%w(lib/NMock.dll lib/NUnit.Framework.dll),buildDir)
end

file nanoDll => :compileInit do compileProj("src/NanoContainer","NanoContainer.csproj") end
file nanoTestsDll => [:compileInit, nanoDll] do compileProj("src/NanoContainer.Tests","NanoContainer.Tests.csproj") end
file testCompDll => :compileInit do compileProj("src/TestComp","TestComp.csproj") end
file testComp2Dll => [:compileInit, testCompDll] do compileProj("src/TestComp2","TestComp2.csproj") end
file notStartableDll => [:compileInit, testCompDll] do compileProj("src/NotStartable","NotStartable.csproj") end
task :compile => [nanoDll,nanoTestsDll,testCompDll,testComp2Dll,notStartableDll]

directory testCompBinDir
directory testCompBinDebugDir
directory nanoTestVsBinDir
directory nanoTestVsBinDebugDir

task :testInit => [testCompBinDir,testCompBinDebugDir,nanoTestVsBinDir,nanoTestVsBinDebugDir] do
cp "src/Build/TestComp.dll", testCompBinDebugDir
copyToDir(%w(src/Build/NMock.dll src/Build/PicoContainer.dll src/Build/Castle.DynamicProxy.dll),nanoTestVsBinDebugDir)
copyToDir(%w(src/Build/NUnit.Framework.dll src/Build/NanoContainer.dll src/Build/NanoContainer.Tests.dll),nanoTestVsBinDebugDir)
end

task :test => [:compile,:testInit] do
cd nanoTestVsBinDebugDir
sh "../../../../lib/nunit-console.exe NanoContainer.Tests.dll"
end

def copyToDir(fileArray, outputDir)
fileArray.each { |file| cp file, outputDir }
end

def compileProj(workingDir, projFileName)
cd workingDir
projFile = CSProjFile.new(File.new(projFileName))
sh projFile.create_csc("../build")
cd "../.."
end


There's much room for improvement, but it work well for a first attempt. Feedback welcome.
Post a Comment