Recently, I ran into a need to make use of the Mono.Cecil.dll library from a .net application I was working on. Cecil is a fantastic little project under the Mono project that allows you to interrogate an existing .net assembly (EXE or DLL file) and retrieve all kinds of information about the assembly, from the resources embedded within it to the classes and methods defined, and much more. You can even alter the contents of the assembly and write the new version back out to disk!
The thing is, Cecil comes prebuilt as a C# assembly (a DLL file), and I really did not want to have a second file required for this particular application (it’s just a little command line utility).
My first take was to use Oren Eini’s excellent concept of an assembly locator. My idea here was to embed an assembly as a binary resource, then, when requested, read that resource into a stream, load the assembly from the stream, and resolve references to it via the assembly locator.
I’m still working on that concept, because I think it’s a clever and convenient solution to the problem, but, in researching that, I happened to remember the even easier (as in, no code required at all!) solution of using ILMerge.
ILMerge
If you haven’t already discovered it, ILMerge is a Microsoft research project in the form of a single command line EXE utility, that can “merge” any number of .net assemblies with a “target” assembly, and produce either a .net DLL or EXE output file.
There’s a really good article on CodeProject describing the general use of the program. There’s even a GUI (Gilma) for it.
Automating ILMerge
What the article doesn’t go into is automating the ILMerge process. After all, you won’t want to manually run that command line utility every time you build your application!
The good thing is, it’s trivially easy to automate as long as you take care of one critical step.
- First, download and install ILMerge from the link above.
- Once it’s installed be sure to copy the ILMerge.exe utility to somewhere on your path (or add the folder it’s in to your path).
- Then, grab the Mono.Cecil.dll file (or whatever DLL it is you want to merge into your main project EXE). Put it in the root folder of your project.
- Go ahead and set a reference to that file, so you still get all the .net intellisense goodness, but be sure to set the Copy Local property for the reference to FALSE (after all, you don’t want to copy this DLL to the output folder along with the application if you don’t actually need it, right?)
- With that done, you MAY want to mark the DLL you’re embedding as “included in project” from the Solution Explorer.
If you do this, however, make sure you also set the Copy to Output Folder to “Do not copy” (that’s the default though so it should already be set).
The Tricky Part
One limitation of ILMerge is it that it can’t alter the target assembly “in place”. This means that, for instance, if you want the final resulting executable file name to be called GenerateLineMap (my utility), you can’t have VS compile the assembly to that name. You’ll need to use some other name for the initially compiled assembly.
On the Application tab of the project properties you can change that name, like so:
I just added “-Interim” to the Assembly Name.
Now, go to the compile tab of the project properties and click Build Events
Insert this as the POST BUILD EVENT
ilmerge /target:winexe /out:”$(TargetDir)$(ProjectName)$(TargetExt)” “$(TargetPath)” “$(ProjectDir)Mono.Cecil.dll”
Notice that the OUT parameter specifies the output file name via the $(ProjectName) variable (which is still “GenerateLineMap”) while the first assembly to merge is specified using the $(TargetPath) variable (which is the full filename of the output assembly, including the “-Interim”).
EDIT: Important note: Notice the /target:winexe option. You’ll need to change that as appropriate depending on the type of exe you’re building. In my case, I had a console app where this needed to be set to just /target:exe. Setting it to /target:winexe caused all of the console output functions to just do nothing, which threw me for a bit!
Now, just rebuild the solution and you should see two resulting EXE files in your output folder, one with the “–Interim” (which is the version WITHOUT the embedded Mono.Cecil.DLL file or whatever DLL you’ve chosen) and one WITH the embedded file. As a final cleanup, you may want to add an additional Post build step to remove the “*-Interim” files.
To test, just make sure you DO NOT have the embedded DLL anywhere on the path or in the same folder as the exe and run the exe. If everything went right, your app should work exactly the same as if the embedded DLL was included externally with the app!
Now, your application is back to being a single EXE to distribute, and it was built completely automatically by Visual Studio.
Caveats
You knew there had to be some, right?
The first is that if the dll to embed contains licensing information, it might not work properly after being embedded. I don’t have any DLLs like that to test with, but I’ve read reports about the problem at various sites on the internet. Just something to be aware of.
Second, trying to replace the originally named exe with the newly built one results in VS not being able to debug the exe in the IDE (it throws a message about the assembly manifest being different from what was expected). I haven’t worked that issue out yet. What this means is that you might need to leave any references set to “Copy to Output Folder” while debugging, and not replace the original compiled assembly, just ILMerge it into a new assembly name.