ILMerge is a utility from Microsoft Research that combines multiple .NET assemblies into a single assembly. This is convenient when you want to combine your application and its dependencies into a single DLL file, for example, to make deployment and versioning easier.
ILMerge is released as a console application but also exposes an API to allow you to use it in other applications. For example, I see there are some GUI applications to ease the burden of typing in all those command line switches. ILMerge is mysteriously missing from the community collections of MSBuild tasks, such as the SDC Tasks Library and MSBuild Extended Tasks, probably because it is perfectly feasible to invoke the ILMerge executable using the Exec task that is provided with MSBuild.
The goal is to integrate ILMerge into MSBuild, such that it runs automagically every time the project is built (either within Visual Studio, or with MSBuild from the command line).
Unfortunately there are some interesting details to integrate smoothly into the build, such as making sure the task handles incremental builds properly (so that adding ILMerge to one project in a solution doesn’t force a re-build of that entire sub-tree every time you build!)
I’ve not been able to find an adequate pre-canned way to achieve this, but I’ve hacked something together starting from Jomo Fisher’s solution and addressing some of the shortcomings I found along the way.
Hand-edit your MSBuild project (e.g. *.csproj) file to tag the referenced assemblies you’d like to merge with the ILMerge=True metadata, like this:
<Reference Include="DependencyLibrary, Version=126.96.36.199, Culture=neutral, processorArchitecture=MSIL">
(Note that it is not necessary to set CopyLocal=True for the target assemblies.)
Then, define the following targets and properties at the bottom of your MSBuild project (just above the </Project> tag):
<Target Name="AfterBuild" DependsOnTargets="ILMerge" />
<Target Name="ILMerge" Inputs="@(IntermediateAssembly)"
Outputs="@(MainAssembly -> '%(RelativeDir)%(Filename).ILMergeTrigger%(Extension)')">
<CreateItem Include="@(ReferencePath)" Condition="'%(ReferencePath.ILMerge)'=='True'">
<Output TaskParameter="Include" ItemName="ILMergeAssemblies" />
<Exec Command="$(ILMergeExecutable) /Closed /Internalize /Lib:$(OutputPath) /keyfile:$(KeyFile) /out:@(MainAssembly) "@(IntermediateAssembly)" @(ILMergeAssemblies->'"%(FullPath)"', ' ')" />
<!-- Make a copy of the merged output DLL to use as a trigger for incremental builds -->
DestinationFiles="@(MainAssembly -> '%(RelativeDir)%(Filename).ILMergeTrigger%(Extension)')" />
Here's the full wolking solution: ILMergeExperiments
There are a couple of hacks here to deal with the fact that we want our ILMerged assembly to have the same name as the original:
This is somewhat hacky, and I’m sure there must be a more cunning way to integrate into MSBuild; I’ll have to revisit this once I’ve read the book Inside the Microsoft Build Engine: Using MSBuild and Team Foundation Build.
Invoking a batch file from an MSBuild script (such as any *.csproj file) is a snap with the standard Exec build task. However, I recently discovered a little caveat with this, and it took a little digging to get to the bottom of it.
Consider the following MSBuild project file, which invokes a batch script:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">If your batch script does anything even moderately complicated, it is reasonable that you would want failure of the batch script to cause failure of the MSBuild project. You can do this using the handy EXIT /B command to return from your batch script with an error code:
<Exec Command="batch.cmd" />
ECHO Doing stuff
EXIT /B 42
The only problem is that the MSBuild script above will merrily announce that the build succeeded, prompting you to scratch your head a little and wonder.
You might, as I did, try it without the /B switch, and you'd see that it works — MSBuild traps the non-zero return and fails the build. And if you're the sort of person that doesn't ask many questions this might suffice. Only you'd be left with a rather rude batch file.
Let's review the documentation for the EXIT command:
Quits the CMD.EXE program (command interpreter) or the current batch script.
EXIT [/B] [exitCode]
/B specifies to exit the current batch script instead of
CMD.EXE. If executed from outside a batch script, it
will quit CMD.EXE
exitCode specifies a numeric number. if /B is specified, sets
ERRORLEVEL that number. If quitting CMD.EXE, sets the process
exit code with that number.
So the /B stands for "Behave". If you call EXIT 1 in the middle of your batch you immediately and unconditionally cause the command shell to exit with that error code.
So we want to be good, and have our script Behave, so we put back that /B switch, and scratch our head some more about why MSBuild is missing this.
Then we get bored of scratching and bust out .NET Reflector to go to town on the Exec task.
So it turns out that internally, Exec doesn't just call your command directly. Instead it wraps it in a batch script of its own! It generates a batch in your temp directory that looks something like this:
Then it invokes that with CMD /C and quickly deletes the temporary file. I'm not quite sure why it goes through the trouble of this intermediate batch file, to be honest; what extra value does it add? Wouldn't CMD /C batch.cmd give the same outcome?
One thing that it does add is a little silent caveat. If you invoke a batch file from within another batch file, the first batch file will never resume execution unless you use the CALL command to invoke the second batch.
So what is happening here is that, because we had no idea of this undocumented batch file being created in the background by the Exec task, our command of "batch.cmd" is being plonked in the middle of this generated batch file, and then rudely prevents the generated batch from explicitly calling EXIT to return the error code to MSBuild!
The solution? If you're going to call a batch file with the Exec task, prefix it with a "CALL".
<Exec Command="CALL batch.cmd" />
This will ensure that the outer script runs to completion, and exits CMD returning the appropriate errorlevel value.
(Alternatively, you could upgrade to a suitabley recent operating system, and forget everything you've just read. Somewhere between Windows XP and Windows 7, CMD.EXE has acquired the elegance of picking up the latest errorlevel value and using that as its return code, so that even if that last EXIT command isn't executed, it will still pick up the value set by the inner batch script.)