header image
 

Installing two copies of a single MSI package

Ever had a problem that you can’t install two copies of a MSI installer on the same system? That scenario appeared when I was writing a script that prepared build environment for a certain library. Windows doesn’t have any centralized package management so I needed to take care of getting all dependencies myself (there are 3rd party solutions like Chocolatey but they don’t really help with code libraries).

Some dependencies I could get as a simple archive, but others were only available as MSI installers. I wanted the build environment to be as “sandboxed” as possible so I couldn’t either depend on or interfere with existing software on the system. The ideal scenario was to install my copy of the MSI into selected directory without worrying if there is another copy installed. It’s not that simple though — Windows Installer will refuse to just install another copy, all you can do is repair/reinstall or whatever other options that particular MSI has.

The reason for that behavior is that all MSI packages are identified by a GUID. If you change that GUID to a new one, you will be able to install a second copy because it’ll be something new to Windows Installer. The question is, how to do that?


It’s not particularly hard because we can use Windows Installer’s services exposed as a COM server. Below is a simple code that will patch a MSI file with a GUID supplied as a parameter. It can also set the product name to a supplied value (useful for being able to tell different copies apart).

using System;
using System.IO;
using System.Runtime.InteropServices;
using WindowsInstaller;

// This utility patches a MSI installer package to use new product GUID.
// Reference msi.dll COM wrapper DLL when compiling.

namespace MsiTools
{
    static class MsiPatch
    {
        static int Main(string[] args)
        {
            if (args.Length < 2)
            {
                Console.WriteLine("Usage: msi-patch <msi path> <new GUID (braced form)> [product name]");
                return 2;
            }

            string path = Path.GetFullPath(args[0]);
            string guid = args[1];

            // check if the guid is valid
            try
            {
                Guid g = new Guid(guid);
            }
            catch (FormatException)
            {
                Console.WriteLine("Invalid GUID: {0}", guid);
                return 1;
            }

            // check if it's braced
            if (guid[0] != '{' || guid[guid.Length - 1] != '}')
            {
                Console.WriteLine("GUID must be in braced form (ex. {17201106-ae02-44f1-8d2d-d075a6e5c039})!");
                return 1;
            }

            try
            {
                Type t = Type.GetTypeFromProgID("WindowsInstaller.Installer");
                Installer msi = (Installer)Activator.CreateInstance(t); // instantiate WI COM object
                Database db = msi.OpenDatabase(path, MsiOpenDatabaseMode.msiOpenDatabaseModeTransact); // transact mode, we're making changes

                // update summary info
                SummaryInfo summary = db.get_SummaryInformation(1);
                summary.set_Property(9, guid); // 9 is the product GUID
                summary.Persist();

                // update properties
                string sql = string.Format("UPDATE Property SET Value='{0}' WHERE Property='ProductCode'", guid);
                View view = db.OpenView(sql);
                view.Execute(null);
                view.Close();

                sql = string.Format("UPDATE Property SET Value='{0}' WHERE Property='UpgradeCode'", guid);
                view = db.OpenView(sql);
                view.Execute(null);
                view.Close();

                if (args.Length == 3) // product name
                {
                    sql = string.Format("UPDATE Property SET Value='{0}' WHERE Property='ProductName'", args[2]);
                    view = db.OpenView(sql);
                    view.Execute(null);
                    view.Close();
                }

                db.Commit();
                Marshal.ReleaseComObject(msi);
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception: {0}\n{1}", e.Message, e.StackTrace);
                return 1;
            }
            return 0;
        }
    }
}

In Visual Studio you can just add msi.dll from system directory as a reference and it will automatically generate COM wrapper for you. If you have Windows SDK you can use TlbImp.exe:

TlbImp.exe msi.dll /out:msi-interop.dll /namespace:WindowsInstaller

If you don’t have any of that (running on a clean Windows install for example) you’re still not totally out of luck… but that may be a topic for another post.

Observant reader may notice that I’m using following code to access Windows Installer object’s indexed properties:

SummaryInfo summary = db.get_SummaryInformation(1);
summary.set_Property(9, guid); // 9 is the product GUID

Why not use simple

SummaryInfo summary = db.SummaryInformation[1];
summary.Property[9] = guid; // 9 is the product GUID

instead? Well, the second form will work if you use .NET Framework 3.5 or newer. It fails to compile on 2.0 however and I wanted to be as compatible as possible.

~ by omeg on September 15, 2013.

C#, code, installers, utility

Leave a Reply