Thursday, July 30, 2009

Custom NAnt ILMerge Task. No Assembly Required

ILMerge is a Microsoft utility that enables you to incorporate referenced .NET assemblies into a combined assembly, removing the need to distribute external assemblies. If you are using ILMerge, and you have an automated build process using NAnt, you have several options for integrating ILMerge into your build:

1. You can you use the <exec> task and build your command line arguments as a string.

2. You can use a 3rd party (or write your own) custom tasks library like this one: http://code.google.com/p/ilmerge-tasks/wiki/HowToUse

3. Or you can write a inline custom task using the <script> tag in your NAnt script


I started with option #1. I quickly realized that without <fileSet> support, this could become a difficult option to maintain.

I then moved to option #2. This library didn’t work for me. I suspect it’s because my \tools folder, which contains ILMerge.exe and the ILMergeTask.dll are both on an R:\. And I suspect this is troublesome for the .net reference search. I get back an error that ILMerge was not found.

Which left me with option #3. NAnt presents a <script> tag which allows you to write extensions to core functionality. So I wrote my own ILMergeTask. The thing to note here is that I could have compiled this into a task library as well. The real difference verse option #2 is the ILMergeTask I wrote extends ExternalProgram (as opposed to Task). Which allows me to pass in the path of ILMerge.exe.

Usage is pretty familiar:

<ilmerge outputfile="Combined.Assembly.exe"
program=”R:\tools\ilmerge\ilmerge.exe”
primary=”Primary.Assembly.exe”
log=”${log.dir}\ilmerge.log”>
<assemblies>
<include name="*.dll" />
</assemblies>
</ilmerge>






And here is the code for the custom task:
  <script language="C#" prefix="custom" >
<references>
<include name="System.dll" />
<include name="NAnt.Core.dll" />
</references>
<imports>
<import namespace="System" />
<import namespace="System.Collections" />
<import namespace="System.Collections.Specialized" />
<import namespace="NAnt.Core.Types" />
<import namespace="NAnt.Core.Util" />
<import namespace="NAnt.Core.Tasks" />
</imports>

<code>
<![CDATA[
[TaskName("ilmerge")]
public class ILMergeTask : ExternalProgramBase {

private FileSet m_assemblies;
private string m_logFile;
private string m_outputFile;
private string m_primaryFile;


[TaskAttribute("program", Required = true)]
[StringValidator(AllowEmpty = false)]
public override string ExeName
{
get { return base.ExeName; }
set { base.ExeName = value; }
}


public override string ProgramArguments
{
get { return string.Empty; }
}

[BuildElement("assemblies", Required=true)]
public virtual FileSet InputAssemblies
{
get
{
return this.m_assemblies;
}
set
{
this.m_assemblies = value;
}
}

[TaskAttribute("logfile")]
public virtual string LogFile
{
get
{
if (this.m_logFile == null)
{
return null;
}
return this.Project.GetFullPath(this.m_logFile);
}
set
{
this.m_logFile = StringUtils.ConvertEmptyToNull(value);
}
}

[TaskAttribute("primary", Required=true), StringValidator(AllowEmpty=false)]
public virtual string PrimaryFile
{
get
{
if (this.m_primaryFile == null)
{
return null;
}
return this.Project.GetFullPath(this.m_primaryFile);
}
set
{
this.m_primaryFile = StringUtils.ConvertEmptyToNull(value);
}
}

[TaskAttribute("outputfile", Required=true), StringValidator(AllowEmpty=false)]
public virtual string OutputFile
{
get
{
if (this.m_outputFile == null)
{
return null;
}
return this.Project.GetFullPath(this.m_outputFile);
}
set
{
this.m_outputFile = StringUtils.ConvertEmptyToNull(value);
}
}


protected override void ExecuteTask()
{
try
{
Log(Level.Info, "Executing ILMerge.exe");
Log(Level.Info, string.Format("/out:\"{0}\"", m_outputFile));
Log(Level.Info, string.Format("/log:\"{0}\"", m_logFile));
Arguments.Add(new Argument(string.Format("/out:\"{0}\"", m_outputFile)));

Log(Level.Info, string.Format("assembly[{0}]: {1}", "primary", m_primaryFile));
Arguments.Add(new Argument(string.Format("\"{0}\"", m_primaryFile)));

for (int i = 0; i < m_assemblies.FileNames.Count; i++)
{
Log(Level.Info, string.Format("assembly[{0}]: {1}", i, m_assemblies.FileNames[i]));
Arguments.Add(new Argument(string.Format("\"{0}\"", m_assemblies.FileNames[i])));
}

Arguments.Add(new Argument(string.Format("/log:\"{0}\"", m_logFile)));

base.FailOnError = false;
base.ExecuteTask();
}
catch (Exception ex)
{
throw new BuildException(string.Format("Error executing ILMerge {0}", "test"), Location, ex);
}
}
}
]]>
</code>
</script>




Big kudos to the guy on google code and the guy that wrote reflector ;-)

1 comment:

  1. Hi,

    Thank you for the post. I have extended your sample slightly to achieve what I needed for my purpose. Here's the update: http://weblogs.asp.net/sfeldman/archive/2009/11/11/ilmerge-as-nant-task.aspx

    Sean

    ReplyDelete