Skip to main content

Packaging app for Windows

Instructions for packaging a Flet app into a Windows application.

Info

This guide provides detailed Windows-specific information. Complementary and more general information is available here.

Alternative: flet pack

For a PyInstaller-based way to package desktop apps — no Visual Studio or Flutter toolchain required — see flet pack.

Prerequisites​

Visual Studio​

Visual Studio (2022 or 2026) is required with the Desktop development with C++ workload installed.

Follow this guide for instructions on downloading and installing correct Visual Studio components for Flutter desktop development.

flet build windows​

Note

This command can be run on Windows only.

Builds a Windows application.

Windows on ARM​

On an ARM64 PC, such as a Snapdragon laptop, flet build windows builds an x64 app, which runs there through Windows' built-in x64 emulation. Flet's Python runtime for Windows is x64-only, so Flet builds with its own Flutter SDK, which is x64 as well, even when an ARM64 Flutter SDK is on PATH. The first start of the app takes noticeably longer under emulation.

App termination​

Closing the window terminates the app immediately, without running Python atexit handlers, C++ static destructors, or flushing writes that have not yet reached the operating system. See How a built app terminates.

Distributing​

flet build windows leaves a self-contained folder in build\windows: your app's .exe next to the DLLs it loads and the data, DLLs, Lib, site-packages and app folders. The PCs your app runs on need no Python, and Flet itself needs no Visual C++ Redistributable, because msvcp140.dll, vcruntime140.dll and vcruntime140_1.dll ship in the folder. A package with native code that needs other Visual C++ DLLs, such as msvcp140_1.dll or vcomp140.dll, and doesn't bundle them needs the Redistributable on the user's PC. A folder of DLLs isn't something an end user can download and double-click, though, so wrap it in one of the formats below, or publish it in the Microsoft Store.

Two rules apply to every format:

  • Keep the folder together: The .exe loads its DLLs, data and Python runtime from its own folder, so copied on its own it fails with The code execution cannot proceed because battery_plus_plugin.dll was not found. The folder itself can live anywhere, including C:\Program Files, where the running app has no write access, since Flet writes nothing into its own folder. Make sure your code doesn't either: save files in FLET_APP_STORAGE_DATA, which is also the app's working directory, and never next to __file__, which points into the folder.
  • Keep the company and product names stable: The app keeps its data in %APPDATA%\<company>\<product> and its console log in %LOCALAPPDATA%\<company>\<product>. Rename either between releases and the upgraded app starts with empty storage, while the old data stays behind in the previous folder. Set the company name before your first release: without one, it is Your Company.

Inno Setup turns the folder into a single setup.exe that installs the app, adds it to the Start menu, registers an uninstaller under Settings → Apps, and upgrades an existing installation in place.

License

Inno Setup is free to use, commercially too. Since version 6.5, commercial users are asked, though not required, to buy a license.

Install Inno Setup 6.6 or later, from its download page or with winget:

winget install --id JRSoftware.InnoSetup -e

This installs it for your user only, in %LOCALAPPDATA%\Programs\Inno Setup 6, even from an administrator terminal. Add --scope machine to install it for all users, in C:\Program Files (x86)\Inno Setup 6. Neither adds it to PATH. Inno Setup 7 (JRSoftware.InnoSetup.7) compiles the script below unchanged, and installs into an Inno Setup 7 folder instead.

Save the script below as installer.iss in your project's root directory, next to pyproject.toml, since its paths are relative to that directory. Then edit the two values at the top (AppId and AppExe), and compile it from that directory after each flet build windows:

& "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" installer.iss

Adjust the path for an all-users install or for Inno Setup 7. You can also open the script in the Inno Setup Compiler and choose Build → Compile. Either way the installer lands in build\installer, named after your artifact name and version, for example my_app-1.0.0-setup.exe.

installer.iss
#define AppId "com.example.my_app"
#define AppExe "my_app.exe"
#define BuildDir "build\windows"

#define AppExePath AddBackslash(SourcePath) + BuildDir + "\" + AppExe
#define ArtifactName Copy(AppExe, 1, Len(AppExe) - 4)
#if !FileExists(AppExePath)
#error AppExe is not in BuildDir: run "flet build windows" first, or fix the values above.
#endif
#define AppName GetStringFileInfo(AppExePath, "ProductName")
#define AppPublisher GetStringFileInfo(AppExePath, "CompanyName")
#define FullVersion GetStringFileInfo(AppExePath, "ProductVersion")
#define AppVersion Copy(FullVersion, 1, Pos("+", FullVersion + "+") - 1)

