← Blog |

Package Any MSI as an Intune Win32 App in Just 3 Clicks

By John Marcum

Package Any MSI as an Intune Win32 App in Just 3 Clicks

Most of the Win32 apps I package for Intune are the same job: the vendor ships one MSI, maybe with a transform, and it needs to land on devices silently with a detection rule that actually works. It’s not hard. It is repetitive, though, and repetitive work is where the mistakes creep in.

For years my process was to copy the install script from the last package, edit the hardcoded names, copy the detection script, edit those names too, run IntuneWinAppUtil.exe, and then type the install command and return codes into the portal. Every copy drifted a little from the one before it. A bug fixed in one package lived on in ten others.

Of course, we’d love for everyone to be using App Store for Intune, which automates this whole process for over 13,000 apps and supports your own custom apps via upload. We understand that’s not possible for everyone, though.

So for everyone packaging by hand, I built a tool that does the whole thing. Drop the MSI in a folder, run one script, fill in one window, and you get back:

  • An install script
  • A detection script
  • An uninstall script, when the MSI is a bootstrapper that needs one
  • The .intunewin package
  • A deployment info sheet with every value you need for the Intune portal

There’s no script editing involved at all, and it’s free.

Download the tool

The builder, its templates, and everything it needs are in the PowerStacks intune-scripts repository on GitHub:

github mark2

Get it here

Copy the whole MsiPackagingTools folder, not just the .ps1. The builder needs its Templates, Assets, and PoshUI folders next to it. If you downloaded it as a zip, unblock it once with Get-ChildItem -Recurse | Unblock-File.

Built on PoshUI

The builder window isn’t something I wrote from scratch. It’s drawn by PoshUI, an open-source PowerShell UI framework by KAnders (Kanders-II on GitHub). PoshUI lets you build Windows 11 style wizards, dashboards, and free-form apps from ordinary PowerShell cmdlets, with no WPF, XAML, or C# required. This tool uses its PoshUI.Canvas module.

Before PoshUI, the builder asked its questions as a string of console prompts. It worked, but it was the kind of thing only the person who wrote it enjoys using. PoshUI turned it into a three-page window that anyone on a packaging team can pick up without reading the code first. If you write PowerShell tools for other people, it’s well worth a look.

PoshUI 1.4.1 is included unmodified in the PoshUI folder under its MIT license. That license, including KAnders’ copyright notice, is in PoshUI\LICENSE, and it needs to stay with the folder whenever you copy or share it. Thank you, KAnders, for building it and giving it to the community.

Why I didn’t just keep copying scripts

Three problems kept biting, roughly in order of how much pain they caused, and there were two more reasons on top of those.

Vendor command lines are often wrong, and they fail silently

An app owner sends you a command line. It looks plausible. The install returns 0, Intune reports success, and the setting never gets applied.

I hit this on a real package. The app owner supplied:

BrowserUrl=https://yourtenant.example.com

Both halves of that are wrong. The string BrowserUrl appears nowhere in the MSI, so nothing reads it. And Windows Installer only treats a property as public, meaning settable from the command line, when its name is entirely uppercase. Mixed-case properties are private and get discarded in the elevated part of the install, which is exactly the context an Intune SYSTEM install runs in.

The real property was BROWSERURL. Getting it wrong meant the registry value was written as an empty string and nothing anywhere reported a problem.

The part that surprised me: BROWSERURL has no row in the MSI’s Property table. Whether a property is public has nothing to do with whether it’s declared, so the properties a vendor tells you to set are often missing from the obvious place to look. The builder reads three sources instead of one:

SourceWhat it means
PropertyDeclared in the Property table, so it has a default
SetupUIBound to a control in the setup UI, a field a human would type into
ChainedReferenced as [PROPERTY] in a chained sub-package’s command line

Anything found only through SetupUI or Chained is called out at the top of the list, because those are the ones you’d otherwise miss. Type a mixed-case name and the builder warns you, and tells you if the MSI exposes the uppercase spelling.

Version comparison has a trap that causes reinstall loops

An MSI’s ProductVersion is often three fields (1.43.8), while the DisplayVersion in the registry is four (1.43.8.0). In PowerShell, [version]'1.43.8' has a Revision of -1, so:

Terminal window
[version]'1.43.8' -ge [version]'1.43.8.0' # $false

A detection script built on that comparison says “not installed” on a device where the app is installed. Intune reinstalls it every evaluation cycle, forever.

