Packaging app for Linux
Instructions for packaging a Flet app into a Linux executable.
This guide provides detailed Linux-specific information. Complementary and more general information is available here.
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:
- Package list
- From the CLI
- From the CLI (without jq)
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
flet --version reports the same list, so a setup script never goes stale:
sudo apt update
sudo apt install -y $(flet --version --json | jq -r '.linux_dependencies | join(" ")')
Same thing where jq is not installed:
sudo apt update
sudo apt install -y $(flet --version --json \
| python3 -c "import json,sys; print(' '.join(json.load(sys.stdin)['linux_dependencies']))")
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,binutilsandlibgtk-3-devare required to compile and link the app. In particular, thelldlinker must be present — without it the build fails with a linker error. - Secret storage —
libsecret-1-0andlibsecret-1-devare used for secure storage / keyring access. - Audio and video — the
gstreamer1.0-*,libgstreamer*-dev,libasound2-dev,libmpv-devandmpvpackages are required by theAudioservice andVideocontrol. 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.
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
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
.desktopentry 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,SystemandUtility. 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
TextEditororArcadeGame, 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:
--linux-categories[tool.flet.linux].categoriesUtility
- flet build
- pyproject.toml
flet build linux --linux-categories Game ArcadeGame
[tool.flet.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/liband its Python runtime through its own path, sodata/,lib/,python3.x/,site-packages/andapp/must stay siblings of the executable. Do not scatter them into/usr/binand/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.
- AppImage
- .deb
- .rpm
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"
LD_LIBRARY_PATH in AppRunMany 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.
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.
Save this as build-deb.sh, edit the five variables at the top (BUNDLE,
PKG, BIN, APPID and VER), and run it with bash build-deb.sh on a
machine that has
dpkg-deb — that means Linux, not WSL-less Windows or macOS.
#!/usr/bin/env bash
set -euo pipefail
BUNDLE=build/linux
PKG=my-app
BIN=my-app
APPID=com.example.my_app
VER=1.0.0
ARCH=$(dpkg --print-architecture)
STAGE="build/deb/${PKG}_${VER}_${ARCH}"
rm -rf "$STAGE"
install -d "$STAGE/DEBIAN" "$STAGE/opt/$PKG" "$STAGE/usr/bin" "$STAGE/usr/share"
cp -a "$BUNDLE"/. "$STAGE/opt/$PKG/"
cp -a "$STAGE/opt/$PKG/share/." "$STAGE/usr/share/"
rm -rf "$STAGE/opt/$PKG/share"
sed -i "s|^Exec=.*|Exec=/opt/$PKG/$BIN %U|" "$STAGE/usr/share/applications/$APPID.desktop"
ln -sfn "/opt/$PKG/$BIN" "$STAGE/usr/bin/$PKG"
cat > "$STAGE/DEBIAN/control" <<EOF
Package: $PKG
Version: $VER
Architecture: $ARCH
Maintainer: Your Name <you@example.com>
Section: utils
Priority: optional
Depends: libgtk-3-0 | libgtk-3-0t64, libglib2.0-0 | libglib2.0-0t64, libgdk-pixbuf-2.0-0, libstdc++6, libgcc-s1, libc6
Description: One-line summary of My App
A longer description, with every line indented by one space.
EOF
cat > "$STAGE/DEBIAN/postinst" <<'EOF'
#!/bin/sh
set -e
if [ "$1" = configure ]; then
update-desktop-database -q /usr/share/applications 2>/dev/null || true
gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor 2>/dev/null || true
fi
EOF
chmod 0755 "$STAGE/DEBIAN/postinst"
dpkg-deb --build --root-owner-group "$STAGE" "build/${PKG}_${VER}_${ARCH}.deb"
Check the result before publishing it:
dpkg -c build/my-app_1.0.0_arm64.deb
sudo apt install ./build/my-app_1.0.0_arm64.deb
my-app
Depends: lists the shared libraries the app loads at runtime. These are not
the -dev packages from Prerequisites, which are only needed
on the machine doing the building. The a | b alternatives cover Ubuntu 24.04's
rename of several libraries to …t64, so one package works on both. Add
libmpv2 and the GStreamer runtime packages if your app uses the
Audio service or the
Video control.
The same shape as the .deb: the bundle installs to /opt, its desktop entry
and icon move to /usr/share, and /usr/bin gets a symlink.
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
cp -a build/linux ~/rpmbuild/SOURCES/bundle
Save the following as ~/rpmbuild/SPECS/my-app.spec and build it with
rpmbuild -bb ~/rpmbuild/SPECS/my-app.spec:
%global __os_install_post %{nil}
%global debug_package %{nil}
AutoReqProv: no
Name: my-app
Version: 1.0.0
Release: 1%{?dist}
Summary: One-line summary of My App
License: Apache-2.0
BuildArch: x86_64
Requires: gtk3, glib2, gdk-pixbuf2
%description
A longer description of My App.
%install
mkdir -p %{buildroot}/opt/%{name} %{buildroot}/usr/bin %{buildroot}/usr/share
cp -a %{_sourcedir}/bundle/. %{buildroot}/opt/%{name}/
cp -a %{buildroot}/opt/%{name}/share/. %{buildroot}/usr/share/
rm -rf %{buildroot}/opt/%{name}/share
sed -i "s|^Exec=.*|Exec=/opt/%{name}/my-app %U|" %{buildroot}/usr/share/applications/com.example.my_app.desktop
ln -sfn ../../opt/%{name}/my-app %{buildroot}/usr/bin/%{name}
%files
/opt/%{name}
/usr/bin/%{name}
/usr/share/applications/*
/usr/share/icons/hicolor/*/apps/*
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):
Page.window.center()- setting
Page.window.left/Page.window.top - moving the window programmatically
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
| Symptom | Cause and fix |
|---|---|
| Build fails with a linker error | The 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 packages | One 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 effect | The app is running in a Wayland session — see Window positioning on Wayland. |
| The taskbar/dock shows a generic icon on Wayland | Wayland 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 name | The 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 descriptor | An 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. |