Creating a Wix# Installer That Includes Prerequisites Part 2

Creating an EXE Installer that Bundles Prerequisites

Introduction

This tutorial picks up where Part 1 left off, but if you have a general understanding of C#, it should not be hard to follow. Today, we are going to look at adding the .NET Framework as a prerequisite of the installer.

The parts of this series are:

  1. Creating an MSI installer with Wix#
  2. Creating an EXE Installer that Bundles Prerequisites (this one)
  3. Creating Custom Actions to Simplify Tasks

Background

Installers are great, but telling a user that they need to look up and install a specific framework is a lot for the average user. Even directing them to the website where they can download it can be overwhelming for some. With Wix#, it becomes easy to add in a list of packages to install that includes everything the user needs to get started.

The WiX Toolset relies on a tool called Burn (continuing with the Candle theme) to build EXE packages that install prerequisites and combine multiple MSIs. This process is known as BootStrapping.

Assumptions

I am going to assume a few things going forward. If these are not true, your experience may differ.

  1. Visual Studio 2017 is your IDE
  2. WiX Toolset is installed
  3. You are coding in C# (VB.NET will have similar principals but will require some translation)
  4. You are using the WixSharp Project Templates Extension for Visual Studio
  5. You have read part 1 and have a basic MSI installer project using Wix#

Building a Bundle with Wix#

The BootStrapper for the Installers in Wix is referred to as a Bundle and uses a WiX Toolset BurnProject. In the WixSharp Project Templates Extension, a Burn Project is referred to as a WixSharp Setup - BootStrapper. It's easiest to just start making a few changes to our starter project if you worked through Part 1.

Start by adding WixSharp.Bootstrapper as a using statement and then restructure your installer as follows:

private static void Main()
{
    string productMsi = BuildMsi();

    var bootstrapper =
      new Bundle("SampleInstaller",
          new MsiPackage(productMsi) { DisplayInternalUI = true, Compressed = true })
      {
          UpgradeCode = new Guid(<Insert Your Bundle GUID>),
          Version = new Version(1, 0, 0, 0),
      };
    bootstrapper.Build("SampleBootStrapper.exe");
}

private static string BuildMsi()
{
    var TestDir = new Dir(@"%ProgramFiles%\\My Company\\My Product");
    TestDir.Files = System.IO.Directory.GetFiles
     (@"D:\\Checkouts\\WixSharp Tutorial\\WixSharp Tutorial\\SampleApp\\bin\\Release").Where
     (file => file.EndsWith(".dll") || file.EndsWith(".exe")).Select(file => new File(file)).ToArray();

    var project = new Project("MyProduct", TestDir);

    project.GUID = new Guid(<Insert Your MSI GUID>);

    return project.BuildMsi();
}

As you notice, we are still building the MSI for the installer in essentially the same way, but then we return the path to the freshly built MSI and build a bundle wrapped around it.

Including Prerequisites with Bundle

Now that we have a basic bundle put together, we realize that our application requires .NET 4.7 to be installed and the user may not have it installed. With the PackageRefGroups available through the Wix Toolkit, we can check for and have installed a number of runtimes.

private static void Main()
{
    string productMsi = BuildMsi();

    var bootstrapper =
     new Bundle("SampleInstaller",
       new PackageGroupRef("NetFx462Web"),
       new MsiPackage(productMsi) { DisplayInternalUI = true, Compressed = true })
     {
         UpgradeCode = new Guid(< Insert Your Bundle GUID >),
         Version = new Version(1, 0, 0, 0),
     };

    bootstrapper.IncludeWixExtension(WixExtension.NetFx);
    bootstrapper.Build("SampleBootStrapper.exe");
}

The key lines above are new PackageGroupRef("NetFx462Web") and bootstrapper.IncludeWixExtension(WixExtension.NetFx);

NetFx462Web tells WiX to install .NET 4.62 by bundling in the .NET Web Installer. This keeps your installer size small but requires an internet connection with access to the URL. You could also use NetFx462Redist which tells WiX to bundle the standalone installer from. WiX also has variables that can be used to check if .NET 4.62 is already installed instead of dumping the user into an installer needlessly.

Packaging .NET 4.7 (or any .exe)

If your application needs 4.7+ versions of the .NET Framework, there is at the time of this writing no support in the NetFx extension from WiX. However, the base WiX tools contain everything needed to accomplish this. As noted under Points of Interest, Microsoft has an in depth guide on how to detect what versions of the framework are installed. But to do this in WiX, we need to add a fragment. This fragment combined with another extension (Util) essentially allows us to store the result of a registry search.

bootstrapper.IncludeWixExtension(WixExtension.Util);