[Setup]
AppId={#AppId}
AppName={#AppName}
AppVersion={#AppVersion}
AppPublisher={#AppPublisher}
VersionInfoVersion={#GetVersionNumbersString(AppExePath)}
UninstallDisplayName={#AppName}
UninstallDisplayIcon={app}\{#AppExe}
DefaultDirName={autopf}\{code:SafeAppName}
DisableProgramGroupPage=yes
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
WizardStyle=modern dynamic
SetupIconFile=build\flutter\windows\runner\resources\app_icon.ico
OutputDir=build\installer
OutputBaseFilename={#StringChange(ArtifactName, " ", "-")}-{#AppVersion}-setup
SolidCompression=yes

[Tasks]
Name: desktopicon; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked

[InstallDelete]
Type: filesandordirs; Name: "{app}\app"; Check: IsUpgrade
Type: filesandordirs; Name: "{app}\data"; Check: IsUpgrade
Type: filesandordirs; Name: "{app}\DLLs"; Check: IsUpgrade
Type: filesandordirs; Name: "{app}\Lib"; Check: IsUpgrade
Type: filesandordirs; Name: "{app}\site-packages"; Check: IsUpgrade
Type: files; Name: "{app}\*.dll"; Check: IsUpgrade

[Files]
Source: "{#BuildDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs

[Icons]
Name: "{autoprograms}\{code:SafeAppName}"; Filename: "{app}\{#AppExe}"
Name: "{autodesktop}\{code:SafeAppName}"; Filename: "{app}\{#AppExe}"; Tasks: desktopicon

[Run]
Filename: "{app}\{#AppExe}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

[Code]
function SafeAppName(Param: String): String;
var
I: Integer;
begin
Result := '{#StringChange(AppName, "'", "''")}';
for I := Length(Result) downto 1 do
if Pos(Result[I], '\/:*?"<>|') > 0 then
Delete(Result, I, 1);
end;

function IsUpgrade: Boolean;
begin
Result := CompareText(RemoveBackslash(WizardForm.PrevAppDir), RemoveBackslash(WizardDirValue)) = 0;
end;

function NextButtonClick(CurPageID: Integer): Boolean;
var
Rec: TFindRec;
begin
Result := True;
if (CurPageID = wpSelectDir) and not IsUpgrade and FindFirst(AddBackslash(WizardDirValue) + '*', Rec) then
try
repeat
if (Rec.Name <> '.') and (Rec.Name <> '..') then
begin
SuppressibleMsgBox(WizardDirValue + ' already contains files. Choose an empty or new folder.', mbError, MB_OK, IDOK);
Result := False;
Break;
end;
until not FindNext(Rec);
finally
FindClose(Rec);
end;
end;

Your users get a familiar wizard. It installs into %LOCALAPPDATA%\Programs or C:\Program Files, depending on their choice. Running a newer installer upgrades in place: Setup closes the app if it is running, then replaces its files. Uninstalling from Settings → Apps removes the folder and the shortcuts, but leaves the app's data in %APPDATA% and %LOCALAPPDATA%, as Windows apps conventionally do.

For unattended installs, for example by an IT department, Setup takes command-line parameters:

.\my_app-1.0.0-setup.exe /VERYSILENT /SUPPRESSMSGBOXES /CURRENTUSER

Use /ALLUSERS, from an administrator terminal, to install for all users.

Code signing​

flet build doesn't sign Windows apps. Microsoft Defender SmartScreen can stop users from running an unsigned installer or app they downloaded: it shows Windows protected your PC, names an Unknown publisher, and runs the file only after More info → Run anyway. A self-signed certificate changes nothing here. A certificate from a trusted certificate authority replaces Unknown publisher with your name, but the warning keeps showing until the file or your certificate has built up reputation through downloads. EV certificates no longer skip this. On Windows 11 PCs where Smart App Control is on, unsigned apps can be blocked outright. See Microsoft's SmartScreen reputation guide. Apps from the Microsoft Store are signed by Microsoft instead.

Certificate authorities deliver code-signing certificates on a hardware token or through a cloud signing service, not as a file. With your certificate in the Windows certificate store, sign the app with signtool after each flet build windows, which replaces build\windows, and before you compile the installer or create the zip. In PowerShell, from your project directory:

$signtool = (Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" | Select-Object -Last 1).FullName
if (-not $signtool) { throw "signtool.exe not found: install the Windows SDK" }
$thumbprint = "<your certificate's thumbprint>"
$unsigned = @(Get-ChildItem build\windows -Recurse -Include *.exe, *.dll, *.pyd |
Where-Object { (Get-AuthenticodeSignature $_.FullName).Status -eq "NotSigned" })
for ($i = 0; $i -lt $unsigned.Count; $i += 50) {
& $signtool sign /fd sha256 /sha1 $thumbprint /tr http://timestamp.digicert.com /td sha256 $unsigned[$i..($i + 49)].FullName
if ($LASTEXITCODE -ne 0) { throw "signtool failed with exit code $LASTEXITCODE" }
}

To have Inno Setup sign setup.exe and its uninstaller too, add SignTool=mysign to the script's [Setup] section, and define mysign when you compile, in the same PowerShell session:

& "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" "/Smysign=`$q$signtool`$q sign /fd sha256 /sha1 $thumbprint /tr http://timestamp.digicert.com /td sha256 `$f" installer.iss

Inno Setup replaces $q with a quote and $f with the file to sign; the backticks keep PowerShell from expanding them. To rehearse all of this before you have a certificate, create a self-signed one with New-SelfSignedCertificate -Type CodeSigningCert -Subject "CN=My App Test" -CertStoreLocation Cert:\CurrentUser\My.

In CI, where you can't plug in a token, Artifact Signing, Microsoft's cloud signing service, works with the same signtool command: replace /sha1 $thumbprint with the /dlib and /dmdf options from its signtool guide.

Building the installer in CI​

GitHub-hosted Windows runners come with Inno Setup 6 installed and iscc on PATH. In a workflow like the one under GitHub Actions, add these steps after the step that builds the app:

- name: Build installer
if: runner.os == 'Windows'
run: iscc installer.iss

- name: Upload installer
if: runner.os == 'Windows'
uses: actions/upload-artifact@v7
with:
name: windows-installer
path: build/installer/*.exe
if-no-files-found: error

Microsoft Store​

The Microsoft Store installs desktop apps packaged as MSIX: your build\windows folder plus a manifest that describes the app to Windows. Microsoft signs the package once it passes certification, so you need no code-signing certificate, and the Store keeps installed copies up to date. The Store also accepts EXE and MSI installers, such as one made with Inno Setup, but only when they are signed with a certificate from a trusted certificate authority, install silently, and are hosted by you at a fixed URL per version (see MSI/EXE requirements).

flet build doesn't create MSIX packages yet. You create one from build\windows with the manifest and the packing script below, which runs MakeAppx from the Windows SDK that the Desktop development with C++ workload from Prerequisites installs.

Store prerequisites​

One-time setup, requiring a Microsoft Store developer account.

Creating a developer account​

Registration is free. Start it at storedeveloper.microsoft.com: other ways into Partner Center, Microsoft's publishing portal, lead to an older sign-up flow. Choose the account type carefully, because an individual account can't be converted to a company account later:

  • Individual: for publishing under your own name. You sign in with a personal Microsoft account and verify your identity with a government-issued photo ID and a selfie.
  • Company: required for an organization, when you publish as part of your trade or profession, and when your app's or publisher's name would read as a business's (Store policy 10.14). You sign up with a work email address on your organization's domain, and verify the company with a D-U-N-S number, usually within minutes, or with business documents, which take a few business days.

See Open a developer account for the details.

Reserving the app's name​

In Partner Center, go to Apps and games → + New product → MSIX or PWA app, enter your app's name, then Check availability → Reserve product name. The name is reserved for three months; submit the app within that time to keep it.

Then open Product management → Product identity. Three of its values go into the manifest exactly as shown, matching case, spaces and punctuation:

  • Package/Identity/Name
  • Package/Identity/Publisher, a CN=... value
  • Package/Properties/PublisherDisplayName

Creating the package​

Two files describe and build the package: a manifest and a packing script. Save both in an msix folder in your project's root directory, next to pyproject.toml. In the standard project structure, where your app's code lives in src, flet build leaves that folder out of the app. If your code sits in the root directory instead, add "msix" to your excluded files.

Save the manifest below as msix\AppxManifest.xml, then edit the values marked in the annotations. In XML, write a & in any of them as &amp;.

msix\AppxManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="YourPublisher.MyApp"
Publisher="CN=00000000-0000-0000-0000-000000000000"
Version="1.0.0.0"
ProcessorArchitecture="x64" />
<Properties>
<DisplayName>My App</DisplayName>
<PublisherDisplayName>Your Name</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Resources>
<Resource Language="en-us" />
</Resources>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
<Applications>
<Application Id="App" Executable="my_app.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="My App"
Description="My App"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png" />
</Application>
</Applications>
</Package>

If your app records audio or uses the device's location, for example with flet-audio-recorder or flet-geolocator, also declare <DeviceCapability Name="microphone" /> or <DeviceCapability Name="location" /> after the runFullTrust line, so that Windows lists your app on the matching Settings → Privacy & security page from the start. See App capability declarations.

Save the script below as msix\pack.ps1:

msix\pack.ps1
$ErrorActionPreference = "Stop"
Set-Location (Split-Path $PSScriptRoot)

$manifest = New-Object xml
$manifest.Load("$PWD\msix\AppxManifest.xml")
$exe = Get-Item "build\windows\$($manifest.Package.Applications.Application.Executable)"
$version = ($exe.VersionInfo.ProductVersion -split "\+")[0] + ".0"
if ($version -notmatch "^[1-9]\d*\.\d+\.\d+\.0$") { throw "Version $version isn't valid for the Store: use a build version such as 1.0.0" }
$manifest.Package.Identity.Version = $version

$makeappx = (Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\makeappx.exe" | Select-Object -Last 1).FullName
if (-not $makeappx) { throw "makeappx.exe not found: install the Windows SDK" }

if (Test-Path build\msix) { Remove-Item build\msix -Recurse -Force }
Copy-Item build\windows build\msix -Recurse
$manifest.Save("$PWD\build\msix\AppxManifest.xml")

Add-Type -AssemblyName System.Drawing
$ico = [IO.File]::ReadAllBytes("$PWD\build\flutter\windows\runner\resources\app_icon.ico")
$entry = 0..($ico[4] - 1) | ForEach-Object { 6 + 16 * $_ } | Sort-Object { if ($ico[$_]) { $ico[$_] } else { 256 } } | Select-Object -Last 1
$icon = [System.Drawing.Image]::FromStream([IO.MemoryStream]::new($ico, [BitConverter]::ToInt32($ico, $entry + 12), [BitConverter]::ToInt32($ico, $entry + 8)))
New-Item build\msix\Images -ItemType Directory | Out-Null
foreach ($image in @{ Square44x44Logo = 44; Square150x150Logo = 150; StoreLogo = 50 }.GetEnumerator()) {
$bitmap = New-Object System.Drawing.Bitmap $image.Value, $image.Value
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.InterpolationMode = "HighQualityBicubic"
$graphics.DrawImage($icon, 0, 0, $image.Value, $image.Value)
$bitmap.Save("$PWD\build\msix\Images\$($image.Key).png", [System.Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose(); $bitmap.Dispose()
}
$icon.Dispose()

& $makeappx pack /d build\msix /p "build\$($exe.BaseName)-$version.msix" /o
if ($LASTEXITCODE -ne 0) { throw "makeappx failed with exit code $LASTEXITCODE" }

After each flet build windows, run the script from your project's root directory:

powershell -ExecutionPolicy Bypass -File msix\pack.ps1

-ExecutionPolicy Bypass lets Windows PowerShell run the script without changing your PC's execution policy, which blocks scripts by default.

Testing the package locally​

Windows installs only signed .msix files, so double-clicking yours fails. Instead, with Developer Mode on, which flet build windows normally needs too, register the contents of build\msix as a packaged app, from Windows PowerShell:

Add-AppxPackage -Register build\msix\AppxManifest.xml

Start the app from the Start menu, where it appears under its display name. It runs from build\msix with the package's identity, so its data goes where a Store install's does (see below), but unlike a Store install's folder, build\msix stays writable. Close the app before you pack again. To test a changed manifest, and before installing the app from the Store, unregister it first, using your Package/Identity/Name:

Get-AppxPackage YourPublisher.MyApp | Remove-AppxPackage

To catch technical problems before certification does, run Microsoft's technical tests yourself with the Windows App Certification Kit (WACK). It isn't part of the C++ workload: install it with the Windows SDK installer by selecting Windows App Certification Kit. Then, from an administrator PowerShell in your project's root directory:

$appcert = "${env:ProgramFiles(x86)}\Windows Kits\10\App Certification Kit\appcert.exe"
$package = Get-ChildItem build\*.msix | Sort-Object LastWriteTime | Select-Object -Last 1
Remove-Item build\wack-report.xml -ErrorAction SilentlyContinue
& $appcert reset
& $appcert test -appxpackagepath $package.FullName -reportoutputpath "$PWD\build\wack-report.xml"

The report's OVERALL_RESULT should be PASS. A Flet app also fails three optional tests, which don't change that result: Debug configuration (the Python runtime ships debug builds of some modules), Archive files usage (compressed files, such as Flutter's license notices) and Blocked executables (references to cmd or powershell inside Python and Flutter). Certification also reviews your app and its listing by hand, which WACK can't.

How the Store app behaves​

Installed from the Store, the app differs from the build\windows folder in a few ways:

  • Its folder is read-only. Windows installs it under C:\Program Files\WindowsApps, in a new folder for each version. Flet writes nothing there; make sure your code doesn't either (see the rules for every format).
  • Its data is private to the package. Windows redirects the files and folders the app creates under %APPDATA% and %LOCALAPPDATA%, such as its storage and console log, to %LOCALAPPDATA%\Packages\<package family name>\LocalCache. The app reads and writes the usual paths, but other programs, including File Explorer, find these files only in that folder: for example the console log in LocalCache\Local\<company>\<product>\console.log. Uninstalling the app deletes them. Files that already existed in the real folders, for example from running build\windows on the same PC, are used in place instead, and stay after uninstalling. Get-AppxPackage YourPublisher.MyApp shows the package family name.
  • Its name and icons come from the manifest, not from the .exe.

Submitting the app​

Upload the .msix in a submission, in Partner Center: on your app's overview page, select Start submission, then complete each section. The sections most specific to a Flet app are:

  • Pricing and availability: to try the whole process before your app is public, set Visibility → Audience to Private audience, with a group of testers' personal Microsoft accounts; work and school accounts can't install the app. Testers install it from the private link shown there. The audience defaults to public, and once you have submitted with the public audience, the app can't be made private again. See Visibility options.

  • Properties: a privacy policy is required for desktop apps (Store policy 10.5.1), even when your app collects nothing. Enter its URL, or paste its text.

  • Packages: upload the .msix. Partner Center validates it right away, and rejects a package whose identity or name doesn't match your reservation.

  • Store listings: a description, at least one screenshot (PNG, 1366 × 768 pixels or larger) and, recommended, a 300 × 300 pixel PNG of your icon as the 1:1 app tile icon.

  • Submission options: under Restricted capabilities, explain why the app needs runFullTrust, for example:

    This is a desktop app built with Flet: a Flutter user interface with an
    embedded Python runtime, packaged as a full-trust Win32 app. Its executable
    is a regular Win32 process that can't run in an AppContainer, so it needs
    runFullTrust to start. The app doesn't request elevation, install services
    or drivers, or change system settings.

    Under Notes for certification, tell the testers how to use your app, including test credentials if it needs an account.

Select Submit for certification. Certification takes up to three business days; if your app fails, its report says why. Microsoft's list of common certification failures is worth reading before you submit, and the Store policies are what your app is checked against.

Updating the app​

To release an update, raise your build version, run flet build windows and the packing script, and upload the new .msix in a new submission. The Store then delivers it to your users. Once your first submission is published, the Microsoft Store Developer CLI can upload new versions from CI; it signs in with a Microsoft Entra ID application linked to your Partner Center account.

Troubleshooting​

SymptomCause and fix
Building with plugins requires symlink supportWindows Developer Mode is off — run start ms-settings:developers, enable it (see this guide), and rebuild.
Unable to find suitable Visual Studio toolchainThe Desktop development with C++ workload is missing — install it with the Visual Studio Installer (see Prerequisites).
The app window never appears, stays blank, or renders slowlyImpeller, the default renderer, misbehaves with some GPUs, drivers and virtual machines — set FLET_NO_IMPELLER=1 or build with --no-impeller to render with Skia. See Renderer.
Setup stops with DeleteFile failed; code 5. Access is denied. while upgradingThe app is still running and Setup couldn't close it — close the app and click Try again. Setup normally closes it for you, but not when installing for the current user on Windows on ARM, where Windows' x64 emulation service holds the app's files too.
Windows protected your PC when users run your installerThe installer is unsigned, or signed with a certificate that hasn't earned SmartScreen reputation yet — see Code signing.
Value of [Setup] section directive "SignTool" is invalidThe script names a sign tool that the compile command doesn't define — pass /S<name>=... to ISCC.exe, or remove the SignTool line for unsigned builds. See Code signing.
The packing script stops with Access to the path '<file>' is denied.The app you registered for testing is still running from build\msix — close it and pack again.
Add-AppxPackage -Register fails with 0x80073CFB and reinstallation of the package was blockedThe registered package has the same version but a different manifest — unregister it with Get-AppxPackage <Package/Identity/Name> | Remove-AppxPackage, then register it again.
Partner Center rejects the package: The name found in the package is not one of your reserved app namesThe manifest's <DisplayName> under <Properties> isn't a name you reserved — set it, and the DisplayName of uap:VisualElements, to your reserved name, pack again and upload the new .msix. See Creating the package.