Create MSI packages using PowerShell

Wix4 PowerShell module in action.

Overview

Okay, if you want to create an MSI file then you know what one is. It’s a file containing the installation media and instructions for some software. Also, because you clearly want to package some software into an MSI, you probably have some software that you want to install. Therefore, I’m going to assume that you know that there are several other methods of creating MSI files other than the method below. These other methods include, but are not limited to:

  • Professional packaging software (e.g., Installshield, Advanced Installer, etc.)
  • Visual Studio Installer Project
  • Wix Toolset
  • Some freeware products (AppDeploy Repackager comes to mind.)

Anyway, the fact that you are reading this article mean that you don’t have big professional needs for complex packaging, otherwise you would already be a customer of the professional packaging software. You are looking for something cheap, or better still, free! If so, and you are packaging a Visual Studio project, then the Visual Studio Setup/Installer project is worth a look. It’s easy once you get your head around it and integrates directly into your VS solution. It’ll even handle non-VS stuff with a bit of messing around. This is what I’ve been using for years without issue.

The next option is the Wix Toolset (https://wixtoolset.org/), which is an open-source project designed to create MSI packages. It’s a great option but there is no simple GUI and it will require a bit of work to get up to speed on how to create all the XML files required. It’s a great project and is the core of how I created this PowerShell solution.

Lastly, I mentioned some repackaging software. This records the current state of the machine, allows you to manually install your software, and then scans the machine again. It then creates an MSI based on the changes that happened to the machine in between. It’s messy and can include all sorts of noise that happened during the snapshots. It’s not considered to be a good plan these days.

Where’s the code? Just give us the code!!!

Anyway, given that you almost certainly found this page with the use of your favourite search engine, you know all that stuff and have decided you want to do it in PowerShell. You probably scrolled past my warming preamble without even reading it – Where’s the code? When are they going to get to the code? Okay, just a little further…

Now, why would you want to do this in PowerShell? Probably for the same reasons that I wrote this solution. I wanted a simple mechanism for creating MSI files for a handful of files, maybe a shortcut or two and occasionally throw in a registry key. VS Studio installer projects were working fine, but I wanted to package a PowerShell module, something that is usually a couple of files, but on this occasion, it included several hundred additional files (jpegs, xml files, general stuff), also, I was changing those additional files a lot and the VS installer/setup project just doesn’t like that at all. My search for a solution didn’t find anything simple or free. I had used the Wix Toolset during a period several years ago when Microsoft removed the option of the VS install project (they later bought it back to life). It was okay, but a bit messy at the time – it was a massive relief when Microsoft reinstated the VS Setup project.

Anyway, in my searching I came across a YouTube series by a guy called Rob Mensching about the new Wix Toolset v4 and I got pulled in (Link here). I highly recommend this to anyone interested in application packaging, but you do now need to set aside quite a bit of time to watch it all.

It’s an excellent series and at the time of writing this it’s currently 33 episodes long but was covering a lot about MSI packaging that I did not know. After a few episodes I thought that this could all be done with PowerShell rather than creating XML files by hand and I started to write code alongside the episodes that I watched. The series finally got to a point where it had covered all the things that I required to help package all the software that I ever write, and the module was finally ready to be used in anger. It currently supports the following:

  • Files (anything – Executables, DLLs, jpegs, text files, etc.
  • Shortcuts – Start menu/desktop shortcuts to your other files – In fact, you could put the shortcuts anywhere if you are crazy enough.
  • Registry values (DWORD and String at the minute, multi-string coming shortly).
  • Directories (and all of their child directories and files as well.
  • Icons – for your shortcuts or just for the icon in add/remove programs.
  • Services – This are a big part of what I write, so this was essential.
  • Folder permissions – Just in case you need to modify the NTFS permission of something during an install.

What I had not realised when I started was how simple the PowerShell would end up being and how simple it made my installer packages. For example, this is the code to create an MSI of the PowerShell module itself.

$Wix = New-Wix4Project -PackageName Wix4PowerShellModule -PackageVersion 1.0.0 -Manufacturer "x9000.com Consulting Services Limited" -UpgradeCode 649ee011-39d3-469b-a0ef-be13ea61911c -TargetPlatform x64
$InstallFolder = "[ProgramFiles6432Folder]\WindowsPowerShell\Modules\Wix4"
$SourceFiles = @()
$SourceFiles += "$PSScriptRoot\Wix4.psd1"
$SourceFiles += "$PSScriptRoot\Wix4.psm1"
$Wix.IncludeFiles($InstallFolder, $SourceFiles)
$Wix.SetAddRemoveProgramsIcon("$PSScriptRoot\Wix4Icon.ico")
$Wix.Save()
$Wix.Build()

The first line is the core – It creates the Wix project needs a few parameters – Package name, a version, a manufacturer and optionally an upgrade code GUID and a target platform (missing the last two defaults the platform to x64 and generates a new GUID)

The lines 2-5 create a folder to install it all (values in square brackets are standard folders (full list of these available here), the paths to two files (my PowerShell module and manifest files), and line 6 to bring them all together.

Line 7 adds my icon for Add\Remove programs, line 8 saves the project to several files (mainly .WXS and .wixproj – all just XML files) used by MSBuild to compile the MSI. Finally, line 8 compiles these files and your source files into an MSI. I add on another line to sign the MSI with a code signing certificate, but that is not necessary.

So, where is the module and how do I use it?

The module is already an easy to install MSI file. You can get it from the product page here or download the MSI directly from here.

Just install it and if you are feeling curious, you’ll find it installed under “C:\Program Files\WindowsPowerShell\Modules\Wix4

Note: You must have MSBuild.exe installed for the compilation to work – this probably means having a version of Visual Studio installed. This should work with ANY version installed, including the free community version You can even install just the MSBuild.exe by itself but it’s a bit tricky and I’m not going to cover that for you. Just install VS Studio, even if you are not using it right now, it will come in handy one day.

Here’s a couple more examples to help you out. Some of this is pseudo-code, so prepare for some copy/paste errors – this is just to get you the idea of the methods available:

#Complex service with user app and additional files\DLLs
$Wix = New-Wix4Project -PackageName "Intune Migration Service" -PackageVersion 1.0.5 -Manufacturer "x9000.com Consulting Services Limited" -UpgradeCode 807B4B77-F992-4B88-89EB-BE3840C558FD
$TopLevelSourceFolder = "C:\Users\PaulPrior\OneDrive - x9000.com Consulting Services Limited\_Repository\VSProjects\COPERNICUS\IntuneMigration"
$InstallFolder = "[ProgramFiles6432Folder]\Intune Migration Service"
$SourceFiles = @()
$SourceFiles += "$TopLevelSourceFolder\Intune Migration App\bin\Release\IntuneMigrationClient.exe"
$SourceFiles += "$TopLevelSourceFolder\Intune Migration App\bin\Release\IntuneMigrationClient.exe.config"
$SourceFiles += "$TopLevelSourceFolder\IntuneMigrationService\IntuneIcon.ico"
$SourceFiles += "$TopLevelSourceFolder\IntuneMigrationService\bin\x64\Release\IntuneMigrationService.exe"
$SourceFiles += "$TopLevelSourceFolder\IntuneMigrationService\AutopilotConfigurationFile.json"
$SourceFiles += "$TopLevelSourceFolder\IntuneMigrationWCF\bin\Release\IntuneMigrationWCF.dll"
$Wix.IncludeFiles($InstallFolder, $SourceFiles)
$Wix.SetAddRemoveProgramsIcon("$TopLevelSourceFolder\IntuneMigrationService\IntuneIcon.ico")
$Wix.AddShortcut("$TopLevelSourceFolder\Intune Migration App\bin\Release\IntuneMigrationClient.exe", "ProgramMenuFolder", "Intune Migration App", "$TopLevelSourceFolder\IntuneMigrationService\IntuneIcon.ico")
$Wix.AddRegistryString("HKLM", "SOFTWARE\Microsoft\Windows\CurrentVersion\Run", "IMSClientAutolaunch", """$InstallFolder\IntuneMigrationClient.exe"" /AUTOLAUNCH")
$Wix.AddServiceComponents("$TopLevelSourceFolder\IntuneMigrationService\bin\x64\Release\IntuneMigrationService.exe", "IntuneMigrationSVC", "Intune Migration Service", "auto", "Service to migrate devices from WS1 to Intune.")
$Wix.Save()
$Wix.Build()
# Sign the MSI
$SIGNTOOLPATH = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe"
Start-Process -FilePath $SIGNTOOLPATH -ArgumentList "sign /tr http://timestamp.sectigo.com /td SHA256 /fd SHA1 /sha1 2be3ce4e76fdda41b63b813d34f9034437b23d4d ""bin\x64\Release\Intune Migration Service.msi""" -NoNewWindow -Wait
# DeploymentTools
$Wix = New-Wix4Project -PackageName "Deployment Tools PowerShell Module" -PackageVersion 1.3.0 -Manufacturer "x9000.com Consulting Services Limited"
$TopLevelSourceFolder = $PSScriptRoot
$SourceFiles = @()
$SourceFiles += "$TopLevelSourceFolder\DeploymentTools.psd1"
$SourceFiles += "$TopLevelSourceFolder\DeploymentTools.psm1"
$ModuleInstallFolder = "[ProgramFiles6432Folder]\WindowsPowerShell\Modules\DeploymentTools"
$Wix.IncludeFiles($ModuleInstallFolder, $SourceFiles)
$Wix.IncludeDirectory("[ProgramFiles6432Folder]\WindowsPowerShell\Modules\DeploymentTools\_Extras", "$TopLevelSourceFolder\_Extras")
$Wix.IncludeDirectory("[ProgramFiles6432Folder]\WindowsPowerShell\Modules\DeploymentTools\_WINPE-AddedFiles", "$TopLevelSourceFolder\_WINPE-AddedFiles")
$Wix.IncludeDirectory("[ProgramFiles6432Folder]\DeploymentTools-PEAddon", "$TopLevelSourceFolder\DeploymentTools-PEAddon")
$Wix.Save()
$Wix.Build()

I’ll try and document this out a little bit in a future post, and hopefully by then Rob’s YouTube series will have covered additional functionality and I’ll have added it to the module.

In the meantime, if you want to reach out about this, the best way is to use twitter – my DMs are open: @Engineerasaurus

As a small legal point – all the usual caveats apply – This product may cause unforeseen side-effects or mild itching. If you experience problems, discontinue use. Also, if you are going to steal my work, please make a token effort to credit me or send a pile of cash in the post.