MSI Authoring


Here we will walk through the steps involved in building msi out of build output of csproj or vbproj. A set of dll's and a single exe. Wix Toolset is going to be used to build msi. You will need to prepare MSBuild environment with extensions to reproduce the build. You can download project described here.

WiX Project File

WiX Project files (wixproj) are also build-able by MSBuild. The main difference is that instead of producing exe or dll like code compilation does, it produces MSI out of other build output. Bellow is one important part of Wix Project file:

  <PropertyGroup>
    <DefineConstants>
      ProductID=$(ProductID);
      ProductName=$(ProductName);
      ProductVersion=$(ProductVersion);
      ProductManufacturer=$(ProductManufacturer);
      ProductUpgradeCode=$(UpgradeCode);
      BasePath=$(HarvestPath);
      IconPath=$(IconPath);
      LauncherID=$(LauncherID);
      LauncherRegistryID=$(LauncherRegistryID);
      AssemblyName=$(AssemblyName);
      DesktopShortcutID=$(DesktopShortcutID)
    </DefineConstants>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="CustomUI\CustomInstallDirDlg.wxs" />
    <Compile Include="CustomUI\CustomWixUI_InstallDir.wxs" />
    <Compile Include="installer\harvest.wxs" />
    <Compile Include="installer\static.wxs" />
  </ItemGroup>

Here we define constants so that we can pass in parameters to MSBuild and customize our build. Define our MSI properties in particular. Project contains at least one WiX Source file (wxs). Parameters  defined in DefineConstants can be used in wxs files in a form of $(var.PropertyName). This project include 4 wxs files. 2 are related to custom UI and and 2 includes actual components. If you want to dig in to how UI works you can read about it here. UI will not be covered here. Instead we will cover static.wxs and harvest.wxs. But first we will describe what is WiX Source file.

WiX Source File

WiX source file starts with wix. But central part of Wix Project is actually a Product. Using this element creates an msi file. Critical part of Project definition is UpgradeCode. You can look at it as a Project Specific ID (expressed as GUID) that defines installation of a Product. So when next version is released it is installed with the same UpgradeCode and it replaces previous version of same Product. Product then in turn can have Features.  Features are the smallest install-able units in the context of msi.

static.wxs

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:netfx="http://schemas.microsoft.com/wix/NetFxExtension">
  <Product Id="$(var.ProductID)" Name="$(var.ProductName)" Version="$(var.ProductVersion)" Manufacturer="$(var.ProductManufacturer)" Language="1033" UpgradeCode="$(var.ProductUpgradeCode)">
    <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
    <MajorUpgrade DowngradeErrorMessage="A newer version of $(var.ProductName) is already installed." IgnoreRemoveFailure="no" />
    <Media Id="1" Cabinet="container.cab" EmbedCab="yes" CompressionLevel="high" />
    <!-- Define features to be included when installed-->
    <Feature Id="Application" Title="$(var.ProductName)" Level="1">
      <ComponentGroupRef Id="mm" />
    </Feature>
    <!--Define what UI components to use for installer -->
    <Property Id="WIXUI_INSTALLDIR">INSTALLFOLDER</Property>
    <UIRef Id="CustomWixUI_InstallDir" />
    <Property Id="INSTALLDESKTOPSHORTCUT" Value="1" />
    <!-- Defining components and its content -->
    <DirectoryRef Id="INSTALLFOLDER">
      <Component Id="launcher" Guid="$(var.LauncherID)">
        <File Id="executable" Source="$(var.BasePath)\$(var.AssemblyName).exe" KeyPath="yes">
          <Shortcut Id="startmenushortcut" Directory="launcher_dir" Name="$(var.ProductName)" Icon="app.ico" IconIndex="0" WorkingDirectory="INSTALLFOLDER" Advertise="yes" />
        </File>
      </Component>
    </DirectoryRef>
    <DirectoryRef Id="launcher_dir">
      <Component Id="launcher_reg" Guid="$(var.LauncherRegistryID)">
        <RemoveFolder Id="launcher_dir" On="uninstall" />
        <RegistryValue Root="HKCU" Key="Software\$(var.ProductManufacturer)\$(var.ProductName)\Shortcuts" Type="integer" Name="shortcutforstartmenu" Value="1" KeyPath="yes" />
      </Component>
    </DirectoryRef>
  </Product>
  <!-- Define directory structure -->
  <Fragment>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <!-- mandatory root directory -->
      <Directory Id="DesktopFolder" Name="Desktop">
        <Component Id="desktop_shortcut" Guid="$(var.DesktopShortcutID)">
          <Condition>INSTALLDESKTOPSHORTCUT</Condition>
          <RegistryKey Root="HKCU" Key="Software\$(var.ProductManufacturer)\$(var.ProductName)\Shortcuts">
            <RegistryValue Name="shortcutfordesktop" Value="1" Type="integer" KeyPath="yes"/>
          </RegistryKey>
          <Shortcut Id="DesktopShortcut" Directory="DesktopFolder" Name="$(var.ProductName)" Icon="app.ico" Target="[#executable]"/>
        </Component>
      </Directory>
      <Directory Id="ProgramFilesFolder">
        <!-- Special name for "Program Files" folder-->
        <Directory Id="INSTALLFOLDER" Name="$(var.ProductName)" />
      </Directory>
      <Directory Id="ProgramMenuFolder" Name="Programs">
        <!-- Special folder for start menu folder-->
        <Directory Id="launcher_dir" Name="$(var.ProductManufacturer)">
        </Directory>
      </Directory>
    </Directory>
  </Fragment>
  <!--define components to be included in marketmaker installer-->
  <Fragment>
    <ComponentGroup Id="mm" Directory="INSTALLFOLDER">
      <ComponentGroupRef Id="installer_harvest" />
      <ComponentRef Id="launcher" />
      <ComponentRef Id="launcher_reg" />
      <ComponentRef Id="desktop_shortcut" />
    </ComponentGroup>
  </Fragment>
  <!--Define All icons to be used-->
  <Fragment>
    <Icon Id="app.ico" SourceFile="$(var.BasePath)\$(var.IconPath)" />
    <Property Id="ARPPRODUCTICON" Value="app.ico" />
    <!-- ARPPRODUCTICON defines icon to be used in add remove programs-->
  </Fragment>
