Thursday, May 19, 2011

Adding both 32 and 64 bit versions of DLLs to a Visual Studio 2010 Setup project

I ran into a snag, when trying to create an installer using a Visual Studio 2010 Setup project that would install for 32 and 64 bit versions of Autodesk Revit Architecture. Since my program depends on a DLL (in this case System.Data.SQLite.dll) that ships in two different versions, I wanted to include both in the MSI and then copy the correct one to the installation folder with a CustomAction project. The snag consists of this pesky error message whenever you add a 64 bit dll to a Setup project that is targeting x86:

File 'System.Data.SQLite.dll' of project output 'Content Files from MYPROJECT (Active)' targeting 'AMD64' is not compatible with the project's target platform 'x86'

That was easy to fix! All I had to do was include the file with a different extension, right? So... I renamed System.Data.SQLite.dll to System.Data.SQLite.txt and added it to MYPROJECT as a new content file. Same error when building the setup project:

File 'System.Data.SQLite.txt' of project output 'Content Files from MYPROJECT (Active)' targeting 'AMD64' is not compatible with the project's target platform 'x86'

It seems, the setup project figures out that you have a DLL here, even if the extension doesn't is something totally different. I tried various extensions to no avail.

So, the next logical step would be to use the old email zip file trick, right? It is rather easy to zip a DLL. Any number of tools will do that for you. It turns out to be rather tricky to unzip a DLL in C# inside your CustomAction.

It is possible to use a 3rd party library for unzipping, but I didn't really want to add a dependency just for the installer. Instead, I opted to use the DEFLATE algorithm provided in .NET here: System.IO.Compression.DeflateStream.

Given a file compressed with this algorithm, the code to inflate it back to normal size is trivial:

using (var srcFile = File.OpenRead(@"C:\MYPATH\System.Data.SQLite.x64"))
{
    using (var dstFile = File.OpenWrite(@"C:\MYPATH\System.Data.SQLite.dll"))
    {
        using (var deflator = new DeflateStream(srcFile, CompressionMode.Decompress))
        {
            CopyStream(deflator, dstFile);
        }
    }
}

Just... how to get the original file into the deflated version? I couldn't find a tool that reliably did this for me, so I created my own: http://code.google.com/p/deflate/

To quote from the projects page:

This project provides two simple programs:

deflate.exe inflate.exe They are implented using the System.IO.Compression.DeflateStream provided in .NET.

Usage:

C:\> type MYFILE | deflate.exe > MYFILE.deflated
C:\> type MYFILE.deflated | inflate.exe > MYFILE

I now have the honour to present the steps to create a DLL targeted at a different architecture as your setup:

  1. deflate the file using deflate.exe, naming it with a different extension (e.g. .x64)
  2. add it to your main project as a content file
  3. add a custom action project to your solution
  4. add the custom action to the setup projects "Install" custom actions
  5. inflate the file inside the custom actions Install method using System.IO.Compression.DeflateStream (see code above)
  6. do a little dance around your desk, down the hall, and past as many coworkers as you care to annoy :)

3 comments:

  1. Hi, you can avoid the deep nesting of your "using"´s by writing it as follows:

    using (var srcFile = File.OpenRead(@"C:\MYPATH\System.Data.SQLite.x64"))
    using (var dstFile = File.OpenWrite(@"C:\MYPATH\System.Data.SQLite.dll"))
    using (var deflator = new DeflateStream(srcFile, CompressionMode.Decompress))
    {
    CopyStream(deflator, dstFile);
    }

    Best regards, Lasse Espeholt

    ReplyDelete
  2. @Lasse: I kind of like this. Normally, with `if` statements, I make a point of *never ever* using the single statement form, but I think here it makes a lot of sense. Thanks!

    ReplyDelete
  3. I agree it makes sense here despite my instinctive suspicion. Another example of the single statement form that I think makes sense is simple argument validation:

    private void Example1(string inputText)
    {
    if (inputText == null)
    throw new ArgumentNullException("inputText");

    ...
    }

    ReplyDelete