Saturday, December 19, 2009

Hosting IronRuby in MSBuild

This post builds on earlier post in this series and provides a mechanism to host IronRuby in MSBuild.

Basics of MSBuild

A very brief introduction of MSBuild is present here. MSBuild is a modern build file format built on XML technologies.
Important components of MSBuild are

  1. Project
  2. Target
  3. Task
  4. ItemGroup
  5. PropertyGroup
  6. Imports

Project stands as root element with a default namespace declaration. Find declaration in following example

<Project DefaultTargets = "HelloMSBuild"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >

In this DefaultTargets attribute is of interest. Target is the set of actions that msbuild will perform against the project by default.

To understand better let us create a sample project by name “test.msproj”.

1) Create a new text file

2) Add Project element as shown above

3) Add target ‘HelloMSBuild’ as shown below

<Project DefaultTargets = "SayHello"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >

<Target Name="SayHello">
<Message Text="Hello MSBuild" />
</Target>

</Project>

From visual studio command prompt run “msbuild test.msproj”. That is the simplest helloworld with MSBuild where target executes only task “Message”. “Message” is a built-in task. Built-in tasks are included by default. Thus we don’t find any declaration about where Message task is defined.

<Project DefaultTargets ="Lab02"

xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >

<ItemGroup>

<
Items Include="Item1" />

<Items
Include="Item2" />

</
ItemGroup>


<
PropertyGroup>

<
PropertyA>Property1</PropertyA>

</
PropertyGroup>

<
TargetName="Lab02">


<
Message Text="$(PropertyA)"/>

<
Message Text="@(Items)"/>


</Target>

</
Project>

That gives a basic idea about using items and properties. A project can contain more than on ItemGroup elements. All items in ItemGroup must be of same type. In this case they are of “Items”. But a different ItemGroup can contain different kind of elements. We can only refer to items as a group. In this case we referred them @(Items). But properties can be referred individually. Actually we can refer to properties individually only. Format to access a property is $(PropertyName).

So far we have see built-in task “Message”. Now let us see how to build a custom task

Getting started with Custom Task

Here I describe the procedure to build a custom task by name SimpleTask. This task does nothing fancy, but this exercise provides some insight about working with custom tasks. Look at MyTasks01.dll custom task. And we are using UsingTask option to refer to custom task.

<Project DefaultTargets = "CreateDirectory"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
<
UsingTask AssemblyFile="D:\MsBuildTasks\MyTask01.dll" TaskName="SimpleTask"/>

<
ItemGroup>
<
Param Include="Param1" />
<
Param Include="Param2" />
</
ItemGroup>

<
PropertyGroup>
<
Param3>Param3</Param3>
</
PropertyGroup>

<
PropertyGroup>
<
Status />
</
PropertyGroup>


<
Target Name ="CreateDirectory">

<
SimpleTask MyProperty ="@(Param)" >
<
Output TaskParameter="MyMessage" PropertyName="Status"/>
</
SimpleTask>

<
Message Text="$(Status)"/>

<
SimpleTask MyProperty ="$(Param3)" >
<
Output TaskParameter="MyMessage" PropertyName="Status"/>
</
SimpleTask>
<
Message Text="$(Status)"/>

</
Target>

</
Project>

Alternative approach for UsingTask is importing external project as a whole. Sometimes this would be useful as the task needs more specific parameters or item groups. In such case all those parameters and item groups can be defined in external project and imported as a whole. To do this create a file by name “MyTasks.Targets” in same directory as task MyTask01.dll.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<
UsingTask AssemblyFile="D:\MsBuildTasks\MyTask01.dll" TaskName="SimpleTask"/>
</
Project>

And to import this file in project file replace UsingTask element with

<Import Project="D:\MsBuildTasks\MyTasks.Targets"/>

Building Custom Task

Now let us see how to write a custom task. Create a library and refer to libraries (You can choose version 2.0 or 3.5)


  1. Microsoft.Build.Framework
  2. Microsoft.Build.Utilities

Then update class file as shown below

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace MyTasks
{
public class SimpleTask : Task
{
public override bool Execute()
{
//Parameters are semi column seperated values
string[] parameters = myProperty.Split(';');
foreach (string paramter in parameters)
myMessage += paramter + " - Done\n";
return true;
}

private string myProperty;
public string MyProperty
{
get { return myProperty; }
set { myProperty = value; }
}

private string myMessage;
[Output]
public string MyMessage
{
get { return myMessage; }
set { myMessage = value; }
}
}
}


Here observe that we added ‘Output’ attribute to MyMessage. And that is taken into status property in output element of SimpleTask. Then the value is printed through Message task.

With this background about MSBuild, we can move to hosting IronRuby in MSBuild source.

IronRuby Task

This part creates a new task to host IronRuby Task.

Create a new class library. Add references to MSBuild libraries and IronRuby libraries which include

IronRuby, IronRuby.Libraries, Microsoft.Build.Framework, Microsoft.Build.Utilities.v3.5, Microsoft.Dynamic, Microsoft.Scripting, Microsoft.Scripting.Core.

Then replace source contents with following

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using IronRuby;
using IronRuby.Runtime;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;

