Skip to content

Developer's Guide

Oleg Shilo edited this page Apr 1, 2024 · 17 revisions

Overview


Authoring MSI

Defining Wix# Project

<Wix# Samples>/CodingStyles

Authoring MSI with Wix# consist of three distinctive steps: declaring an instance of the WixSharp.Project, defining the compilation action with WixSharp.Compiler and executing the build script.

Instantiating the Wix# project is as simple as instantiating any C# class. And 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 end lists 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 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 avoiding potential id clashes associated with the manual id assignments. See "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 in a more conventional way:

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

mainExe.Shortcuts = ...

A typical project declaration scenario consist 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

Building Wix# project into MSI or MSM can be accomplished by invoking one of the WixSharp.Compiler.Build* major methods:

  • 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. All WiX files are included in the WiX# downloadables and Wix# engine will use then by default. However if Wix# cannot find WiX compilers in the default location (e.g. Wix# from NuGet) it will always try to locate an installed standalone WiX instance in the well-known locations (e.g. Program Files/...).

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).

Building MSI File

<Wix# Samples>/Building 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 executing the build script with Visual Studio/MSBuild or with CS-Script script engine. All Wix# samples are distributed with a copy if the script engine (cscs.exe) and the corresponding batch file (build.cmd) for building the sample .msi.

<Wix# Samples>/Building with Visual Studio

The build script can also be executed with Visual Studio. You just need to include it as an source .cs file to the C# Console Application project and add the reference to WixSharp.dll. You may also need to adjust the working directory in the Debug tab of the project settings.

The actual building of the .msi file will be triggered by the execution (F5) of the project build output file. It is highly recommended that you read about how to build MSI using Wix# with Visual Studio.

NuGet packages

All this can also be accomplished in a single step by adding the WixSharp NuGet package to the C# Console Application project. image

NuGet package integration also includes injection of the custom target for to the project build sequence. Thus you don't have to execute the build script. It will be done automatically at the end of the project build.

Note: NuGet packages automatically install dependency package WixSharp.bin, which contains only Wix# assemblies and no other content. Thus if you want to fetch the latest Wix# binaries for already existing project you can just execute update-package wixsharp.bin NuGet command for that.

There are quite a few NuGet packages that you can use when building your MSI based setup. You can list all of them with your package manager app:

nuget list wixsharp*
  • WixSharp compilers and dependencies

    • WixSharp
      WixSharp compilers, MSBuild integration (*.targets) and a code sample.
    • WixSharp.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 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;
    }
}

Building environment integration

The simplest, and in many cases the most practical, MSI authoring approach is the direct execution of the build script (setup.cs). You can do it running "cscs.exe setup.cs". cscs.exe is a standalone CS-Script engine, which is included in Wix# downloadables. Every Wix# sample is accompanied with 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.

An alternative approach is to use VS project, which is created from one of the Wix# VS project templates and properly adjusted by installing WixSharp NuGet package.

During the build Wix# compiler resolves app environment variables found in any path definition. See EnvVariables sample for details.

You can also map VS project variables to environment variables by using custom MSBuild task SetEnvVar.dll distributed with Wix# NuGet packages. The following XML definition placed in WixSharp*.targets file will always map the VS project variables to the environment ones so your build script can access them:

<SetEnvVar Values="ProjectName=$(ProjectName);ProjectDir=$(ProjectDir);SolutionName=$(SolutionName);SolutionDir=$(SolutionDir)"/>     

If you want to debug your build script intensively you probably may want to disable the Import...WixSharp.targets in your project file and switch to $(TargetPath) execution in the post-build event. However in many cases you may achieve practically the same convenience by simple placing asserts in your build script and attaching the debugger when prompted.

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 streamlined, lightweight MSI development model. Thus many of the MSI features which 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 the full control over the building 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 attribute 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 some 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 that 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 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 straight forward.

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

The sample below implements WiX RemoveFolderEx element from 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 require XML and adding it to the required parent XML element.

And 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: ..." 
                              });