bootstrapper.AddWixFragment("Wix/Bundle",
    new UtilRegistrySearch
    {
        Root = Microsoft.Win32.RegistryHive.LocalMachine,
        Key = @"SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full",
        Value = "Release",
        Variable = "NetVersion"
    });

The value found by this result is stored as `NetVersion` and is used in conditions to determine if we need to install the framework or not. This is a good first step and difficult to translate from direct WiX examples.

private static ExePackage Net471()
        {
            string currentN2t471webinstaller = 
              @"https://download.microsoft.com/download/8/E/2/8E2BDDE7-F06E-44CC-A145-56C6B9BBE5DD/
              NDP471-KB4033344-Web.exe";
            string Net471Installer = "Net471-web.exe";
            using (var client = new WebClient())
            {
                client.DownloadFile(currentN2t471webinstaller, Net471Installer);
            }
            ExePackage Net471exe = new ExePackage(Net471Installer)
            {
                Compressed = true,
                Vital = true,
                Name = "Net471-web.exe",
                DetectCondition = "NetVersion >= 460798",
                PerMachine = true,
                Permanent = true,
            };

            return Net471exe;
        }

Now we have declared a static method that generates an ExePackage for our installer to use. There are a few key things to obtain from this code sample:

  • WebClient is from the System.Net namespace and is used here to download the file at build time. If the link you are using may not always be obtainable, you will want to store it locally and replace this with logic to copy the file into the working directory.
  • Compressed if true causes the executable to be compressed and stored in the resulting installers instead of distributed as a separate file.
  • Vital causes the install to fail and roll back if this EXE fails.
  • DetectCondition is where we use the variable from above. Per Microsoft, 460798 is the min version (depends on OS) that can be .NET 4.7. In this case, my app is built in 4.7 but we are installing 4.71 with the installer to prevent needless framework installs if we decide to upgrade in the near future.

Putting It All Together - Complete Code

using System;
using System.Linq;
using System.Net;
using WixSharp;
using WixSharp.Bootstrapper;

namespace WixSharp\_Setup2
{
    class Program
    {
        private static void Main()
        {

            string productMsi = BuildMsi();


            var bootstrapper =
              new Bundle("SampleInstaller",
              Net471(),
                  new MsiPackage(productMsi) { DisplayInternalUI = true, Compressed = true })
              //new PackageGroupRef("NetFx47Web"))
              {
                  UpgradeCode = new Guid(<Insert Your Bundle GUID>),
                  Version = new Version(1, 0, 2, 0),
              };

            bootstrapper.IncludeWixExtension(WixExtension.Util);

            bootstrapper.AddWixFragment("Wix/Bundle",
                new UtilRegistrySearch
                {
                    Root = Microsoft.Win32.RegistryHive.LocalMachine,
                    Key = @"SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full",
                    Value = "Release",
                    Variable = "NetVersion"

                });
            bootstrapper.PreserveTempFiles = true;
            bootstrapper.Application.LicensePath = null;
            bootstrapper.Build("SampleBootStrapper.exe");
        }

        private static string BuildMsi()
        {
            var TestDir = new Dir(@"%ProgramFiles%\\My Company\\My Product");

            TestDir.Files = System.IO.Directory.GetFiles
            (@"D:\\Checkouts\\WixSharp Tutorial\\WixSharp Tutorial\\SampleApp\\bin\\Release").Where
            (file => file.EndsWith(".dll") || file.EndsWith(".exe")).Select
            (file => new File(file)).ToArray();

            var project = new Project("MyProduct", TestDir);

            project.GUID = new Guid(<Insert Your MSI GUID>);
            //project.SourceBaseDir = "<input dir path>";
            //project.OutDir = "<output dir path>";

            return project.BuildMsi();
        }

        private static ExePackage Net471()
        {
            string currentN2t471webinstaller = 
               @"https://download.microsoft.com/download/8/E/2/8E2BDDE7-F06E-44CC-A145-56C6B9BBE5DD/
               NDP471-KB4033344-Web.exe";
            string Net471Installer = "Net471-web.exe";
            using (var client = new WebClient())
            {
                client.DownloadFile(currentN2t471webinstaller, Net471Installer);
            }
            ExePackage Net471exe = new ExePackage(Net471Installer)
            {
                Compressed = true,
                Vital = true,
                Name = "Net471-web.exe",
                DetectCondition = "NetVersion >= 460798",
                PerMachine = true,
                Permanent = true,
            };

            return Net471exe;
        }
    }
}

Points of Interest

History

Did you find this article valuable?

Support CodingCoyote Blog by becoming a sponsor. Any amount is appreciated!