Version checking in the builder is optional. It’s a single checkbox, Perform version check, on the first page. Leave it unticked and detection only looks for the product name, which is the right choice for apps that update themselves, like Adobe Acrobat Reader. If you do use it, the generated scripts normalize every version to four fields before comparing, and use -ge so a newer version still counts as installed.

Either way, there’s one more protection against the same loop: if the name matches but the installed version is missing or can’t be parsed, the app is treated as installed and a warning is logged, rather than running msiexec over the top of it.

Hand-copied scripts drift apart

All the shared logic lives in templates. The generated scripts are disposable output. Fix a bug once in the template, regenerate, and every package that gets rebuilt picks it up.

A detection script is the most reliable detection method

In my experience, a custom detection script is the most reliable way to detect a Win32 app. Intune’s built-in rules each depend on one thing staying put, and none of them reliably do. MSI product codes change between releases. File names, versions, and install paths change. Personally, I’ve found registry rules unreliable too. A script can check both the 64-bit and 32-bit uninstall keys, normalize versions, and fail safe when something’s missing, and detection by script has always worked for me.

The catch is that someone has to write that script for every app. That’s exactly the part the builder takes off your plate.

You don’t need to be comfortable in PowerShell

Not every Intune admin writes PowerShell, and they shouldn’t have to in order to package an app properly. Because the builder generates the install, detection, and uninstall scripts for you, anyone on the team can produce a solid package by filling in a window. The people who do know PowerShell can still read every line of what it generates, and fix the templates when something needs to change.

Using the builder

You need Windows PowerShell 5.1 (or PowerShell 7 on Windows), an elevated session on an interactive desktop, and IntuneWinAppUtil.exe from Microsoft’s Win32 Content Prep Tool if you want the .intunewin built for you. The MSI is read through the WindowsInstaller.Installer COM object that’s already on every supported Windows build, so there’s nothing else to install.

Put the MSI (and its .mst or .msp, if it has them) in a clean folder of its own. Everything in that folder goes into the .intunewin, so don’t leave anything else in there.

1. Run the builder

From an elevated PowerShell prompt, with no arguments:

Terminal window
cd C:\MsiPackagingTools
.\New-MsiDeploymentScripts.ps1

Running New-MsiDeploymentScripts.ps1 from an elevated PowerShell prompt

The builder window opens. It has three pages, and Back works on every one of them without losing what you entered.

2. Package page

Pick the MSI. Optionally pick a package folder (leave it empty to use the folder the MSI is in), a transform, and a patch. Then decide on the version check: leave it on for apps you upgrade by repackaging, turn it off for apps that update themselves.

The Package page of the MSI deployment script builder

Select Next and the builder reads the MSI, through the transform and patch if you chose them. A patch matters here because it changes the version and often the product name, and detection has to match what actually ends up installed. A patch that was built for a different product is rejected before anything is packaged.

3. Properties page

This is the page to actually read. The top shows what the builder found: product name, version, product code, publisher. Below that is every property you can set on the command line, with the undeclared ones flagged in orange.

The Properties page listing every settable MSI property, with undeclared properties flagged

Most packages need nothing here, so Next is all this page asks of you. To set a property, select its row, type a value, and select Set value. If a transform already sets a property, that shows in the Transformed value column. A value you set here goes on the msiexec command line and wins over the transform without changing the .mst.

Bootstrapper MSIs get a warning on this page too, more on those below.

4. Build page

The Build page shows the complete msiexec command line the install script will run, so you can check it before anything is generated. Give it a short product name if you want readable file names, leave Build the .intunewin package ticked, and select Generate.

The Build page showing the full msiexec command line and build options

The build log streams into the window. When it finishes you get the install and uninstall commands with Copy buttons, and Open package folder takes you straight to the output.

The Build page after a successful build

What you get

The generated install script, detection script, .intunewin and deployment info sheet in the package folder

FileWhat to do with it
Install-<Product>_V<Version>.ps1Already inside the .intunewin
Detect-<Product>_V<Version>.ps1Upload to Intune separately as the custom detection script
Uninstall-<Product>_V<Version>.ps1Inside the .intunewin, only generated for chained MSIs
Install-<Product>_V<Version>.intunewinUpload to Intune as the app package
<Product>_V<Version>-IntuneDeploymentInfo.txtYour reference, not shipped

