Packaging a Python Desktop Tool as MSIX for the Microsoft Store
2026-04-27
Tags: Tutorial · Python · Windows · MSIX · Packaging
Introduction
In a previous article, I wrote about the motivation and process behind developing RDP Heartbeat. At the end of that post, I mentioned:
I'm planning to write an article detailing the entire Python-to-MSIX packaging pipeline when I have time.
Here it is.
Honestly, getting a Python desktop tool onto the Microsoft Store is much more involved than using C# / WinUI 3. With the latter, Visual Studio handles most of it with a few clicks. But if you're like me and use Python, the path looks like this:
Python source → PyInstaller → EXE → Inno Setup → Installer → MSIX Packaging Tool → MSIX → StoreEvery step has its pitfalls, but every step also has solutions. This article is the roadmap and walkthrough.
This article uses the RDP Heartbeat project as an example, but the methods and approach apply to any Windows desktop tool written in Python + Tkinter (or PyQt, wxPython, etc.).
A side note: theoretically, you might be able to skip Inno Setup and use the MSIX Packaging Tool directly on a PyInstaller-generated EXE. We chose to go through Inno Setup first mainly because it makes self-distribution easier (e.g., putting a Setup.exe on GitHub Releases for people to download). It may not be the theoretically optimal approach, but it's the one we actually got working, so we're sharing it.
Reference source code: The complete project code discussed in this article is available at [email protected], including
build_release.py,setup.iss, and all packaging configurations.
Overview: The Four-Stage Pipeline
Before we start, let's lay out the entire chain:
- PyInstaller:
.py→.exe - Inno Setup:
.exe→ Installer - MSIX Packaging Tool: Installer →
.msix - Microsoft Store: Submission
Why so roundabout? Because the Microsoft Store ultimately requires an MSIX format package, and the MSIX Packaging Tool works by "converting" a traditional installer that already installs and runs correctly — it monitors the installation process, captures all file writes, registry changes, and shortcut creation, then automatically generates an MSIX. So you first need a clean, complete traditional installer.
In other words: PyInstaller handles "it runs", Inno Setup handles "it installs", and the MSIX Packaging Tool handles "it can be listed on the Store".
Let's go step by step.
Step 1: PyInstaller — Turning Python into an EXE
1.1 Prepare Your Project
Before you start, make sure your Python project has a clear structure and a well-defined entry point. Take RDP Heartbeat as an example:
prune-rdp-heartbeat/
├── main.py # Entry script
├── heartbeat_window.py # Core window logic
├── win_utils.py # Win32 API wrappers
├── tray_icon.py # System tray
├── settings_dialog.py # Settings panel
├── about_dialog.py # About dialog
├── config_manager.py # Configuration management
├── i18n.py # Internationalization
├── startup.py # Auto-start on boot
├── logger.py # Logging
├── version.py # Version number
├── icon.ico # Application icon
├── icon.png # Source icon (for generating MSIX resources)
├── requirements.txt # Dependencies
└── packaging/ # MSIX-related resources (used later)Dependencies in requirements.txt:
pystray
Pillow
pywin32
customtkinter
packaging1.2 Handling PyInstaller's "Hidden Dependencies"
PyInstaller uses static analysis to detect dependencies, but some libraries (especially pystray, PIL) aren't detected and need to be manually declared:
# Key configuration in build_release.py
cmd = [
sys.executable, "-m", "PyInstaller",
"--noconsole", # No console window
"--onefile", # Bundle into a single exe
"--name=RDPHeartbeat",
"--clean",
"--icon=icon.ico",
"--add-data=icon.ico;.", # Windows uses semicolons; Linux/macOS use colons
"--hidden-import=PIL._tkinter_finder",
"--hidden-import=pystray",
"main.py"
]Also, resource files (like icons) are extracted to a temporary directory after packaging, so you need resource_path() for compatibility:
# Approach in tray_icon.py
def resource_path(relative_path):
try:
base_path = sys._MEIPASS # PyInstaller's temp directory
except Exception:
base_path = os.path.abspath(".") # Dev mode: current directory
return os.path.join(base_path, relative_path)Any resource that needs to be read from the filesystem (icons, fonts, config file templates, etc.) should go through this function.
1.3 One-Click Build Script
Wrap the PyInstaller command into build_release.py:
# Read version.py to get the version number
from version import APP_VERSION
def run_build():
# 0. Clean previous build artifacts
if os.path.exists("build"): shutil.rmtree("build")
if os.path.exists("dist"): shutil.rmtree("dist")
# 1. Run PyInstaller
subprocess.check_call([...])
# 2. Verify output
exe_path = os.path.join("dist", "RDPHeartbeat.exe")
assert os.path.exists(exe_path), "Build failed!"Run python build_release.py, and if everything goes well, you'll get RDPHeartbeat.exe in dist/.
You can double-click it to verify it works properly.
Step 2: Inno Setup — Turning the EXE into a Proper Installer
The MSIX Packaging Tool needs an installer as its conversion source. In the Windows ecosystem, Inno Setup is a free, mature, scriptable installer creation tool.
2.1 Install Inno Setup
Download and install from jrsoftware.org. It's also recommended to install Inno Script Studio (a visual editor), which saves a lot of effort.
2.2 Writing the Install Script
The core file setup.iss:
#define MyAppName "RDP Heartbeat"
#define MyAppVersion "1.1.3.0"
#define MyAppPublisher "Prune Lab"
#define MyAppExeName "RDPHeartbeat.exe"
[Setup]
AppId={{DDEC247A-5DC0-4393-BB13-466CCAD7F90B} ; Unique GUID per application
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName} ; Default to Program Files
ArchitecturesAllowed=x64compatible ; 64-bit systems
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes ; Skip "Select Program Group" page
OutputBaseFilename=RDPHeartbeat_Setup ; Output filename
SolidCompression=yes
WizardStyle=modern ; Modern wizard style
SetupIconFile=icon.ico
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; Flags: unchecked
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,...}"; Flags: nowait postinstall skipifsilentA few things worth noting:
- AppId must be unique: This is how Windows identifies the application. Don't conflict with other software. Inno Setup IDE can generate a GUID with one click.
- No need to write registry entries: MSIX conversion will automatically handle registry writes during installation, but our app doesn't need them anyway, so it's cleaner to skip.
2.3 Automated Version Number Sync
Your version number might be defined in Python code (version.py), and there's also a copy in setup.iss. Manual syncing will eventually lead to mistakes. So add an auto-patch section in build_release.py:
def patch_setup_iss():
with open("setup.iss", 'r', encoding='utf-8') as f:
content = f.read()
content = re.sub(
r'#define MyAppVersion ".*?"',
f'#define MyAppVersion "{APP_VERSION}"',
content
)
with open("setup.iss", 'w', encoding='utf-8') as f:
f.write(content)Now when you run python build_release.py, it will automatically write the latest version into setup.iss, then run PyInstaller.
2.4 Compiling the Installer
Open Inno Setup IDE, load setup.iss, and click Compile. The output is RDPHeartbeat_Setup.exe.
Verify: Run the installer on a clean VM or another computer and confirm:
- It installs correctly
- The program launches
- No leftovers after uninstall (Inno Setup's uninstaller is at
{app}\unins000.exe, MSIX will package it too, that's fine)
Step 3: MSIX Packaging Tool — From Installer to Store Format
If you plan to publish to the Microsoft Store, it's recommended to first complete Partner Center registration and reserve your app name (see Step 4), because the Package Name and Publisher filled in MPT need to match the Store's reserved information. If you're only testing yourself, you can skip registration and use any name.
3.1 Prepare the Environment
You'll need:
- MSIX Packaging Tool (Official docs)
- A clean environment (recommended to use a VM or a separate computer, because MPT monitors the entire installation process — other programs on your system might introduce noise)
- Signing certificate: For testing, you can use a self-signed certificate. Run in PowerShell (admin):
New-SelfSignedCertificate -Type Custom -Subject "CN=YourName" -KeyUsage DigitalSignature -FriendlyName "Your Cert" -CertStoreLocation "Cert:\CurrentUser\My" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")Export the certificate for later use:
$password = ConvertTo-SecureString -String "YourPassword" -Force -AsPlainText
Export-PfxCertificate -Cert "Cert:\CurrentUser\My\<Thumbprint>" -FilePath "C:\cert.pfx" -Password $passwordWhen officially publishing, the Microsoft Store will re-sign with its own certificate, so you don't need to purchase a code signing certificate. But if you want to distribute outside the Store (e.g., offering MSIX downloads on your own website), you'll need a proper code signing certificate.
3.2 Running the MSIX Packaging Tool
- Open MPT, select "Application package" → "Create a new package".
- In the "Select environment" step, keep the defaults and click Next.
- Key step: MPT will ask you to select the installer path. Choose the
RDPHeartbeat_Setup.exeyou just generated. - MPT starts monitoring the system and launches the installer. Complete the installation normally — if the installer checks "Launch RDP Heartbeat" at the end, that's fine too.
- After installation completes, click "Next" — MPT stops monitoring.
- MPT will list all processes it detected. Uncheck any that don't belong to your app (e.g., system processes, antivirus updates). Typically, only keep things like
RDPHeartbeat.exe,unins000.exe. - Fill in the application information:
- Package name: If publishing to the Store, use the name assigned by Partner Center when you reserved the app (e.g.,
PruneLab.RDPHeartbeat). If testing locally, you can use any name — recommended format isYourName.YourApp. - Package display name: The app's display name, e.g.,
RDP Heartbeat. - Publisher display name: Publisher name, e.g.,
Prune Lab. - Publisher: If publishing to the Store, use the Publisher certificate subject from your Partner Center account (e.g.,
CN=21C32622-53AF-45A6-8AB1-B35BCD475CAD). If testing locally, use the Subject from your self-signed certificate (e.g.,CN=YourName). - Version: Automatically extracted from the installer.
- Package name: If publishing to the Store, use the name assigned by Partner Center when you reserved the app (e.g.,
- Specify the output directory and generate the MSIX package.
3.3 Testing the MSIX Package
After generating the .msix file, don't rush to edit the manifest. Double-click to install and see if it runs properly. If you're using a self-signed certificate, you need to import it into the system's "Trusted People" store first (see the certificate export command in the previous section — use Import-PfxCertificate).
Step 4: Submitting to the Microsoft Store
4.1 Registration and Name Reservation
- Register for a Microsoft Partner Center developer account.
- In Partner Center, reserve your app name (Products → New Product → enter the name). After reserving, you'll get the Package Name and Publisher information, which must match the
AppxManifest.xmlin your MSIX package.
4.2 Submission Process
- Go to your app → Submissions → New submission.
- Fill in the fields:
- Packages: Upload your
.msixfile - Description: App description (supports multiple languages, recommend at least English and Chinese)
- Screenshots: At least one 1366×768 app screenshot
- Store listing: Icons, promotional images, feature list
- Packages: Upload your
- Submit for review. Initial review typically takes 1-3 business days.
Conclusion: Is This Path Worth It?
If you're looking to make big money from a Python tool, the answer is "probably not." Rewriting with WinUI 3 / .NET would give you a much smoother packaging experience.
But if you're like me — you already have a Python tool that you use regularly and want to share it with more people — then while this process is roundabout, it at least works.
Looking back at the pipeline:
| Stage | Tool | Output |
|---|---|---|
| .py → .exe | PyInstaller | Single exe |
| .exe → Installer | Inno Setup | Setup.exe |
| Installer → MSIX | MSIX Packaging Tool | .msix |
| Submission | Partner Center | Store listing |
Hope this article helps you avoid some detours. If you've also gotten a Python tool onto the Store, feel free to share your experience at RDP Heartbeat's GitHub.