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.