The info sheet is the part I use most. It has the install command, uninstall command, return codes, detection settings, and client log paths, all ready to paste.

Creating the app in Intune

In the Intune admin center, go to Apps > Windows > Add > Windows app (Win32) and select the .intunewin. Take Publisher and App Version straight from the info sheet. They come from the MSI, so they match what detection looks for. The Intune screenshots in this section are from a different package than the Node.js example above.

Intune App information tab with the generated .intunewin selected and Publisher and App Version taken from the MSI

On the Program tab, set the installer and uninstaller type to PowerShell script and browse to the generated scripts. Set Device restart behavior to Determine behavior based on return codes. That’s what lets the script’s exit codes drive restarts.

Intune Program tab with the generated install and uninstall scripts and restart behavior set from return codes

The install script passes msiexec’s real exit code through to Intune rather than flattening it. 3010 stays 3010, 1641 stays 1641, and 1618 (another install holding the Windows Installer lock) comes back as a retry instead of a failure. Those all line up with Intune’s default return codes, so normally there’s nothing to change. Don’t add 1 as a success code to make a red app turn green. It means the install really failed, and the log will tell you why.

On Detection rules, choose Use a custom detection script, upload the Detect- script, and leave both Run script as 32-bit process and Enforce script signature check set to No.

Intune Detection rules tab using the generated custom detection script

The detection script matches DisplayName plus a minimum version in both the 64-bit and 32-bit uninstall keys, rather than the MSI product code. A lot of vendors change the product code on every release, and the built-in MSI rule can’t express “this version or newer”. The info sheet still lists the product code if you’d rather use it for a given app.

Bootstrapper MSIs

Some MSIs are bootstrappers that chain a set of sub-MSIs. The builder detects this and tells you, because it changes three things:

  • Restart suppression doesn’t propagate. /norestart applies to the parent. A sub-package that ignores it can trigger a restart.
  • Properties don’t propagate either. A property only reaches a sub-package if the parent forwards it explicitly.
  • Each sub-package registers its own Add/Remove Programs entry, so uninstalling the parent alone can leave orphans behind.

For chained MSIs the builder generates an uninstall script that removes the parent first, waits for the vendor’s chainer to finish, then removes any component that’s still registered, one product code at a time. Every product code is fixed at build time, so the script can never remove something unrelated. If a prerequisite’s product code isn’t recorded anywhere in the MSI, the builder names it so you can find the code on a test device and pass it back in with -AdditionalProductCode.

Running it unattended

The window is a front end for the same script. Every value can be passed on the command line, and -NonInteractive skips the window entirely, which makes it usable in a pipeline:

Terminal window
.\New-MsiDeploymentScripts.ps1 -PackageFolder 'C:\Packages\MyApp' `
-MsiProperties ([ordered]@{ BROWSERURL = 'https://example.contoso.com' }) `
-NonInteractive -BuildIntuneWin -Force

The window’s Generate button runs exactly this, so the window and a pipeline produce identical output. Over PowerShell Remoting, in a scheduled task, or with -NoGui, the builder falls back to console prompts automatically. Run Get-Help .\New-MsiDeploymentScripts.ps1 -Full for every parameter and more examples.

A few rules I follow

  • Never trust a vendor-supplied property name. Check it against the MSI. Uppercase, or it isn’t public.
  • Read the Properties page before selecting Next. The undeclared-property list and the bootstrapper warnings are the whole point.
  • Verify the property landed on a pilot device. An exit code of 0 proves nothing here, because it returns 0 either way.
  • Fix logic in the templates, not in a generated script. Anything you change in a generated script is lost the next time it’s rebuilt.
  • Test the uninstall for any chained MSI before it reaches production.

What it doesn’t do

It installs exactly one MSI. Multi-MSI installs, EXE installers, and anything needing user interaction need a different approach (PSADT is the usual answer for the last one). Detection works by reading the product name and version out of the MSI at build time (through any transform or patch) and baking them into the detection script, which then looks for a matching DisplayName and DisplayVersion in Add/Remove Programs, the uninstall keys in the registry. That covers any well-behaved MSI, but an app that needs file or file-version detection needs its own detection script.

If you’re packaging by hand, this builder has saved me a lot of time, and I hope it does the same for you.

As always, these scripts are provided as-is. Test in a lab before you use them in production.

IntuneWin32PowerShellApp PackagingFree Tools