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.

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.

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

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.