Thursday, December 4, 2008

Using ILMerge in real life

There are some notable figures blogging about how you can use ILMerge to merge assemblies in Visual Studio by tweaking the project file. Scott Hanselman wrote a good post in how to merge assemblies generated from multiple languages into a single assembly. Starting out with that post I was trying to get this to work in a real world situation. In the samples provided in various blog posts, including Hanselman's, the assembly being generated with ILMerge was the final result of the build. But what if you need Visual Studio to pick up the modified assembly and use it as a reference for another project in the solution? I ran into a couple of issues but managed to get it working in the end.

The project file
First, setup your project so it builds with all the references it needs. Then to get ILMerge working you need to tweak the project file. Basically we need an AfterBuild target that:

  • Enables the project output to be used as a dependency for other projects in the same solution.
  • Runs ILMerge only when the project output is updated.
  • Maintains the assemblies strong name.
  • Enables unit tests to run on the merged project output.

Here's the code for the MSBuild target:

<Target Name="AfterBuild" 
        Condition="'$(_AssemblyTimestampBeforeCompile)'!='$(_AssemblyTimestampAfterCompile)'">
<CreateItem Include="@(ReferencePath)" 
            Condition="'%(ReferencePath.IlMerge)'=='true'">
  <Output TaskParameter="Include" ItemName="IlmergeAssemblies" />
</CreateItem>
<Message Text="MERGING: @(IlmergeAssemblies->'%(Filename)')" Importance="High" />
  <Exec Command="..\..\..\Tools\ILMerge\ILMerge.exe /internalize /keyfile:"..\..\myapp.snk" /out:Migrate.dll "..\..\@(IntermediateAssembly)" @(IlmergeAssemblies->'"%(FullPath)"', ' ')"       WorkingDirectory="$(MSBuildProjectDirectory)\$(OutputPath)" />
  <Copy SourceFiles="@(MainAssembly)" 
      DestinationFolder="$(IntermediateOutputPath)" 
      SkipUnchangedFiles="true" OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)">
  </Copy>
  <Copy SourceFiles="@(_DebugSymbolsOutputPath)" 
      DestinationFiles="@(_DebugSymbolsIntermediatePath)" SkipUnchangedFiles="true"       OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" 
      Condition="'$(_DebugSymbolsProduced)'=='true' and '$(SkipCopyingSymbolsToOutputDirectory)' != 'true'">
  </Copy>
</Target>

Copy paste the target into the project file.

ILMerge and paths
The code above uses ILMerge from a folder relative to the project. You may have it installed on your system somewhere. If that is the case, set the absolute path to the tool. By default ILMerge works in the current working directory and fortunatly the <Exec> task has a WorkingDirectory setting. Setting that to $(MSBuildProjectDirectory)\$(OutputPath) makes the command operate on the output folder (e.g. bin\Release). All other paths supplied to ILMerge are either relative to that folder or absolute paths.

Marking files to merge in
The code above uses custom metadata (<ilmerge>True</ilmerge>) on the reference to figure out what references to merge into the target assembly. A reference with the ILMerge metadata will look like this:

<Reference Include="Datalayer, Version=2.0.4.2, Culture=neutral, PublicKeyToken=76bf2dedaa68ccb5, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>..\..\..\..\lib\Datalayer.dll</HintPath>
  <IlMerge>True</IlMerge>
  <Private>False</Private>
</Reference>

The Private tag reflects the CopyLocal setting on the reference's properties in Visual Studio. The first part of the task in the first code snippet builds a list of the assemblies marked for inclusion using the ILMerge tag.

Working with Visual Studio
The next problem I encountered is that Visual Studio, or rather the MSBuild targets used underneath, do some unexpected things. First, when the assembly is referenced from another project, it's not picked up from the output folder (bin/release) but from the intermediate folder (obj/release). Merging the assemblies in the intermediate folder is not an option because the source and target assembly (and .pdb) cannot be the same file. The solution is to copy the merged assembly and .pdb back to the intermediate folder after merging using the <Copy> task. To prevent problems with builds that don't update the project output I also added a condition on the target. This causes ILMerge to run only when the assembly is updated. In order for this to work the RunPostBuildEvent needs to be set:

<RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent>
This setting corresponds to the following setting in Visual Studio 2008: Visual Studio 2008 Build Events Tab Make sure you don't change that setting in Visual Studio because it can result in build errors.