Skip to main content

Packaging with flet pack

Instructions for packaging a Flet app into a standalone desktop executable with flet pack — a lightweight, PyInstaller-based alternative to flet build. Users can run the packaged app without installing a Python interpreter or any modules.

How it relates to flet build​

Both commands are supported — they occupy different points on the speed-vs-control curve:

flet packflet build
TargetsDesktop only: Windows, macOS, LinuxDesktop, mobile (Android/iOS), and web
ToolchainPyInstallerFlutter SDK (auto-installed)
How the app runsYour Python code alongside the prebuilt Flet desktop clientFlutter-compiled app with Python embedded, running in-process
Python dependenciesDiscovered by PyInstaller's static analysis of your importsInstalled in full from your declared app dependencies
Build timeFast — no native compilationSlower — a full Flutter build
CustomizationIcon and executable/bundle metadataEverything: icons, splash, build template, signing and store packaging

Reach for flet pack when you want a desktop artifact quickly; use flet build when you target mobile or web, or need deeper customization.

Like PyInstaller itself, flet pack is not a cross-compiler: run it on each OS you target — CI makes this painless.

Prerequisites​

PyInstaller powers the packaging and must be installed first:

pip install pyinstaller

Packaging​

From the directory containing your program, run:

flet pack your_program.py

The packaged app lands in the dist folder (--distpath changes that): a single-file executable on Windows and Linux, or a .app bundle on macOS. Pass --onedir for a one-folder bundle instead of a single file (macOS always produces a .app bundle). Try running it:

open dist/your_program.app

By default the executable or bundle is named after the Python script; change it with --name:

flet pack your_program.py --name bundle_name

If non-empty build or dist folders remain from a previous run, flet pack asks before deleting them — pass --yes to skip all prompts (useful in CI).

To distribute, zip the contents of the dist folder and hand it to your users — they don't need Python or Flet installed to run it.

Custom icon​

Set the icon with --icon:

flet pack your_program.py --icon your-icon.ico

Provide the icon in the target platform's native format: .ico on Windows and .icns on macOS. It is applied both to the outer executable and to the embedded Flet viewer, so the app window, Dock/taskbar entries, and the executable itself all match.

Linux works differently

Linux has nowhere to put an icon inside an executable — the desktop reads it from an installed desktop entry. Pass a .png and flet pack writes both the entry and the icon for you to install; see Linux taskbar identity.

Linux taskbar identity​

flet pack launches the shared, prebuilt Flet client, so without help every packed app reaches the taskbar under that binary's name — grouped together and labelled flet. To avoid that, the app is relaunched under its own identity (see FLET_APP_ID): --bundle-id when you pass one, and otherwise the executable's name.

--bundle-id also lets you choose that identity rather than inherit it from the executable's name, which is worth doing if the executable is named something a desktop entry should not be keyed on — a versioned my-app-1.2.3, say.

That gives the app its own taskbar group and label. Its display name and icon come from somewhere else: a desktop entry, which the desktop matches to the window through StartupWMClass. flet pack writes one next to the executable — along with the icon, if you passed a .png to --icon:

dist/
my-app the executable
com.example.my_app.desktop
com.example.my_app.png only when --icon is a .png

Name= comes from --product-name (falling back to the executable's name) and Comment= from --file-description. Nothing is installed for you, because installing would change your application menu as a side effect of building. Copy the two files into place yourself and refresh the caches:

cp dist/com.example.my_app.desktop ~/.local/share/applications/
update-desktop-database ~/.local/share/applications

# only if you passed --icon with a .png
mkdir -p ~/.local/share/icons/hicolor/256x256/apps
cp dist/com.example.my_app.png ~/.local/share/icons/hicolor/256x256/apps/
gtk-update-icon-cache -f -t ~/.local/share/icons/hicolor
Moving the executable breaks Exec=

The entry points at wherever flet pack built the binary, since that is the only location known when it is written. If you install the executable somewhere else — /opt, ~/.local/bin — edit Exec= to match:

sed -i 's|^Exec=.*|Exec="/opt/my-app/my-app"|' ~/.local/share/applications/com.example.my_app.desktop
Or let flet build package it

flet build linux compiles a runner per app, so the identity is built in rather than applied at launch. Reach for it if you would rather have packaging handled than assemble it yourself.

Including assets​

If your app uses assets, include them with --add-data, in the form source:destination:

flet pack your_program.py --add-data "assets:assets"

The option can be repeated to include multiple files or folders.

Executable and bundle metadata​

Details shown by the OS about your app can be customized.

On macOS — the "About" dialog and Dock/Activity Monitor entries of the bundle:

Flet app bundle about

On Windows — the executable's "Details" properties dialog:

Like the icon, the metadata is embedded into both the outer executable and the Flet viewer inside it.

More options​

  • --hidden-import — add modules that are imported dynamically and therefore missed by PyInstaller's static analysis.
  • --add-binary — bundle additional binary files.
  • --debug-console 1 — keep a console window with Python output open, for troubleshooting the packaged app.
  • --uac-admin — request elevated permissions on start (Windows).
  • --codesign-identity — sign the app bundle (macOS).
  • --pyinstaller-build-args — pass any other argument straight through to the underlying pyinstaller command.

The full option list is in the flet pack reference.

Packaging in CI​

Since each OS must package its own artifact, a CI matrix produces all three in one go:

name: Pack Flet App

on:
push:
workflow_dispatch:

jobs:
pack:
name: Pack on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.14"

- name: Install dependencies
run: pip install flet pyinstaller

- name: Pack app
run: flet pack your_program.py --yes

- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: your_program-${{ matrix.os }}
path: dist

Troubleshooting​

SymptomCause and fix
ModuleNotFoundError inside the packaged app onlyThe module is imported dynamically, so PyInstaller's static analysis missed it — repackage with --hidden-import <module>.
The packaged app exits or misbehaves with no visible errorRepackage with --debug-console 1 to get a console window showing Python output and tracebacks.
macOS: "App" is damaged and can't be opened on other MacsDownloaded apps must be signed and notarized for Gatekeeper to run them. Sign the bundle with --codesign-identity, then notarize and staple it — the Notarization section explains the concepts and commands, which apply to any signed app. flet build macos automates this entire chain.