Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatically building an MSI

I would like to create a C# program that creates an MSI based on a number of parameters. For example, based on user settings, certain files would be included, or runtime parameters set.

Can anyone point me towards any documentation that might help, or give me an idea where I might start with something like this?

like image 354
Paul Michaels Avatar asked May 24 '10 17:05

Paul Michaels


1 Answers

I have been using WixSharp (WiX#) for just the reasons you describe. I found the XML-driven installer too ugly and unintuitive to deal with, i.e., editing XML files as a way of building an installer. I dug into the C#-based WiX# as a more approachable alternative. In my case, I needed to produce an MSI to install a .NET dll (not in GAC) on a server and then register it for COM Interop.

More info at the CodePlex home page for Wix#: http://wixsharp.codeplex.com/

For the type of installation the OP describes, the MSI/WiX concept of "Features" would seeem to apply, e.g., where the user can check a box to install or omit certain features (A "feature" can be a set of files/programs, a set of registry entries, etc.)

In the WiX# installer, you declare a "Feature"with an "ID", e.g., binaries, docs, registryEntries, etc. at the top of the C# installer code, and then reference that Feature ID when you declare additional installer components. (see the "AllInOne" Wix# code in the "Samples" folder that comes with the Wix# install),

        Feature binaries = new Feature("MyApp Binaries");
        Feature docs = new Feature("MyApp Documentation");
                    ...
                    new File(binaries, @"AppFiles\MyApp.exe",
                        new FileShortcut(binaries, "MyApp", @"%ProgramMenu%\My Company\My Product"),
                        new FileShortcut(binaries, "MyApp", @"%Desktop%")),

Note how the Feature ID "binaries" is the first parameter in declaring the File and FileShortcut components, i.e., "new File(binaries, ...) and links these components to be included in the Feature ID "binaries".

Also, MSI installers allow you to specify feature ID's on the command line, e.g.,

msiexec /i install.msi ADDLOCAL=binaries

Also see this post: WIX: How to Select Features From Command Line

While WiX has ways of looking at the target system and determining what to install, I haven't seen Wix# examples that seem intuitive, with the potential exception of using Custom Actions (which Wix# supports.) One way to adapt an installer's behavior to the target system would be use Custom Actions to set a property, like in the Wix# sample, "Conditional Installation" in the Wix# deployment.

                //setting property to be used in install condition
                new Property("INSTALLDESKTOPSHORTCUT", "no"),
                new ManagedAction(@"MyAction", Return.ignore, When.Before, Step.LaunchConditions, Condition.NOT_Installed, Sequence.InstallUISequence));

Which links back to this code earlier in the Wix# installer:

                 new Dir(@"%Desktop%",
                    new ExeFileShortcut("MyApp", "[INSTALLDIR]MyApp.exe", "")
                    {
                        Condition = new Condition("INSTALLDESKTOPSHORTCUT=\"yes\"") //property based condition
                    }),

Note that "Condition" in curly braces that is set to test the property "INSTALLDESKTOPSHORTCUT" for the value "yes", which gets set as the result of a Custom Action. The C# code for the custom action looks like the example below.

public class CustomActions
{
    [CustomAction]
    public static ActionResult MyAction(Session session)
    {
        if (DialogResult.Yes == MessageBox.Show("Do you want to install desktop shortcut", "Installation", MessageBoxButtons.YesNo))
            session["INSTALLDESKTOPSHORTCUT"] = "yes";

        return ActionResult.Success;
    }
}

Another way, for C# skillset, would be to front-end the msi installer with a C# program that does something like this:

  • Look at the target system user settings, determines which features are appropriate to install on that system.
  • Assemble & format the msiexec command line and feature parameters for selected features.
  • Execute the MSIExec command line.