</Wix>

Here we define one feature and few fragments. In a feature we define what components should be included. We include one Component group reference. If you would search that component group reference by id you can find it defined in one of fragments. Bellow is a portion of XML defining a component group.

<Fragment>
  <ComponentGroup Id="mm" Directory="INSTALLFOLDER">
      <ComponentGroupRef Id="installer_harvest" />
      <ComponentRef Id="launcher" />
      <ComponentRef Id="launcher_reg" />
      <ComponentRef Id="desktop_shortcut" />
  </ComponentGroup>
</Fragment>

Most of the components referenced here are defined in the same wxs file. But not the installer_harvest. It should actually be defined inside harvest.wxs.

harvest.wxs

If you would open harvest.wxs as is you would find it almost empty:

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" />

What is going on? Why is it empty? In fact it gets populated based the result of harvest_folder target in Setup.wixproj:

<Target Name="harvest_folder" DependsOnTargets="validate_properties">
  <Message Text="Harvesting folder.." Importance="high" />
  <HeatDirectory
    OutputFile="installer\temp.xml"
    Directory="$(HarvestPath)\"
    DirectoryRefId="INSTALLFOLDER"
    ComponentGroupName="installer_harvest"
    SuppressCom="true"
    SuppressFragments="true"
    SuppressRegistry="true"
    SuppressRootDirectory="true"
    AutoGenerateGuids="false"
    GenerateGuidsNow="true"
    ToolPath="$(WixToolPath)"
    PreprocessorVariable="var.BasePath" />
  <Message Text="Applying transformations to harvested xml.." Importance="high" />
  <XslTransformation
    XmlInputPaths="installer\temp.xml"
    XslInputPath="installer\transformations.xslt"
    OutputPaths="installer\harvest.wxs" />
</Target>

We are doing 2 things here. First we heat the directory and then we transform the resulting xml. This part is actually critical to collecting folder as a component group. As you can see you can define ComponentGroupName and we used the same name in component group from static.wxs.

transformations.xslt

Code preview does not play nice here so you will have to manually open it from included archive. Main reason we actually need the transformation here is that we do not want to include exe 2 times. so we rewrite xml to remove it.

Conclusion

Here we defined a reproducible folder transformation to MSI. Check readme.txt for instructions how to invoke built with MSBuild. If you will need to verify MSI build result you can use lessmsi tool. Check MSBuild command line reference for details how to use MSBuild. And also do not forget you can always look at other samples.


Related articles

Placeholder "LocalizeWeb2016" failed