Simplify MSI Creation with PowerShell and WiX Toolset

Creating an MSI can become a surprisingly large job. Sometimes that is justified: installers can contain custom actions, complex user interfaces, drivers, patches and all sorts of other cleverness. Sometimes, though, you just want to install a folder of files, add a shortcut and perhaps create a registry value without adopting application packaging as a new hobby.

That was the reason I originally wrote this PowerShell module. It provides a friendly PowerShell interface over the WiX Toolset, and version 2.1.0 is a fairly substantial refresh. It now targets WiX Toolset 7, builds through the .NET SDK and keeps the simple object-oriented workflow of the original Wix4 module.

Where is the code? Just give me the code!

Fair enough. Here is a complete example that turns the contents of a Payload folder into an MSI:

PowerShell
Import-Module Wix

$Wix = New-WixProject `
    -PackageName "Example Application" `
    -PackageVersion "1.0.0" `
    -Manufacturer "Example Company" `
    -PackageId "ExampleCompany.ExampleApplication" `
    -TargetPlatform "x64" `
    -OutputPath (Join-Path $PSScriptRoot "Installer") `
    -AcceptWixEula

$Wix.IncludeDirectory(
    "[ProgramFiles6432Folder]\Example Company\Example Application",
    (Join-Path $PSScriptRoot "Payload")
)

$MSI = $Wix.Build()
$MSI.FullName

That creates the WiX project and source files, runs dotnet build, and returns the completed MSI as a FileInfo object. You can pass that straight into your signing or release process.

What is new in version 2.1.0?

The short version is that the module has grown up a little without becoming difficult to use. The main changes are:

  • WiX Toolset 7 SDK-style projects, built with dotnet build.
  • No Visual Studio or hard-coded MSBuild path required.
  • Stable WiX identifiers derived using SHA-256 instead of runtime GetHashCode() values.
  • A merged directory tree when several source folders share a destination.
  • Explicit identifiers for components, files, registry entries, shortcuts and services.
  • Safer, parameterised queries for Windows Installer database operations.
  • -WhatIf and -Confirm support when changing MSI properties or creating transforms.
  • Better path validation, terminating errors and reliable COM cleanup.
  • Compatibility with Windows PowerShell 5.1 and PowerShell 7.
  • Explicit WiX 7 EULA handling through the new -AcceptWixEula switch.

The old New-Wix4Project command is still there as a compatibility wrapper, so existing scripts do not need to be rewritten in one heroic afternoon. New scripts should use New-WixProject.

What can it package?

The module is aimed at straightforward Windows installers. It currently handles:

  • Selected files or complete directory trees.
  • Start menu and desktop shortcuts.
  • Add/Remove Programs icons.
  • String and integer registry values.
  • Windows services, including start, stop and removal behavior.
  • SDDL-based permissions on files and their containing folders.
  • Standard WiX user interfaces and RTF licence pages.
  • Public install-directory property mappings.
  • Reading, adding and updating properties in existing MSI files.
  • Creating MST transforms from two MSI databases.

It is deliberately not a wrapper for every WiX feature. If you need bundles, patches, drivers, complex custom actions, IIS configuration or an elaborate bespoke user interface, you will probably want to work directly with WiX authoring. This module is for the many jobs that are much less exciting than that.

Requirements

  • Windows PowerShell 5.1 or PowerShell 7.
  • A supported .NET SDK available on PATH.
  • Internet access for the first build, unless the required NuGet packages are already cached or available from an internal feed.
  • WiX Toolset 7 licensing appropriate for your use.

You can check the main prerequisite with:

dotnet --version

The separate global wix .NET tool is only required if you use New-MSITransform. Normal MSI projects build through the WiX MSBuild SDK.

A quick but important note about the WiX 7 EULA

WiX Toolset 7 requires an explicit acceptance gesture. The module will not quietly accept anything on your behalf. Review the WiX OSMF guidance and the WiX OSMF EULA, then use -AcceptWixEula if the appropriate person or organisation has accepted the terms.

With that switch, the module writes <AcceptEula>wix7</AcceptEula> into the generated project. Without it, Build() stops before invoking WiX and tells you where to find the relevant information. The switch does not change WiX 6 projects.

Installing the module

You can install version 2.1.0 using either of these options:

  • ZIP archive: download the module ZIP and copy the Wix folder into a directory listed in $env:PSModulePath.
  • MSI installer (Recommended method): download the MSI and let Windows install it for you. This is a device level installer so all users on the device will have access. This was actually packaged with the module itself in a bit of an Inception moment.

For a per-user ZIP installation, the usual destination is Documents\PowerShell\Modules\Wix for PowerShell 7 or Documents\WindowsPowerShell\Modules\Wix for Windows PowerShell 5.1. If Windows has marked the downloaded files as coming from the Internet, unblock them before importing the module.

PowerShell
Import-Module Wix -Force
Get-Command -Module Wix

Adding files, shortcuts and registry values

Use IncludeDirectory() for an entire folder tree, or IncludeFiles() when you only want selected files. Destination paths begin with a WiX standard directory such as [ProgramFiles6432Folder].

PowerShell
$Application = Join-Path $PSScriptRoot "Payload\ExampleApp.exe"
$Icon = Join-Path $PSScriptRoot "ExampleApp.ico"

$Wix.IncludeFiles(
    "[ProgramFiles6432Folder]\Example Company\Example App",
    @($Application)
)

$Wix.AddShortcut(
    $Application,
    "ProgramMenuFolder",
    "Example App",
    $Icon
)

$Wix.SetAddRemoveProgramsIcon($Icon)

$Wix.AddRegistryString(
    "HKLM",
    "SOFTWARE\Example Company\Example App",
    "InstalledVersion",
    "1.0.0"
)

The ordering matters: include a source file before attaching a shortcut, service or permission to it. This small rule avoids quite a lot of head-scratching.

Packaging a Windows service

PowerShell
$ServiceExecutable = Join-Path $PSScriptRoot "Payload\ExampleService.exe"

$Wix.IncludeFiles(
    "[ProgramFiles6432Folder]\Example Company\Example Service",
    @($ServiceExecutable)
)

$Wix.AddServiceComponents(
    $ServiceExecutable,
    "ExampleService",
    "Example Service",
    "auto",
    "Provides the Example Company background service."
)

The generated installer starts the service during installation, stops it for uninstall or reinstall, and removes it during uninstall. An overload is available for a service account and password, but do not place service-account passwords in source-controlled packaging scripts. They have an unfortunate habit of becoming less secret than intended.

Working with existing MSI files

Version 2.1.0 also exports a small set of commands for inspecting and changing Windows Installer databases:

PowerShell
$Version = Get-MSIProperty `
    -Path "C:\Packages\Application.msi" `
    -PropertyName "ProductVersion"

Add-MSIProperty `
    -Path "C:\Packages\Application.msi" `
    -PropertyName "MYPROPERTY" `
    -PropertyValue "Example" `
    -WhatIf

Update-MSIProperty `
    -Path "C:\Packages\Application.msi" `
    -PropertyName "MYPROPERTY" `
    -PropertyValue "Replacement"

