How AppImages are built
This page explains the mechanics behind AppImage packaging for Python applications — both the
manual process and what appimage.build automates. Understanding the manual steps makes it
clear what the module does and why each piece exists.
What an AppImage is
An AppImage is a single executable file that bundles an application together with all its
dependencies into a SquashFS filesystem image. A small ELF runtime is prepended to the
image; when the file is executed, that runtime uses FUSE to mount the SquashFS and then
runs the AppRun entry point inside it. The application sees a consistent, self-contained
directory tree at $APPDIR, regardless of what is installed on the host system.
The AppDir layout
Before appimagetool packs everything into a .AppImage file, it expects a directory with
this structure:
AppDir/
├── AppRun ← executable entry point (bash script)
├── myapp.desktop ← Freedesktop metadata
├── myapp.png ← application icon
└── python/ ← complete Python distribution
├── bin/
│ └── python3
└── lib/
└── python3.x/
└── site-packages/
├── appimage/ ← the runtime module
└── myapp/ ← your application
Three files at the root are mandatory for AppImage: AppRun, a .desktop file, and an icon
matching the desktop file’s Icon= value. Everything else is up to you — for a Python
application that means shipping a complete Python distribution under python/.
python-build-standalone and uv
The Python distribution bundled in the AppImage comes from python-build-standalone, a project that produces pre-built, highly portable Python interpreters for Linux, macOS, and Windows.
Astral — the team behind uv — now maintains python-build-standalone
and uses it as the direct source for uv python install. This means the Python you get when
you run uv python install 3.11 and the Python bundled in your AppImage are built from
exactly the same release artifacts. Same compiler flags, same standard library, same
binary layout.
The practical consequence: if you develop and test with uv, there are no interpreter
surprises at packaging time. The AppImage ships the interpreter your code already ran against.
python-build-standalone provides several artifact variants. The one used for AppImages is
install_only_stripped — it contains the interpreter, standard library, and headers, but
omits test suites and debug symbols, keeping the download small. The archive extracts to a
python/ directory ready to be placed directly inside AppDir.
Releases and download URLs
Releases are published at
github.com/astral-sh/python-build-standalone/releases.
Each release is tagged with a date (e.g. 20260211). The download URL follows a fixed
pattern:
https://github.com/astral-sh/python-build-standalone/releases/download/{date}/
cpython-{python_version}+{date}-{arch}-unknown-linux-gnu-install_only_stripped.tar.gz
The {arch} token depends on the host machine:
|
URL arch token |
|---|---|
|
|
|
|
|
|
Note
uv uses the same python-build-standalone distributions when you run
uv python install. If uv is already available on the build machine, it can
download and cache the interpreter for you — but the tarball it uses internally is
the same install_only_stripped artifact described here.
Building manually
The following steps reproduce what appimage.build does, without using the module.
Each step can be run and verified independently. Requirements: curl, tar, a Linux host.
Set these variables in your shell before running the steps below. Adjust APP,
PYTHON_MINOR, and the entry point in the AppRun script to match your project.
APP="myapp"
PYTHON_MINOR="3.11"
ARCH="$(uname -m)"
Step 1 — Create the AppDir skeleton
The AppDir is the staging directory that appimagetool will later pack into a single file.
mkdir -p build/AppDir
Step 2 — Download Python
The python-build-standalone URL uses a different arch token than uname -m on armv7.
Apply the mapping first:
case "$ARCH" in
armv7l) PBS_ARCH="armv7" ;;
*) PBS_ARCH="$ARCH" ;;
esac
Then resolve the download URL and fetch the tarball:
RELEASE_DATE=$(curl -s \
"https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest" \
| grep '"tag_name"' | cut -d'"' -f4)
PBS_URL=$(curl -s \
"https://api.github.com/repos/astral-sh/python-build-standalone/releases/tags/${RELEASE_DATE}" \
| grep '"browser_download_url"' \
| grep "cpython-${PYTHON_MINOR}\." \
| grep "${PBS_ARCH}-unknown-linux-gnu-install_only_stripped" \
| grep -v "freethreaded" \
| cut -d'"' -f4)
curl -L "$PBS_URL" -o build/python.tar.gz
For reproducible builds, replace the first curl with a hardcoded date:
RELEASE_DATE="20260211"
Step 3 — Extract Python into AppDir
The tarball extracts to a python/ directory. Placing it directly inside AppDir gives
the final path AppDir/python/bin/python3.
tar -xf build/python.tar.gz -C build/AppDir
After this step, verify the interpreter works:
build/AppDir/python/bin/python3 --version
Step 4 — Install the application
Use the bundled interpreter’s pip to install the application and the appimage runtime
module into the bundled site-packages. This keeps everything self-contained inside AppDir.
build/AppDir/python/bin/python3 -m pip install appimage myapp
The appimage package is the runtime component that handles entry point dispatch,
--python-interpreter, and virtual environment support at launch time.
Step 5 — Write the AppRun script
AppRun is the executable entry point that the Linux kernel calls when the AppImage is
run. It must be at the root of AppDir and must be executable.
Create a file named AppRun with the following content:
#!/bin/bash
set -e
if [ -n "$APPIMAGE" ]; then
appimage_path=$(dirname "$APPIMAGE")
if [ -d "$appimage_path/squashfs-root" ]; then
export APPDIR="$appimage_path/squashfs-root"
fi
fi
if [ -z "$APPDIR" ]; then
export APPDIR=$(dirname $(readlink -f "$0"))
fi
exec "$APPDIR/python/bin/python3" -P -m appimage --python-main myapp "$@"
Replace myapp in --python-main with the console script name your package defines in
[project.scripts].
Then copy it into place and mark it executable:
cp AppRun build/AppDir/AppRun
chmod +x build/AppDir/AppRun
The script sets APPDIR to the root of the mounted filesystem, then hands off to the
appimage runtime module. The squashfs-root block at the top handles the case where
the AppImage was extracted manually (e.g. on systems without FUSE).
The -P flag passed to Python activates isolated mode:
PYTHONPATHfrom the host environment is ignoredThe user site-packages directory (
~/.local/lib/python3.x/site-packages) is not added tosys.pathPYTHONSTARTUPis not executed
Without -P, a package installed in the user’s home directory could shadow a package
bundled in the AppImage. The flag ensures the AppImage only ever loads what is inside
AppDir/python/.
Step 6 — Write the .desktop file
AppImage requires a Freedesktop .desktop file at the root of AppDir. The filename
must match the Icon= value and the AppImage filename prefix.
Create a file named myapp.desktop with the following content:
[Desktop Entry]
Type=Application
Name=myapp
Icon=myapp
Categories=Utility;
Terminal=true
Then copy it into place:
cp myapp.desktop build/AppDir/myapp.desktop
Step 7 — Add an icon
AppImage requires an icon file whose basename matches the Icon= value in the .desktop
file. PNG and SVG are both accepted.
cp myapp.png build/AppDir/${APP}.png
If no icon is available, a placeholder can be created with ImageMagick:
convert -size 256x256 xc:gray build/AppDir/${APP}.png
Step 8 — Download appimagetool
appimagetool is the official tool for packing an AppDir into a SquashFS-based AppImage.
It is distributed as a self-contained AppImage itself.
curl -L \
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage" \
-o build/appimagetool
chmod +x build/appimagetool
Step 9 — Pack the AppImage
mkdir -p dist
build/appimagetool build/AppDir dist/${APP}-${ARCH}.AppImage
appimagetool compresses the AppDir into a SquashFS image, appends the AppImage runtime
(a small ELF binary that mounts and executes it), and writes the result as a single
executable file.
What appimage.build automates
python -m appimage.build performs the same steps as described above, with these
additions:
Configuration resolution — app name, entry point, and Python version are read from
[project] in pyproject.toml. Running --check shows exactly what was detected and from
where before anything is built.
GitHub API lookup — the module queries the python-build-standalone releases API to find
the correct install_only_stripped asset for the current architecture and the requested
Python minor version. Passing python_date in [tool.appimage.build] pins the exact
release tag, making the build reproducible byte-for-byte.
Caching — the Python tarball and appimagetool binary are cached in build/ and
reused on subsequent builds.
AppRun generation — the AppRun script is generated from a template that also handles
the squashfs-root fallback for environments without FUSE (containers, some CI systems),
and injects any extra environment variables defined under [tool.appimage.build.env].
Desktop file generation — the .desktop file is generated from pyproject.toml
metadata (name, description). A custom file can be provided via the desktop key in
[tool.appimage.build].
Lifecycle hooks — shell scripts can run after pip install (post_install) or after
all files are in place but before appimagetool runs (pre_package). The APPDIR
environment variable is set when the hook executes, so hooks can modify the AppDir directly.
Extra files — arbitrary files or directories are copied into the AppDir via
[tool.appimage.build.extra_files].
The runtime module
The appimage Python package (installed into AppDir/python/lib/.../site-packages/) is
the bridge between the static AppRun bash script and your application. It is invoked as
python3 -P -m appimage --python-main <entry_point> by every generated AppRun.
At startup it:
Reads
--python-mainto know the default entry pointChecks
VIRTUAL_ENVandARGV0to detect whether it was invoked through a virtual environment symlink — if so, it activates that environment by adjustingsys.path,sys.prefix, and the relevant environment variablesStrips all
--python-*arguments fromsys.argvbefore forwarding to the application, so your code never sees them
The --python-interpreter flag replaces the process with a fresh python3 -P invocation
(via os.execvp) passing any remaining arguments. This is how interactive use and
-m venv work: the module hands control back to the raw interpreter after the AppImage
environment is set up.
When creating a virtual environment via --python-interpreter -m venv, the module patches
the venv so that its python3 symlink points to the AppImage file itself rather than to
the interpreter binary inside AppDir. This makes the AppImage act as the Python
interpreter for the venv — all bundled packages are available, and pip install into the
venv adds packages on top without repackaging.