Windows Installer Notes
Overview
Surprisingly, Windows Installers still exist today and there remains an abundance of confusion around this framework, I have answered a few questions on StackOverflow - Geoff. I have attempted to collate as much of my understanding of it when I touched it many years ago on this page - I hope it is helpful to someone out there!
Wikipedia summarizes the format of the Windows Installer Package .msi
but I'll repeat it here for completion sake.
A Windows Installer package is made up of one or more full products and is universally identified by a GUID. A product is a single, installed, working program (or a set of programs)
A package includes the package logic and other metadata that relates to how the package executes when running.
A feature is a hierarchical group of components. It be contain any number of components and other sub-features. You can see this on the feature selection dialog that sometimes presents itself during an installation.
A component is a basic unit of a product, they are threaded as a unit and are installed atomically. These can be program files, folders, COM, registry keys and shortcuts. They are identified by a GUID and can be shared among several features.
A key path is a specific file, registry key or ODBC data source that the package author specifies as critical for a given component.
When an msi-based program is launched, Windows Installer checks the existence of key paths. If there is a mismatch between the current system state and the value specified in the MSI package (e.g., a key file is missing), the related feature is re-installed. This process is known as self-healing or self-repair. No two components should use the same key path.
These notes will be very focused on building Windows Installers using the WiX Toolset.
Resources
A collection of very useful links:
WiX Toolset - includes VS extension, docs, built-in dialogs etc.
Stackoverflow: WiX Tips and Tricks - don't take everything as being the correct way
MS Docs: Windows Installer - once again, be careful about recommendations
A Real-world example of a Windows Installer - very useful but not all parts are relevant and there is room for improvements
InstEd - a tool for inspecting Windows Installers, another popular tool is Orca
Must read
WiX: A Developer's Guide to Windows Installer XML
Devil in the details
Once you build a more complex installer, be sure to read these:
Rob Mensching: Windows Installers Intro - he has a whole bunch of blogs I think are very important to read but might be non-beginner friendly
Rob Mensching: Component Rules 101 - this may be a little confusing but at least be aware of the trouble WiX actually does in the background to make your installer behave correctly
Bob Arnson: paying for upgrades - some additional information about major vs minor although not that clear
Someone having a rant about issues of Minor Upgrades - I agree with this person here, mixing upgrades is dangerous
Components and GUIDs - caution of using hard-coded and auto-generated GUIDs
Open source
Community MSI Extensions - no longer maintained but useful to see how extensions/UI dialogs are written
Classic WiX Burn theme - interesting example of a custom Burn UI theme
Git Extensions - a fairly robust example
WiX and Windows Installers
The WiX Toolset can be used to create Windows Installers and is well documented. It offers many built-in dialogs, actions and is excellent for basic installations.
Despite shift in the industry to move away from Windows Installers, it is still widely used (particularly in Enterprise) and well supported by Windows.
WiX Gotchas
In Visual Studio, it doesn't support the
AnyCPU
build configurationThis is due to the Registry x86/x64 locations
WiX v3 is maintained and stable, v4 is still under development and is not documented
WiX v3 does not support .NET Core
Components
This is the smallest atomic unit of a Windows Installer. Be sure to read Devil in the details.
Tips:
You can let WiX auto-generate the identifiers
Component/@Id="*"
The GUID is generated based on the installation directory and filename of the
File/@KeyPath
for a componentThe GUID will stay consistent from build-build provided the directory and filename of the
KeyPath
do not change
Do not change component GUIDs if the absolute path to your resource does not change - else it will conflict with the component rules
Variables, properties, localization
There are a few key difference you need to be aware of, this Stackoverflow post best explains it:
There are different layers of variables in WiX:
Candle's pre-processor variables
Light's WixVariables/localization variables/binder variables
MSI's properties
Each have different syntax and are evaluated at different times:
Candle's preprocessor variables "
$(var.VariableName)
" are evaluated when candle runs, and can be set from candle's command line and from "" statements. Build-time environment properties as well as custom variables can also be accessed similarly (changing the "var.
" prefix with other values). Light's variables accessible from the command-line are theWixVariables
, and accessing them is via the "!(wix.>VariableName)
" syntax. To access your variable from your command line, you would need to change your String to: "This build was prepared on!(wix.BuildMachine)
" If you instead need to have theBuildMachine
value exist as an MSI property at installation time (which is the "[VariableName]
" syntax) you would need to add the following to one of your.wxs
files in a fragment that is already linked in:Now, the environment variable
COMPUTERNAME
always has held the name of my build machines in the past, and you can access that this way:$(env.COMPUTERNAME)
. So, you can get rid of the command line addition to light.exe and change your wxs file like this:
<WixVariable Id="BuildMachine" Value="$(env.COMPUTERNAME)"/>
Registry search
Remember that there are x86/x64 Registry locations and need to be specified accordingly if you want to search for a key/value.
<Property Id="RegistryKeyExists" Value="False">
<RegistrySearch
Id="RegistryKeyExists"
Root="HKLM"
Key="SOFTWARE\Somewhere"
Name="[PropertyKeyName]"
Type="raw"
Win64="$(var.Win64)" />
</Property>
WiX supports many Registry operations - WiX RegistrySearch Util Extension - and more example can be found here.
MSI Conditions
You can schedule CAs, features, components etc. depending on MSI conditions. A great resource from InstallShield detailing the common ones is super handy to know.
Custom Actions
WiX supports writing Custom Actions in C# targeting the .NET Framework. You should be aware of the scheduling on the CA:
commit
- indicates that the custom action will run after successful completion of the installation script (at the end of the installation)deferred
- for elevated privileges and for running in the deferred execute stage. If any system state is changed, apply the reverse in the rollback.firstSequence
immediate
- (default) with user privileges or impersonates the installing user, it has read-write access to properties. Does not change system state. oncePerProcessrollback
- for rolling back a (deferred) custom actionsecondSequence
- indicates that a custom action should be run a second time if it was previously run in an earlier sequence
Example C# Custom Action
// CustomActions.cs
namespace CustomActions
{
public static class CustomActions
{
[CustomAction]
public static ActionResult SomeCustomAction(Session session)
{
session.Log("Begin SomeCustomAction"); // This will show in the MSI logs
var foo = session["SOME_PROPERTY_IN_YOUR_WIX_PROJECT"]; // Inject the MSI property to the CA
return ActionResult.Success; // Return a suitable result
}
[CustomAction]
public static ActionResult MessageBoxTest(Session session)
{
// If "Cancel" was clicked, it will interrupt the installer (prematurely)
session.Message(InstallMessage.Warning | (InstallMessage)MessageButtons.OKCancel,
new Record
{
FormatString = "Some text for the Message Box."
});
return ActionResult.Success;
}
}
}
Take note:
If you don't return an
ActionResult
, your MSI installer will not be able to detect your custom action for some reasonAfter building the project, it will generate a dll located in
bin\$(Configuration)\CustomActions.CA.dll
You can ignore the return result if you wish in the installer
The
session.Log()
may not appear in your installer log file if your CA appears in theInstallUISequence
or is attached to a UI button
<!-- Product.wxs -->
<!-- Reference the CA.dll of the custom actions -->
<Binary Id="CustomActionsDll" SourceFile="$(var.CustomActions.TargetDir)\CustomActions.CA.dll" />
<!-- Define your custom action -->
<CustomAction
Id="SomeCustomAction"
BinaryKey="CustomActionsDll"
DllEntry="SomeCustomAction"
Execute="immediate"
Return="ignore" />
<!-- Schedule when your CA is run -->
<InstallExecuteSequence>
<Custom Action="SomeCustomAction" After="InstallInitialize">
NOT Installed AND NOT PATCH
</Custom>
</InstallExecuteSequence>
You can also call the CA on a button click on your dialog, for example:
<Control Id="TestConnButton" Type="PushButton" Default="yes" Text="!(loc.SQLServerDlg_TestLabel)">
<Publish Event="DoAction" Value="TestSqlServerConnection" Order="3">1</Publish>
</Control>
Deferred Custom Actions
Deferred CAs are different than the "default" custom actions as they are run with elevated privileges (e.g. System, for modifying ACLs). They can only be executed in the "deferred" execution stage and must have an equivalent rollback custom action - any state changes must be reversed.
Deferred CAs cannot access MSI properties - you need to inject the property values via the CustomActionData
- which will still be read-only. Alternatively, you can use immediate custom actions to update MSI properties or set values in the Registry.
<!--
You can put these in a CustomAction.wxi and include them in the Product.wxs.
-->
<!-- Declare your custom action like normal -->
<CustomAction
Id="DeferredCustomAction"
DllEntry="DeferredCustomAction"
BinaryKey="CustomActions"
Execute="deferred"
Return="check" />
<!-- Similar to how we set "copy" properties -->
<SetProperty
Id="DeferredCustomAction"
Before="DeferredCustomAction"
Value="someKey=[SOME_PROPERTY];" />
// Deferred custom actions
CustomActionData customActionData = session.CustomActionData;
// Immediate custom actions
// So if we were to change these into deferred, it would be a one-line change as opposed to
// changing session["SOME_PROPERTY"] -> session.CustomActionData["someKey"]
CustomActionData customActionData = new CustomActionData(session[nameof(yourCustomActionMethodName)]);
Rollback Custom Actions
Rollback CAs are special types of deferred custom actions in that they are only invoked during the "rollback" stage e.g. failed installation. You cannot rollback CAs outside of the InstallExecute/@InstallInitialize
and InstallExecute/@InstallFinalize
- this is a limitation of deferred custom actions.
<!-- Declare your custom action like normal -->
<CustomAction
Id="DeferredCustomAction"
DllEntry="DeferredCustomAction"
BinaryKey="CustomActions"
Execute="rollback"
Return="check" />
<!-- Declare the rollback custom action -->
<CustomAction
Id="RollbackDeferredCustomAction"
DllEntry="RollbackDeferredCustomAction"
BinaryKey="CustomActions"
Execute="rollback"
Return="ignore" />
<!-- And it needs to be scheduled before the execution of your CA that you wish to rollback -->
<InstallExecuteSequence>
<Custom Action="RollbackDeferredCustomAction" After="DeferredCustomAction">1</Custom>
<Custom Action="DeferredCustomAction" After="InstallServices">1</Custom>
</InstallExecuteSequence>
Capturing stdout from a Process
Your CA may want to execute an executable and capturing stdout could be useful in debugging or displaying feedback.
// CustomActions.cs
// Disable process window spawns and redirects the specified streams
ProcessStartInfo processStartInfo = new ProcessStartInfo()
{
Arguments = arguments,
FileName = fileName,
UseShellExecute = false,
RedirectStandardOutput = captureStdout,
};
using (var process = new Process())
{
process.StartInfo = processStartInfo;
process.OutputDataReceived += (sender, args) => output += args.Data;
process.Start();
process.BeginOutputReadLine();
return output;
}
Windows Service
WiX supports the managements of Windows Services which is really helpful, particularly with upgrades.
<Component Id="WindowsServiceExe" Guid="*">
<File Id="WindowsServiceExeNS" Source="..\WindowsService\bin\Debug\WindowsService.exe" />
<ServiceInstall
Id="SomeService"
Name="SomeService"
Description="A Windows Service."
Type="ownProcess"
Start="auto"
ErrorControl="normal"
Interactive="no"
Account="[SERVICEACCOUNT_USERNAME]"
Password="[SERVICEACCOUNT_PASSWORD]"
Vital="yes" />
<!-- Only supported in MSI v5.0 so be careful! -->
<ServiceControl
Id="SomeServiceSc"
Name="SomeService"
Remove="uninstall"
Start="install"
Stop="uninstall"
Wait="yes" />
</Component>
Service Isolation
For Windows Vista and later, service SIDs based on the service name allowed authorization of resources to a specific service instead of the build-in identity it runs under e.g. LocalService. The MS Docs on using Service SIDs is important to read.
Unfortunately WiX doesn't support the restrictions of ACLs to service SIDs - the closes is the WiX PermissionEx Util Extension which can update ACLs on File
, Registry
, CreateFolder
and ServiceInstall
. Information seems scarce but something from Stackoverflow seems to require some knowledge in SDDL (shudder).
C# Custom Actions to modify ACLs
Much easier to modify ACLs at the expense of burying the details in the CA and a direct coupling that is easy to forget.
// Creating a SID like normally on the commandline
using (var process = new Process())
{
process.StartInfo.WorkingDirectory = workingDir;
process.StartInfo.FileName = "sc.exe";
process.StartInfo.Arguments = "sidtype someServiceName unrestricted";
process.StartInfo.Verb = "runas";
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.Start();
process.WaitForExit();
}
// Create an ACL for someUser and give it FullControl for example
var someUser = new SecurityIdentifier(SomeServiceSID);
var fullControlAccessRule = new FileSystemAccessRule(
someUser,
FileSystemRights.FullControl,
AccessControlType.Allow);
// List of existing or empty access rules you want to apply on the file/directory
var accessRules = new List<FileSystemAccessRule>()
{
fullControlAccessRule
};
// You can append or overwrite existing ACLs
var fileSecurity = file.GetAccessControl();
foreach (var accessRule in accessRules)
{
fileSecurity.AddAccessRule(accessRule);
}
// Now set or apply the access rules
file.SetAccessControl(fileSecurity);
There can be issues with modifying ACLs that aren't in canonical form which will cause an exception. A possible scenario would be if the machine needs to rejoin to the domain and has "orphaned" SIDs.
Drivers
The WiX DIFx Extension is very useful but also pretty horrible - just like Windows Installers. I have very limited knowledge in Windows drivers but the WiX extension needs the following files:
driver.cat
driver.inf
driver.sys
This can install kernel drivers if the driver.inf
specifies the DriverPackageType
:
[Version]
Signature = "$Windows NT$"
Class = Something
ClassGuid = {000}
Provider = %ProviderString%
CatalogFile = driver.cat
DriverPackageType = FileSystemMinifilter
This installs the driver to System32
else it will install it under System32/DriverStore
. A blog about installing filter drivers with the DIFx extension was one of the few resources.
Driver upgrades
DIFx (Windows Driver Install Framework) will automatically uninstall and upgrade its drivers. If you set difx:Driver/@ForceInstall
to "yes", then it may do a downgrade equivalent of Windows Installers.
There doesn't seem to be too much information around this as the WiX DIFx extension is based on the DIFx documentation which is no longer in active development.
If in your driver.inf to copy driver.sys to System32\Drivers
instead, specifying the difx:Driver/@DeleteFiles="yes"
will not remove any copied files.
Note Starting with Windows 7, the DIFxApp configuration flag to remove installed files, together with the
DriverDeleteFiles
attribute, are ignored by the operating system. Binary files, which were copied to a system when a driver package was installed, can no longer be deleted by using DIFxApp. MS Docs
Driver execution sequence
There wasn't much about when the install/uninstall of the drivers scheduling in the InstallExecuteSequence
- MsiProcessDrivers was exposed.
Basically the MsiProcessDrivers
custom action has the MsiInstallDrivers
and MsiUninstallDrivers
management inside it.
Using RunDll32.exe
Initially, one way was to using a CA to call the RunDll32.exe
- this is not recommended but if all else fails!
# rundll32.exe advpack.dll,LaunchINFSection inf filename[,section name][,flags][,smart reboot]
# This somehow works
cmd.exe /c RunDLL32.Exe syssetup,SetupInfObjectInstallAction DefaultInstall 128 .\driver.inf
# Apparently better methods are below
rundll32.exe setupapi.dll,InstallHinfSection DefaultInstall 128 .\driver.inf
rundll32.exe advpack.dll,LaunchINFSection .\driver.inf,,3,N
See also:
Driver resources
Signing the Installer
<!-- WiX Installer project .wixproj -->
<Target Name="SignCabs" Condition=" '$(PfxFile)' != '' AND '$(PfxPassword)' != '' AND '$(SignToolPath)' != '' ">
<Exec Command=""$(SignToolPath)" sign /debug /f $(PfxFile) /p $(PfxPassword) /fd SHA256 /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /td SHA256 "%(SignCabs.FullPath)"" />
</Target>
<Target Name="SignMsi" Condition=" '$(PfxFile)' != '' AND '$(PfxPassword)' != '' AND '$(SignToolPath)' != '' ">
<Exec Command=""$(SignToolPath)" sign /debug /f $(PfxFile) /p $(PfxPassword) /fd SHA256 /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /td SHA256 "%(SignMsi.FullPath)"" />
</Target>
There are a few ways of signing your WiX Bootstrapper/Installer.
WiX Bootstrapper and Installer UI
This is a big topic and generally it is best to use the WiX built-in dialogs as much as possible. It is common to see installers override certain dialogs - WiX Customizing Built-in WiXUi.
UI Level
If you want to detect what UI level the installer is running in, you can read the UILevel property.
WiX Heat
The WiX Heat tool is able to generate a ComponentsGenerated.wxs
file which can include all your build binaries - all components will meet the component rules. This is particularly useful for programs with a large number of binaries e.g. dotnet publish
, although with .NET 5, many files can be consolidated.
The Heat tool can be invoked in the VS Installer project .wixproj
where you can modify the Target
.
<!-- App.Installer.wixproj -->
<Target Name="BeforeBuild">
<!-- Remove any old builds -->
<Exec Command="rd /s /q $(SolutionDir)\BuildOutput\" />
<!-- Publish binaries to a BuildOutput folder -->
<Exec Command="dotnet publish ..\ -c Release -r win-x64 -o "$(SolutionDir)\BuildOutput\"" />
<!-- Define variables -->
<PropertyGroup>
<DefineConstants>BuildVersion=%(AssemblyVersion.Version);BasePath=$(SolutionDir)\BuildOutput\</DefineConstants>
</PropertyGroup>
<!-- Use the Heat tool to harvest the build binaries and apply any filters -->
<HeatDirectory
OutputFile="ComponentsGenerated.wxs"
DirectoryRefId="INSTALLFOLDER"
ComponentGroupName="GeneratedComponents"
SuppressCom="true"
Directory="$(SolutionDir)\BuildOutput\"
SuppressFragments="true"
SuppressRegistry="true"
SuppressRootDirectory="true"
AutoGenerateGuids="true"
GenerateGuidsNow="true"
ToolPath="$(WixToolPath)"
PreprocessorVariable="var.BasePath"
Transforms="$(SolutionDir)\ProjectInstaller\Transforms\FilterFiles.xsl" />
</Target>
See more of the configuration options for the HeatDirectory, most of the default settings can be kept. The WiX documentation is excellent, be sure to read further.
Heat transforms
The WiX Heat tool also supports any xml transform you wish to apply to the components generated. One way use-case is to exclude certain files from being harvested by the Heat tool - I think there are more elegant solutions to this. A Stackoverflow post details an example.
I don't recommend this way as it is similar to Custom Actions; in the sense it hides the internals of the Installer creating another layer of complexity. For example, if the transform was to exclude all .json
files, the team must remember how that was achieved - burying this business logic in an xml transform is not intuitive. A paid extension from Fire Giant - Heat Wave supports filtering in the .wxs
file which makes it far more ideal.
Further reading on Heat
Installer Upgrades
See [Windows Installer Upgrades]({{ site.baseurl }}
).
Burn Engine and WiX Bootstrapper
See [WiX Burn Engine]({{ site.baseurl }}
).
Tips
Access MSI properties in your localized resource files
For example, in your en-us.xml
file:
<String Id="Message_Foo">A localized string... [Property1]</String>
Then set the property:
<!-- Product.wxs -->
<SetProperty Id="Property1" Value="FooBar" Sequence="execute" After="FindRelatedProducts"/>
Setting pre-processor variables to MSI properties
<Property Id="Property1">$(var.Property1)</Property>
Copying MSI properties
There isn't a way to "copy" variables, you need to use a custom action to effectively mimic it.
<!-- This is a cleaner way compared to defining a CA and scheduling it -->
<SetProperty Id="PropertyNew" Value="[ProductName]" Before="AppSearch" />
WiX Include files
These act like C header files and is usual to avoid a very bloated Product.wxs
.
<!-- Configuration.wxi -->
<?xml version="1.0" encoding="utf-8"?>
<Include>
<?define ProductVersion="1.0.0"?>
<?define UpgradeCode = "{7761351c-3716-4284-979f-1bb12a2f7f5e}" ?>
</Include>
Modifying Add/Remove Programs
<!-- Remove Repair and Modify buttons -->
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" />
<Property Id="ARPNOMODIFY" Value="yes" Secure="yes" />
<!-- Add links, descriptions to the program in the Add/Remove Programs -->
<Property Id="ARPCONTACT" Value="!(loc.AppContact)"/>
<Property Id="ARPHELPLINK" Value="!(loc.AppHelpUrl)"/>
<Property Id="ARPURLINFOABOUT" Value="!(loc.AppInfoUrl)"/>
<Property Id="ARPURLUPDATEINFO" Value="!(loc.AppInfoUrl)"/>
Suggested Installer layout
After contributing to some WiX Installer projects, I was surprised there isn't a "standard" approach on the layout/structure. Given they vary in complexity, laying out some ground-work early can limit the snowball and mess an Installer project can look like.
A Visual Studio representation:
App
App
App.Tests
App.Database
App.Database.Tests
App Bootstrapper
This is the WiX Burn project - see...
There are duplicate files under /Themes
and /1033
, this is due to the default language to use for the Bootstrapper.
Bundle.wxs
BootstrapperVariables.wxs
/Resources
/1033
AppTheme.wxl
AppTheme.xml
SideDialog.png
/Themes
AppTheme.wxl
AppTheme.xml
SideDialog.png
App Installer
Localizing an Installer should be ready from the start, building it in later is a nightmare. In-fact, localizing an installer and integrating it into the CI/CD pipeline is very painful.
The InstallFlowSequence.wxs
file contains the InstallExecuteSequence
- moving this into its own file isn't recommended but for complex Installers (avoid anyway...) it makes it easier to determine what and when CAs are scheduled at.
Product.wxs
Variables.wxi
InstallFlowSequence.wxs
UninstallFlowSequence.wxs
/Lang
/1033
App.strings.en-us.wxl
Eula.en-us.rtf
/UI
CommonComponents.wxs
CustomDialog.wxs
WixUI_Basic_Override.wxs
App CustomActions
CustomAction.cs
App CustomActions Tests
Unit testing custom actions do exist - WiX Lux. Often custom actions are the cause for installation failures and become highly complex. It is strongly recommended to limit the number of CAs and its complexity.
CustomActionTests.cs
Last updated