Skip to main content

Packaging app for Linux

Instructions for packaging a Flet app into a Linux executable.

Note

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

Alternative: flet pack

For a PyInstaller-based way to package desktop apps — without the build-toolchain prerequisites below — see flet pack.

Prerequisites

Flet uses Flutter to build Linux apps. Compiling the app and its native plugins links against GTK and a number of system libraries, so these must be installed before running flet build linux.

On Debian/Ubuntu-based distributions, install the required packages with apt:

sudo apt update
sudo apt install -y \
binutils clang cmake gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 \
gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly \
gstreamer1.0-pulseaudio gstreamer1.0-qt5 gstreamer1.0-tools \
gstreamer1.0-x libasound2-dev libgstreamer-plugins-bad1.0-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev libgtk-3-dev \
libmpv-dev libsecret-1-0 libsecret-1-dev libunwind-dev lld llvm mpv \
ninja-build pkg-config

This is the same set of packages Flet uses in its own build environment. A few notes on what they are for:

  • Build toolchainclang, cmake, ninja-build, pkg-config, llvm, lld, binutils and libgtk-3-dev are required to compile and link the app. In particular, the lld linker must be present — without it the build fails with a linker error.
  • Secret storagelibsecret-1-0 and libsecret-1-dev are used for secure storage / keyring access.
  • Audio and video — the gstreamer1.0-*, libgstreamer*-dev, libasound2-dev, libmpv-dev and mpv packages are required by the Audio service and Video control. You can omit them if your app does not play media, but installing the full set above avoids surprises. See those pages for control-specific details.
Other distributions

Package names differ on non-Debian distributions (e.g. Fedora, Arch). Install the equivalent GTK 3, GStreamer, mpv/libmpv, libsecret, clang/llvm, lld, cmake and ninja development packages for your distribution.

flet build linux

Note

This command can be run on Linux only (or WSL).

Builds a Linux executable.

App icon

The app icon is taken from icon_linux.png (falling back to icon.png, or the default Flet icon) in the assets directory of your Flet app — see Icons. flet build linux copies it into the bundle as data/app_icon.png, and the app sets it as its window icon on startup.

How the icon shows up depends on the display server (see Window positioning on Wayland for checking the session type):

  • X11 (and XWayland): taskbars and window switchers read the window icon directly — it works out of the box, no installation needed.
  • Wayland (the default session on modern GNOME/Ubuntu): the protocol has no window-icon concept. The desktop environment resolves the app's name and icon from an installed .desktop entry matching the app id, and shows a generic icon until one is installed.

For Wayland (and for listing the app in the application launcher on any session), the bundle ships a ready-to-install desktop entry and icon under share/:

share/applications/<bundle_id>.desktop
share/icons/hicolor/<size>/apps/<bundle_id>.png

<size> matches the icon's own pixel size when the icon theme declares it (16x16, 22x22, 24x24, 32x32, 36x36, 48x48, 64x64, 72x72, 96x96, 128x128, 192x192, 256x256, 512x512), and is 256x256 for any other size — desktop environments scale from it either way, though some packaging linters expect the file to match its directory.

To register the app for the current user, copy them into ~/.local/share and point Exec= at the absolute path of the executable:

cp -r share/. ~/.local/share/
sed -i "s|^Exec=.*|Exec=\"$PWD/<executable>\" %U|" ~/.local/share/applications/<bundle_id>.desktop
update-desktop-database ~/.local/share/applications

(run from the bundle directory, replacing <executable> and <bundle_id>; a system-wide install to /usr/share works the same way). Keep the quotes around Exec= — without them a path containing spaces is split into separate arguments and the launcher fails. Linux packaging tools (.deb/.rpm/AppImage builders) can pick up the same two files.

The desktop entry's name comes from --product and its comment from --description (or the corresponding pyproject.toml settings); the app id is the bundle ID<org_name>.<project_name> by default. Until the entry is installed the desktop environment has no name for the app and falls back to that id, so the dock tooltip reads com.example.my_app rather than your product name; installing the entry fixes the name and the Wayland icon together, since both are resolved from it.

