Better method for runtime modification of install folders (msi)

255 Views Asked by At

Context:

So this is a package, that installs an old software. When I say old, think 20 years old. It's huge, we depend on it, we are writing the replacement, but till that is ready we are stuck with this old application for the next few years.

Problem:

The msi is to be pushed via SCCM, and our IT department insists that this means that we cannot have any UI elements in this msi package (like browse for installation path).

The program must be installed at: volume + ":\" + companyabbreviation. The default path is [WindowsVolume]companyabbreviation

We also have to generate a few empty folders for the program to function properly, like "TRANSFER"

To achieve this, without drowning in ICE warnings the wix xml looks like this:

<Directory Id="TARGETDIR" Name="SourceDir">
    <Directory Id="VOLUME_WindowsVolume_companyabbreviation" Name="WindowsVolume_companyabbreviation">
        <Directory Id="Directory_0_1_0" Name="subfolderA">
            <Directory Id="Directory_0_2_1" Name="subsubfolderAA" />
        </Directory>
        <Directory Id="Directory_0_1_0" Name="subfolderB" /> 
        <!-- and a lot more directories -->
        <Directory Id="Directory_0_2_6" Name="TRANSFER" />
    </Directory>
</Directory>

<!-- Component groups -->
    <ComponentGroup Id="FEATUREID_Directory_0_2_6" Directory="VOLUME_WindowsVolume_companyabbreviation">
        <Component Id="INSTALL_Directory_0_2_6" Guid="4a9cd25e-4d66-4398-af52-356a3b48c337">
            <CreateFolder Directory="Directory_0_2_6" />
            <RemoveFile Id="PurgeCacheFolderDirectory_0_2_6" Name="*.*" On="uninstall" />
            <RemoveFolder Id="Directory_0_2_6" On="uninstall" />
        </Component>
    </ComponentGroup>
<!-- more Component Groups, components, features, etc.. -->

<SetDirectory Id="VOLUME_WindowsVolume_companyabbreviation" Value="[WindowsVolume]companyabbreviation" />

This works as intended.

Since this is an old application, (that was never packaged but distributed via a homebrewed installer program), the target machines typically have an "installation" of some sort before they get the msi.

Hence I created a custom action for cleaning up, from the old homebrewed installation. Which also works nicely. We just make sure that it only runs, if trying to install, on a system where nobody have previously installed via msi..

<Binary Id="CustomActionsId" SourceFile="../customActions/CA UPDATE FROM REGISTRY.CA.dll" />
<CustomAction Id="Launch_UPDATE_FROM_REGEDIT" BinaryKey="CustomActionsId" DllEntry="UPDATE_FROM_REGISTRY" Execute="immediate" Return="check" />
<InstallExecuteSequence>
     <Custom Action="Launch_UPDATE_FROM_REGEDIT" Before="CostFinalize">NOT Installed</Custom>
</InstallExecuteSequence>

Now we should be ready for release :)

Except, we have just found out that someone predecessor of mine? decided a long time ago, that they would tell customers how the program could be moved from the default location...

See, the developers could use multiple installations, and simply choose which one to run, by modifying a set of registrykeys..

So basicly a developer made sure that the application can be moved to another drive, to something like: D:\companyabbreviation and via the registry edit which version to start. which TRANSFER folder to use, and so on, and so forth...

Well you may argue, that this requires alot of partitions, but in the time before the company was aware of CVS, SVN, GIT, etc.. well it made sense I guess..