Add-MSIProperty and Update-MSIProperty support -WhatIf and -Confirm. Direct database changes invalidate an existing digital signature, so make all changes first and sign the final MSI afterwards.

Migrating an old Wix4 script

The common methods from the original module are still available, including IncludeDirectory(), IncludeFiles(), AddShortcut(), AddServiceComponents(), the registry and permission methods, Save() and Build().

For an existing product, keep its established UpgradeCode. Changing that identity prevents Windows Installer from recognising the old and new releases as members of the same product family. I recommend copying the existing build script, switching the module import, adding an explicit output path, calling Save() and reviewing the generated WiX files before attempting an upgrade test.

Documentation and downloads

The full guide covers every exported command and public project method, complete packaging scenarios, CI/CD, signing order, migration and troubleshooting. In other words, it contains the details that would turn this pleasantly readable article into a small book.

That is probably enough installer excitement for one day

The module is intended to keep ordinary MSI jobs ordinary. If you only need to install some files, create a shortcut, add a registry value or run a Windows service, the packaging script should be short enough to understand when you return to it six months later.

The usual caveats apply: test installation, repair, upgrade and uninstall before distributing anything widely. This software may still cause unforeseen side effects or mild itching. If symptoms persist, inspect the generated WiX source and make another cup of tea.

Leave a Reply