There are probably other "neater" ways to do this, e.g., involving custom actions, etc. However, coming from a C# skill set, this way is approachable and can help with the Windows Installer/WiX learning curve. What I've found, as I've worked on Wix# installers, is that it's helped me incrementally learn some WiX and Windows Installer along the way. I often look for how to do a certain deployment task in WiX, and then work on translating the Wix way to Wix#, to get Wix# to generate the desired XML in the .wxs file it produces.

In those cases where I can't figure out how to get Wix# to do a certain deployment task, I can often use the crutch of simply including the WiX XML statements in the .wxs file generated by Wix#, e.g., by manually editing the .wxs file.

For example, WiX and Wix#, the way to supply a specific drive and path that is not one of the "supported" destinations is not intuitive. I found that adding this xml snippet to the generated .wxs file from Wix#, and then manually running Candle and then Light, did the trick for me.

I recall running ran across some examples of how to get the Wix# C# code to programmatically include XML in the .wxs statements, and then compile the .wxs to generate the msi. I will update this answer if I find that Wix# example again.

Update I did find the example code... there are a couple in the Wix# code examples in the Wix# examples folder included with Wix#. However...they didn't work the way I wanted them to, and involved additional Wix# classes and objects I wasn't familiar with yet. I wanted to work with the Wix# output as XML text, using "standard" XML C# toolsets, not Wix#-specific classes. So, I tried parsing the .wxs document XML, and was unsuccessful at using XPath to navigate to where I wanted to insert additional WiX XML statements. (Maybe I'll post an SO question for help with that). However, I did accomplish what I wanted by using text substitution of the WiX XML, treated as a text string. Below is what I got to work successfully.

What the code does is assign a delegate to the Compiler.WixSourceSaved event, and this event gets fired as part of what occurs when the "Compiler.BuildMsi" method is invoked in Wix# code. After reading the .wxs file that gets created (but before the MSI file has been built from the wxs), I update the text in the file to contain the XML line(s) I want to include.

The underlying Wix# sequence of events then continues and builds the modified wxs into the final MSI.

 // ...
internal class Script
    static string myWIX_SET_DIRECTORY_STATEMENT = "    <SetDirectory Id=\"INSTALLDIR\" Value=\"D:\\Program Files\\DOL\\WA.DOL.HQSYS.ExecECL\" />";
    static string myWIX_INSERT_AFTER_TEXT = "    <InstallExecuteSequence >";
    public static void Main()
    {
    // ... (your other Wix# code goes here...)
        // Hook an event to Wix# save of .wxs file to post-process the .wxs
        Compiler.WixSourceSaved += PostProcessWxsXMLOutput;

        // Trigger the MSI file build
        Compiler.BuildMsi(project);
    }

    /// <summary>
    /// Post-process the Wix .wxs file before compiling it into an MSI
    /// </summary>
    /// <param name="wxsFileName"></param>
    private static void PostProcessWxsXMLOutput(string wxsFileName)
    {
        StreamReader sr = new StreamReader(wxsFileName);
        string myWixDocument = sr.ReadToEnd();
        sr.Close();
        string myProcessedWixDocument = WiXHelpers.InsertFragmentInWiXDocument(myWixDocument, myWIX_INSERT_AFTER_TEXT, myWIX_SET_DIRECTORY_STATEMENT);
        StreamWriter sw = new StreamWriter(wxsFileName);
        sw.Write(myProcessedWixDocument);
        sw.Close();
    }

Note: The .wxs file is deleted after the BuildMsi finishes. To force your resulting .wxs file to be saved, you will need to add a line to your Wix# code, after the Compiler.BuildMsi line, as follows:

Compiler.BuildWxs(Project)

What this actually does is re-fire the WixSourceSaved event, which then calls my PostProcessXMLOutput delegate, which regenerates a new, content-identical copy of the .wxs file. This time, the wxs file does not get automatically deleted. The resulting wxs file also will have a later timestamp than the corresponding MSI file from the build.

like image 171
Developer63 Avatar answered Oct 04 '22 15:10

Developer63