Later another developer saw this feature, as a way to get around some customers lack of administrative rights (so you couldn't install on WindowsVolume Root) and decided that the old install program would respect the registry key when present.. And that if the key was not present, he would make the old installer create it, and set the value to the default paths.

As a consequence, years later, now our MSI, need to respect this too, if any of our users have moved the application... (and we have checked, some do this..)

Our idea is to use the custom action for updating the paths:

We made a helper functions:

    public static ActionResult getCustomActionInfo(Session session, string WhereSourceStringContains, ref List<string> data)
    {
        var CustomActionsActions = session.Database.ExecuteQuery("SELECT Action from CustomAction");
        var CustomActionsTypes = session.Database.ExecuteQuery("SELECT Type from CustomAction");
        var CustomActionsSources = session.Database.ExecuteQuery("SELECT Source from CustomAction");
        var CustomActionsTargets = session.Database.ExecuteQuery("SELECT Target from CustomAction");

        session.Log("Retrieved " + CustomActionsTargets.Count.ToString() + " custom actions");
        int index = -1;
        for (int i = 0; i < CustomActionsTargets.Count; i++)
        {
            if (CustomActionsTypes[i].ToString().Trim() == "51" && CustomActionsSources[i].ToString().Contains(WhereSourceStringContains))
            {
                index = i;
                break;
            }
        }

        if (index == -1)
        {
            session.Log("Error could not locate the " + WhereSourceStringContains + " setDirectory customAction, cannot set a different installation target..");
            return ActionResult.SkipRemainingActions;
        }
        else
        {
            session.Log("row found with index: " + index.ToString() + "\r\n\t [action] => " + CustomActionsActions[index].ToString() + "\r\n\t [type]   => " + CustomActionsTypes[index].ToString() + "\r\n\t [source] => " + CustomActionsSources[index].ToString() + "\r\n\t [target] => " + CustomActionsTargets[index].ToString());
            data.Add(index.ToString());
            data.Add(CustomActionsActions[index].ToString());
            data.Add(CustomActionsTypes[index].ToString());
            data.Add(CustomActionsSources[index].ToString());
            data.Add(CustomActionsTargets[index].ToString());
        }

        return ActionResult.Success;
    }

and:

    public static void setCustomActionTarget(Session session, string ActionData, string TargetData)
    {
        session.Database.Execute("UPDATE CustomAction SET Target='" + TargetData + "' WHERE Action='" + ActionData + "'");
    }

and then this code that makes stuff happen:

[CustomAction]
public static ActionResult UPDATE_FROM_REGISTRY(Session session)
{
    var view32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
    RegistryKey ProductKey = view32.OpenSubKey(@"secretPath\To\Applications\Key");

    /* instDir is the previous location of the folder */
    string instDir = ProductKey.GetValue("instDir ").ToString().Trim();
    session.Log("discovered a instDir  key: " + instDir );
    string BaseDir = instDir .Substring(0, DataDir.ToUpper().LastIndexOf("\\")).ToUpper();

    List<string> PRODUCT = new List<string>();
    ActionResult test = getCustomActionInfo(session, "companyabbreviation", ref PRODUCT );
    if (test != ActionResult.Success) return test;
    string BaseTarget = SABITC[4];
    session.Log("BaseTarget: " + BaseTarget + " == BaseDir: " + BaseDir.Replace(@"C:\", "[WindowsVolume]") + " ?? ");
   if (BaseTarget.ToUpper() == BaseDir.Replace(@"C:\", "[WindowsVolume]").ToUpper())
    {
        session.Log("\tdefault directory was used for SABITC, do nothing to change that :D ");
    }
    else
    {
        session.Log("\tinstall at old location: " + BaseDir);
        setCustomActionTarget(session, SABITC[1], BaseDir);  //here throws exception

        //TODO: update "target" and instdir in msi registry entries, to ensure information is not overwritten by defaults..
     }
}

well apparently you are not allowed to modify the session database, but then how can we achieve this?

I am not interested in creating a property in the wix xml, if I can avoid it. - The reason is that currently the xml is automatically generated, as our developers (mostly engineers) should not have to learn wix, to be able to maintain math code.

If we have to use properties efined in the xml, we would have to somehow let the custom action know what the ID of the directory node is.

Say the customer decided to move only the mentioned TRANSFER folder: it could be made possible like this:

<!-- information for the custom action -->
<Property Id="TRANSFER_ID" Value="Directory_0_2_6"/>
<!-- values below can be overwritten by the custom action:
     but unfortunately also this line also sets default paths,
     and in case of errors, it makes the directory tree above unreliable
     at least for humans that read it in the future.-->
<Property Id="Directory_0_2_6" Value="C:\companyabbrevation\TRANSFER\" />

To make this work we would have to manually modify all those autogenerated wix files, and set the "default" path values to what is implicitly set by the directory tree.. - or make something that parsed the xml, found the correct directory ids, and then added the property nodes.

perhaps if we can dynmically add a property to a MSI on install time, we can still make that work?

summary of requirements for the non-functioning solution:

* change installation folder using information found in registry
* do not overwrite registry information for specific keys, if they are present.
0

There are 0 best solutions below