Its application categories decide where the app is filed in application menus.

Application categories

The Categories key of the generated desktop entry determines which menu sections the app appears under. Values come from the freedesktop category registry, which splits them in two:

  • Main categories — the fourteen every desktop environment must support: AudioVideo, Audio, Video, Development, Education, HealthFitness, Game, Graphics, Network, Office, Science, Settings, System and Utility. Normally give exactly one: the spec allows several, but then "the entry may appear more than once in the menu".
  • Additional categories — finer-grained values such as TextEditor or ArcadeGame, listed alongside a main category to refine placement.

A value outside the registry is not an error, but no menu rule matches it, so it has no effect on where the app appears.

Its value is determined in the following order of precedence:

  1. --linux-categories
  2. [tool.flet.linux].categories
  3. Utility
flet build linux --linux-categories Game ArcadeGame

Distributing

flet build linux leaves a relocatable bundle directory — an executable alongside data/, lib/, python3.x/, site-packages/ and app/ — not something an end user can download and double-click. To ship it, wrap it in one of the formats below.

Packaging is mostly relocation, because the bundle already contains the two files every Linux format wants:

share/applications/<bundle_id>.desktop
share/icons/hicolor/<size>/apps/<bundle_id>.png

Two rules apply to every format:

  • Keep the bundle together. The executable finds its libraries through RPATH $ORIGIN/lib and its Python runtime through its own path, so data/, lib/, python3.x/, site-packages/ and app/ must stay siblings of the executable. Do not scatter them into /usr/bin and /usr/lib.
  • Rewrite Exec=. The shipped entry names the executable without a path, since the bundle does not know where it will be installed. Every recipe below replaces that line with the real location.

A single executable file that runs without installation — the closest equivalent to a macOS .dmg.

The script below fetches appimagetool for you, so there is nothing to download by hand and no path to keep in step. It picks the build matching the machine you are on — uname -m reports x86_64 or aarch64 — and only downloads when the file is not already there, so it costs nothing on a rebuild and never touches a copy you placed yourself.

appimagetool is itself an AppImage, so running it needs FUSE 2. Distributions that have moved to FUSE 3 may not have it, so check before assuming either way:

ldconfig -p | grep libfuse.so.2

If that prints nothing, either install it (sudo apt install libfuse2, or libfuse2t64 on Ubuntu 24.04 and later) or skip FUSE entirely, by setting the variable when you run the script below:

APPIMAGE_EXTRACT_AND_RUN=1 bash build-appimage.sh

The same applies to the AppImage you produce: your users need FUSE 2, or must run it with the same variable set.

Save the below script as build-appimage.sh (pasting it straight into a terminal is fragile, and you will re-run it each time you rebuild), then edit the three variables at the top (BUNDLE, APP & ID), and run it with bash build-appimage.sh.

#!/usr/bin/env bash
set -euo pipefail

BUNDLE=build/linux
APP=my_app
ID=com.example.my_app

APPDIR=MyApp.AppDir
ARCH=$(uname -m)
APPIMAGETOOL=$PWD/appimagetool-$ARCH.AppImage

if [ ! -e "$APPIMAGETOOL" ]; then
wget -O "$APPIMAGETOOL.part" \
"https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-$ARCH.AppImage"
mv "$APPIMAGETOOL.part" "$APPIMAGETOOL"
fi
chmod +x "$APPIMAGETOOL"

test -d "$BUNDLE/share/applications" || { echo "no desktop entry in $BUNDLE"; exit 1; }

ICON_SRC=$(find "$BUNDLE/share/icons/hicolor" -type f -name "$ID.png" | head -n1)
ICON_SIZE=$(basename "$(dirname "$(dirname "$ICON_SRC")")")

rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin"
mkdir -p "$APPDIR/usr/share/applications"
mkdir -p "$APPDIR/usr/share/icons/hicolor/$ICON_SIZE/apps"

cp -a "$BUNDLE"/. "$APPDIR/usr/bin/"
rm -rf "$APPDIR/usr/bin/share"