namespace ABB.INCRC.MSBuildTasks
{
public class IronRubyTask : Task
{
#region Properties

string script;

[Required]
public string Script
{

get { return script; }

set { script = value; }

}

string status;
[Output]
public string Status
{
get { return status; }
}


#endregion
public override bool
Execute()
{
try
{
ScriptEngine engine = Ruby.CreateEngine();

ScriptSource source = engine.CreateScriptSourceFromString(script, SourceCodeKind.Statements);

Object result = source.Execute();

status = result.ToString();
}
catch (Microsoft.Scripting.SyntaxErrorException syntaxError)
{
status = "SyntaxErrorException : " + syntaxError.Message;
return false;
}
catch (MemberAccessException maex)
{
status = "MemberAccessException : " + maex.Message;
return false;
}
return true;
}
}
}

Build this library and copy to folder like D:\MsBuildTasks\IronRubyTask. Note that execution of this task depends on referred libraries. Thus you need to copy all libraries to this folder. In last post we have seen how to refer a task with UsingTask element.

Here we take a better approach by using an external task definition.

External Project Reference

Create a new file by name MyMSBuildTasks and place following content. This script does lot of work


  • Makes a reference to IronRubyTask
  • Creates property group to hold script elements, which increment AssemblyFileVersion. This is done by updating AssemblyInfo.cs file in properties folder. BuildNumber is persisted in a separate BuildNumber.txt file
  • Creates tasks to execute scripts
  • Scripts are concatenated from multiple properties thereby keeping scripts modular
<?xml version="1.0" encoding="utf-8"?>
<
Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<
UsingTask AssemblyFile="D:\MsBuildTasks\IronRubyTask\IronRubyTask.dll" TaskName="IronRubyTask"/>

<
PropertyGroup>
<
GetBuild>
<![CDATA[
#Get Build

def getBuild(buildIndexFile)

begin

file = File.new(buildIndexFile, "r")

while (line = file.gets)
@token = line.split("=")
if (@token[0] == "build")
@build = Integer(@token[1])
end
if (@token[0] == "revision")
@revision = Integer(@token[1])
end
end

file.close

rescue => err

err
end

end

@build = -1
@revision = -1

getBuild("D:\\BuildNumber.txt")

]]>
</
GetBuild>

<
SetBuild>
<![CDATA[

#Set Build
def setBuild(buildIndexFile)
begin
buildIndex = "build=#{@build}\r\nrevision=#{@revision}"
File.open(buildIndexFile,"w") {|f| f.write(buildIndex) }

"Updated : #{buildIndexFile}"

rescue => err
err
end
end
]]>
</
SetBuild>

<
DefineFileVersion>
<![CDATA[

#update AssemblyVersion

def updateFileVersion(assemblyInfoFile)


if(@build > 65536)
@build = 0
end

if(@revision > 65536)
@revision = 0
end


assemblyInfo = File.read(assemblyInfoFile)

assemblyInfo = assemblyInfo.gsub(/AssemblyFileVersion\(\"1\.0\.(\d+)\.(\d+)\"\)/,
"AssemblyFileVersion(\"1.0.#{@build}.#{@revision}\")")
#puts assemblyInfo

begin
File.open(assemblyInfoFile,"w") {|f| f.write(assemblyInfo) }
"Updated : #{assemblyInfoFile}"
rescue => err
puts err
err
end


end
]]>
</
DefineFileVersion>

<
UpdateFileVersion>
<![CDATA[

#Execution Starts here

if @revision > 0
@revision = @revision + 1
else
@build = @build + 1
end

updateFileVersion("D:\\MsBuildTasks\\Properties\\AssemblyInfo.cs")

]]>
</
UpdateFileVersion>


<
SetBuildNumber>
<![CDATA[

#Execution Starts here

@build = @build + 1
@revision = 0

setBuild("D:\\BuildNumber.txt")

]]>
</
SetBuildNumber>

<
SetRevisionNumber>
<![CDATA[

#Execution Starts here

@revision = @revision + 1

setBuild("D:\\BuildNumber.txt")

]]>
</
SetRevisionNumber>

</
PropertyGroup>

<
Target Name="UpdateFileVersion">
<
IronRubyTask Script="$(GetBuild)$(DefineFileVersion)$(UpdateFileVersion)">
<
Output TaskParameter="Status" PropertyName="Status" />
</
IronRubyTask>
<
Message Text="$(Status)" />
</
Target>

<
Target Name="SetBuildNumber">
<
IronRubyTask Script="$(GetBuild)$(SetBuild)$(SetBuildNumber)">
<
Output TaskParameter="Status" PropertyName="Status" />
</
IronRubyTask>
<
Message Text="$(Status)" />
</
Target>

<
Target Name="SetRevisionNumber">
<
IronRubyTask Script="$(GetBuild)$(SetBuild)$(SetRevisionNumber)">
<
Output TaskParameter="Status" PropertyName="Status" />
</
IronRubyTask>
<
Message Text="$(Status)" />
</
Target>

</
Project>

Referring target file

Now create a test project file and refer above file.

<Project DefaultTargets = "Test"

xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >

<
Import Project="D:\MsBuildTasks\ABB.INCRC.MSBuildTasks"/>

<
Target Name="Test" DependsOnTargets="UpdateFileVersion;SetBuildNumber;SetRevisionNumber" />

</
Project>

Here we use “DependsOnTargets” to call multiple targets from referred file.

Hope this post provided a use-case to host dynamic languages like IronRuby for useful tasks. Advantage of this approach is you need to build only one custom task. All other needs can be achieved using scripts. And scripts are part of msbuild project in the form of properties.

No comments: