Skip to content

Developer's Guide

Oleg Shilo edited this page May 4, 2024 · 20 revisions

Overview


Authoring MSI

Defining Wix# Project

<Wix# Samples>/CodingStyles

_Build Script - a C# file containing definition of your setup project.

Authoring MSI with Wix# consists of three distinctive steps: declaring an instance of the WixSharp.Project (or WixSharp.ManagedProject), defining the compilation action with WixSharp and executing the build script.

Instantiating the Wix# project is as simple as instantiating any C# class. As with any trivial class declaration, it can done in various ways/styles:

Traditional (plain C# routine)

var docFile = new File(@"Files\Docs\Manual.txt");
var exeFile = new File(@"Files\Bin\MyApp.exe");
var dir = new Dir(@"%ProgramFiles%\My Company\My Product");
var myCompanyDir = dir.Dirs[0];
var myProductDir = myCompanyDir.Dirs[0];
myCompanyDir.Files = new[] { docFile, exeFile };
        
var project = new Project();
project.Dirs = new[] { dir };
project.Name = "MyProduct";

Class initializers

var project =   
    new Project()
    {
        Name = "MyProduct",
        GUID = new Guid("6f330b47-2577-43ad-9095-1861ba25889b"),
        Dirs = new[]
        {
            new Dir("%ProgramFiles%")
            {
                Dirs = new[]
                {
                    new Dir("My Company")
                    {
                        Dirs = new []
                        {    
                            new Dir("My Product")
                            {
                                Files = new []
                                {
                                    new File(@"Files\Docs\Manual.txt"),
                                    new File(@"Files\Bin\MyApp.exe")
                                }
                            }
                        }
                    }
                }
            }
        }
    };

XDocument style constructors (preferred)

var project =
    new Project("MyProduct",
        new Dir(@"%ProgramFiles%\My Company\My Product",
            new File(@"Files\Docs\Manual.txt"),
            new File(@"Files\Bin\MyApp.exe")));

XDocument style deserves special attention. WixSharp Project, Dir and File constructors are extremely similar to the XDocument/XElement constructors accepting an open-ended list of parameters of the generic base type.

System.Xml.Linq.XElement(XName name, params Object[])
WixSharp.File(string name, params WixEntity[])

This style of instantiation has proven to be effective and easy to read for the definition of the XML structure (LINQ for XML). Therefore, the XDocument style has been chosen as the default instantiation style for all Wix# samples.

Apart from being convenient using constructors has another strong advantage. It allows automatic generation of the directory structure via splitting the name (path) constructor argument. Thus passing %ProgramFiles%\My Company\My Product as the constructor argument triggers the creation of the nested directories 'My Company' and "My Product". And the next File argument is automatically pushed to the bottom-most subdirectory "My Product".

Using constructors also helps avoid potential id clashes associated with manual id assignments. See the "Explicit IDs" sample for details.

Usually, all Wix# types have multiple overloads allowing passing some specific parameters.

XDocument style declarations represent some minor challenges when you need to access constructor parameters outside of the constructor.

project.FindFile(f => f.Name.EndsWith("MyApp.exe"))
       .First()
       .Shortcuts = new[] 
                    {
                        new FileShortcut("MyApp.exe", "INSTALLDIR"),
                        new FileShortcut("MyApp.exe", "%Desktop%")
                    };

And of course (when it is possible) it can be done more conventionally:

File mainExe;
...
new Dir(@"%ProgramFiles%\My Company\My Product",
    mainExe = new File(@"Files\Bin\MyApp.exe")),  ...

mainExe.Shortcuts = ...

A typical project declaration scenario consists of a single call to the constructor accepting parameters of a WixObject type. However, in some cases, it is beneficial to pass a collection instead of individual objects. While the Project constructor does not allow passing collections this problem can be easily handled by calling the ToWObject extension method of the collection:

var fullSetup = new Feature("MyApp Binaries");
 
IEnumerable<RegValue> regValues = Tasks.ImportRegFile("setup.reg")
                                       .ForEach(r => r.Feature = fullSetup);
        
var project =
    new Project("MyProduct",
        new Dir(@"%ProgramFiles%\My Company\My Product",
            new File(fullSetup, @"readme.txt")),
        regValues.ToWObject());

Compiling Wix# Project

Note, that building msi with WixSharp is a two steps process:

  • Compiling your build script (console application project) containing the C# code defining your setup (Project or ManagedProject).
  • Executing your compiled build script as a VS post-build event. This will produce the msi file.

The overall building process is below:

image

It is highly recommended that you read in detail about building MSI using Wix# with Visual Studio.

Building Wix# project into MSI or MSM is triggered by invoking one of the WixSharp.Compiler.Build* major methods in your build script:

  • Compiler.BuildMsi()
    Will build MSI setup from the project definition
  • Compiler.BuildMsm()
    Will build MSM setup package from the project definition
  • Compiler.BuildWxs()
    Will build WiX source code that can be used to build MSI/MSM setup package.
  • Compiler.BuildMsiCmd()
    Will build WiX source code and create batch file (*.cmd) that can be used to build MSI/MSM setup by invoking WiX tools directly (from batch file).

BuildMsiCmd is particularly useful for troubleshooting as it allows manual adjustments of the generated WiX source file (*.wxs) before it is passed into the WiX compiler/linker.

It can also be useful to preserve .wxs file for further inspections after the build. This can be done by setting the corresponding compiler flag:

Compiler.PreserveTempFiles = true;
Compiler.BuildMsi(project);

It is also important to note that all references to the files to be included in the MSI are relative with respect to the CurrentDirectory. Thus if for whatever reason it is inconvenient you can always:

  • Adjust CurrentDirectory
  • Set project.SourceBaseDir = <root dir of files to be installed>;
  • Use absolute path.

The actual WiX compilers that Wix# uses to build the MSI file come from the standard WiX deployment.

  • WiX3 WiX Toolset installed in the usual well-known locations (e.g. Program Files/...). During build time Wix# will find these tools automatically. If Wix# cannot find any installed WiX instance then you will need to specify the location of the compilers to use. You can do this by setting Compiler.WixLocation to the WiX path. There is another similar Compiler field WixSdkLocation, however you should not change it as it is to be used for the cases when WiX distro is packaged in an unusual way (the WiX SDK tools are placed in the custom locations).

  • WiX4 WiX Toolset is to be installed as .NET Tool. This is the only way WiX team distribute this version of the toolset.

Building MSI with and without Visual Studio

The Wix# Project definition and the compilation are typically placed in a single C# file (build script). Building the final MSI can be accomplished either by building WixSharp project with Visual Studio/MSBuild. But if you prefer you can build it with the CS-Script script engine. All Wix# samples are distributed with a copy of the script engine (cscs.exe) and the corresponding batch file (build.cmd) for building the sample .msi.

NuGet packages

When you create your Wix# project from the corresponding VS template all required nuget packages will be already present in your project.

The NuGet packages are named in such a way that the name indicates the type of the compiling tools. Thus wix4 suffix indicates that the msi is to be compiled with WiX4/WiX5 tools. The .Core suffix indicates that the build script is to be run under a .NET Core.

While manually adding WixSharp packages to your project is possible, it's not recommended. The best way to get your running is to use Visual Studio templates instead.

There are quite a few native and third-party NuGet packages that you can use when building your MSI based setup. These are some of them:

  • WixSharp compilers and dependencies

    • WixSharp, WixSharp_wix4
      WixSharp compilers, MSBuild integration
    • WixSharp.bin, WixSharp_wix4.bin
      WixSharp compilers. Use this package if you aim for as light as possible VS integration. Create WixSharp project as a is a Console Application, define your setup Project in static Main and build VS project executable and then run it.
    • WixSharp.wix.bin
      WiX compilers and binaries. Use this package if you do not want to install WiX SDK.
    • WixSharp.Core
      WixSharp compilers targeting .NET Core runtime.
  • WixSharp extensions

  • WixSharp experimental features

    • WixSharp.ClrDialog
      A sample code for building a simple MSI with the CLR WinForm dialog inserted into UI sequence between InsallDirDlg and VerifyReadyDlg native MSI dialogs.
    • WixSharp.Lab
      Wix# binaries containing experimental features (e.g. native WiX UI support).

Your build script will internally handle all errors associated with Wix#; however, any runtime error in the build script itself will fail the post-build action and will print a generic error '255' error message.

If there is a chance that you can have non-Wix # specific runtime error then you need to change your main signature to return an int and use exception handling:

class Script
{
    static int Main()
    {
        try
        {
            //Wix# build steps
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
            return 1;
        }
        return 0;
    }
}

Build environment integration

The simplest, and sometimes the most practical, MSI authoring approach is the direct execution of the build script (setup.cs). You can do it by running "cscs.exe setup.cs". cscs.exe is a standalone CS-Script engine, which is included in Wix# downloadables. Every Wix# sample is accompanied by the batch file (build.cmd) for building the sample MSI this way. Being just an ordinary executable cscs.exe can be integrated with MSBuild or any other CI building environment.

But the most common way of building msi is to use VS project, which is created from one of the Visual Studio templates. While such a project will work just out of box you can still customize it or integrate it with your CI. IE passing MS BUild context information to your build script. See Wix# Visual Studio project templates page for the details.

Overcoming Wix# limitations

Wix# allows access to a subset (not the full set) of the WiX/MSI functionality. In the majority of cases, it is a deliberate design decision, which is consistent with the Wix# fundamental concept of delivering a streamlined, lightweight MSI development model. Thus many of the MSI features that have very little practical value are deliberately omitted (components, minor upgrades, etc.).

However, there can be specific cases when you may need the full power of WiX to achieve full control over the building of your MSI. Wix# has two different mechanisms to support such development scenarios.

Custom XML attributes The all code in this section is from the <Wix# Samples>\CustomAttributes sample.

In many cases Wix# types are directly mapped to the WiX elements and the Wix# fields/properties are mapped to the WiX element attributes. You may find that some specific WiX attributes may not have a corresponding Wix# field/property mapped. Wix# allows to define such a mapping dynamically. For example, the WiX Shortcut element has a Hotkey integer attribute, which is not mapped in the type FileShortcut. The problem can be addressed as follows:

new FileShortcut("Launch Notepad", @"%Desktop%") 
{ 
    Attributes = new Attributes() { { "Hotkey", "0" } } 
}

In fact, the same can be achieved with a much simpler code and even for multiple attributes:

new FileShortcut("Launch Notepad", @"%Desktop%") 
{ 
    AttributesDefinition = "Hotkey=0;Advertise=yes" 
}

You can even control the attributes of the parent WiX elements. Though the support for the parent elements covers only Component and Custom (associated with CustomAction) elements:

new File(@"C:\WINDOWS\system32\notepad.exe")
{
    AttributesDefinition = "Component:SharedDllRefCount=yes"
}
new InstalledFileAction("Registrator.exe", "", 
                        Return.check, When.After, Step.InstallFinalize, 
                        Condition.NOT_Installed) 
                        { AttributesDefinition="Custom:Sequence=1" } 
 

XML injection

The all code in this section is from the <Wix# Samples>\InjectXML sample.

Sometimes injecting unmapped attributes is not enough and more serious XML adjustments are required. For such cases Wix# allows direct XDocument manipulation of the XML content just before it is passed to the WiX compilers. This is the most powerful mechanism of extending the default Wix# functionality. With XML injection, you can do with Wix# absolutely everything that you can with WiX. Below is an example of how you can set UI dialogs background images:

    ...
    Compiler.WixSourceGenerated += InjectImages;
    Compiler.BuildMsi(project);
}
 
static void InjectImages(XDocument document)
{
    var productElement = document.Root.Select("Product");
        
    productElement.Add(new XElement("WixVariable", 
                            new XAttribute("Id", "WixUIBannerBmp"),
                            new XAttribute("Value", @"Images\bannrbmp.bmp")));
 
    productElement.Add(new XElement("WixVariable", 
                            new XAttribute("Id", "WixUIDialogBmp"),
                            new XAttribute("Value", @"Images\dlgbmp.bmp")));
}

Wix# implements various extensions for convenient (fluent) XML manipulations. Thus the code above can be rewritten as follows:

productElement.AddElement("WixVariable", @"Id=WixUIDialogBmp;Value=Images\dlgbmp.bmp");

Some other XML extensions allow simple XML manipulations without dealing with namespaces. Thus all lookup operations are based on the matching LocalNames instead of Names. For all practical reasons, it is much more convenient than specifying the namespaces all the time. The extensions are mapping all major lookup operations:

//XContainer.Single equivalent
document.FindSingle("Package");
 
//XContainer.Descendants equivalent
document.FindAll("Package");
 
//XPath equivalent
document.Select("Wix/Product/Package");
 
//This is a fluent version of XElement.SetAttributeValue
package.SetAttribute("Count", 10);
package.SetAttribute("Count=10;Index=1");

