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 toolchain — clang, 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 storage — libsecret-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.svg or icon_linux.png (falling back to icon.svg, icon.png, or the default Flet icon) in the assets directory of your Flet app — see Icons. flet build linux renders 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 desktop (see Window positioning on Wayland for checking the session type):

  • X11 (and XWayland): most taskbars and window switchers read the window icon directly. GNOME Shell 46 and later (Ubuntu 24.04) doesn't, even on X11.
  • 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.

So that the dock shows the app's icon and name everywhere, the app installs a desktop entry for the current user while it runs, at applications/<bundle_id>.desktop in $XDG_DATA_HOME (~/.local/share by default), and the last running instance removes it when it closes. The entry is hidden from the application launcher (NoDisplay=true), so it doesn't add the app to the user's app grid, and as it doesn't outlive the app, pinning the app to the dock lasts only while it runs. The app leaves alone an entry with the same name that a package installed system-wide, or that the user installed themselves, and installs none when it runs in Flatpak or Snap, or from a path containing %.

To turn this off, set desktop_entry to false:

pyproject.toml
[tool.flet.linux]
desktop_entry = false

To list the app in the application launcher, or pin it for good, install the desktop entry and icon the bundle ships 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. Without an entry 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; an installed entry, the app's own or this one, fixes the name and the icon together, since both are resolved from it. Installing this entry replaces the one the app installs for itself, which then leaves it alone.

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

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 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 isn't listed in the app grid

The desktop entry travels inside the image, where nothing scans it. While the app runs, the hidden entry it installs for itself (see App icon) gives the dock its icon and name. But the app grid has no entry for the app.

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.
The window is blank or flickers, curves and text look jagged, or the app crashes when maximizedImpeller, 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.