cp "$BUNDLE/share/applications/$ID.desktop" "$APPDIR/usr/share/applications/"
cp "$ICON_SRC" "$APPDIR/usr/share/icons/hicolor/$ICON_SIZE/apps/"

sed -i "s|^Exec=.*|Exec=$APP|" "$APPDIR/usr/share/applications/$ID.desktop"

ln -s "usr/share/applications/$ID.desktop" "$APPDIR/$ID.desktop"
ln -s "usr/share/icons/hicolor/$ICON_SIZE/apps/$ID.png" "$APPDIR/$ID.png"
ln -s "usr/share/icons/hicolor/$ICON_SIZE/apps/$ID.png" "$APPDIR/.DirIcon"

printf '#!/bin/sh\nHERE=$(dirname "$(readlink -f "$0")")\nexec "$HERE/usr/bin/%s" "$@"\n' "$APP" > "$APPDIR/AppRun"
chmod +x "$APPDIR/AppRun"

VERSION=1.0.0 "$APPIMAGETOOL" --no-appstream "$APPDIR"
Do not set LD_LIBRARY_PATH in AppRun

Many AppRun examples export it. LD_LIBRARY_PATH takes precedence over the binary's RUNPATH, so setting it lets system libraries shadow the bundled ones. The bundle needs no environment at all — $ORIGIN resolves against the executable's own path, so AppRun only has to exec it.

appimagetool requires a Categories= key in the desktop entry and runs desktop-file-validate over it, failing on any error. Both are satisfied by the generated entry.

An AppImage registers nothing with the desktop

The desktop entry travels inside the image, where nothing scans it. So the app grid has no entry for the app, and hovering its icon shows the app id rather than the name — the shell has no Name= to read and falls back to what the window calls itself.

The window and dock icons still work on X11, because those come from the window's own _NET_WM_ICON rather than from an entry. On Wayland, where the icon is resolved through the entry too, an unregistered AppImage gets neither.

Users can register it themselves with a tool like AppImageLauncher. If you would rather not ask them to, ship a .deb or .rpm as well — those install the entry as part of the package.

Window positioning on Wayland

On Linux the display server controls window placement, and this differs between X11 and Wayland:

  • X11 lets applications set their own top-level window position.
  • Wayland (the default session on modern GNOME/Ubuntu) does not — by design, a client cannot position its own top-level window; the compositor (e.g. Mutter) decides where windows are placed.

As a result, on a Wayland session the following have no effect (window sizing still works — only positioning is restricted):

This is a Wayland protocol limitation, not a Flet bug. The same code works as expected on Windows, macOS, Linux X11 sessions, and Wayland sessions running the app through XWayland.

To force the X11 backend (XWayland) on a Wayland session and re-enable programmatic positioning, run the app with the GDK_BACKEND environment variable:

GDK_BACKEND=x11 ./your_app

You can check the current session type with:

echo $XDG_SESSION_TYPE # "wayland" or "x11"

Troubleshooting

SymptomCause and fix
Build fails with a linker errorThe lld linker is missing — it is part of the prerequisites: sudo apt install lld (or your distribution's equivalent) and rebuild.
CMake can't find gtk+-3.0 or other packagesOne or more -dev prerequisites are missing — install the full list (package names differ on non-Debian distributions).
The built app won't start on users' machines: error while loading shared libraries: libmpv… (or GStreamer errors)The Audio service and Video control link against system libraries — mpv/libmpv and GStreamer must also be installed on the machine running the app, not only the build machine.
Window positioning or centering has no effectThe app is running in a Wayland session — see Window positioning on Wayland.
The taskbar/dock shows a generic icon on WaylandWayland resolves icons from an installed desktop entry, not from the window — install the bundle's share/ files as described in App icon.
The dock tooltip or app switcher shows the bundle ID instead of the app nameThe desktop entry is not installed, so the desktop environment has no name for the app and falls back to the app id — install the bundle's share/ files as described in App icon.
rpmbuild fails at %mkbuilddir, reporting Bad file descriptorAn earlier build left ~/rpmbuild/BUILD/<name>-<version>-build behind and rpm cannot clear it before rebuilding. The errno is unrelated to the real cause — delete that directory and build again.