Wix# also offers support for WiX includes. It is implemented via Wix# entity extension method AddXmlInclude. Thus

new File("Source="Files\Docs\Manual.txt").AddXmlInclude("FileCommonProperies.wxi")

Will produce the following wxs code:

<File Id="Manual.txt" Source="Files\Docs\Manual.txt">
   <?include FileCommonProperies.wxi?>
</File>

Extending Wix# type system

Sometimes you may find that WixSharp may have no direct support not only for some attributes but for the whole WiX element(s). Of course, you can still use XML injection to insert the desired XML, however, there is a much better approach that makes this task very straightforward.

You can define your ow type that defines the desired XML element content structure. Such a class has to implement the IGenericEntity interface.

The sample below implements WiX RemoveFolderEx element from the Utils extension:

public enum InstallEvent
{
    install,
    uninstall,
    both
}

public class RemoveFolderEx: WixEntity, IGenericEntity
{
    [Xml]
    public InstallEvent? On;

    [Xml]
    public string Property;

    [Xml]
    new public string Id;

    public void Process(ProcessingContext context)
    {
        // indicate that candle needs to use WixUtilExtension.dll
        context.Project.Include(WixExtension.Util); 

        XElement element = this.ToXElement(WixExtension.Util.ToXName("RemoveFolderEx"));

        context.XParent
               .FindFirst("Component")
               .Add(element);
    }
}

The Process method is responsible for emitting the required XML and adding it to the required parent XML element.

In order to use this new WixSharp class you just need to pass it to the Dir constructor:

var project = 
        new Project("CustomActionTest",
            new Dir(@"%ProgramFiles%\CustomActionTest",
                new RemoveFolderEx { On = InstallEvent.uninstall, Property = "DIR_PATH_PROPERTY_NAME" },
                ...

Note that the extension method ToXElement is an extremely convenient approach for creating XElement from the object. All properties and fields of the object that are marked with [XML] attribute become the XML attributes during the ToXElement conversion.

Similarly, you can extend Bandle with a new bal.Condition element from Bal WiX extension:

class BalCondition : WixEntity, IGenericEntity
{
    public string Condition;

    public string Message;

    public void Process(ProcessingContext context)
    {
        context.Project.Include(WixExtension.Bal);

        var element = new XElement(WixExtension.Bal.ToXName("Condition"), Condition);
        element.SetAttribute("Message", Message);

        context.XParent.Add(element);
    }
}

...

var bootstrapper = new Bundle("My Product Suite",...
bootstrapper.GenericItems.Add(new BalCondition 
                              { 
                                 Condition = "some condition", 
                                 Message = "Warning: ..." 
                              });