DEV Community

viraj-sh
viraj-sh

Posted on Edited on

Create App image of python desktop projects

1. Install required tools

sudo apt update
sudo apt install git patchelf desktop-file-utils appstream file nodejs npm libfuse2t64 curl
Enter fullscreen mode Exit fullscreen mode
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
Enter fullscreen mode Exit fullscreen mode
wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
Enter fullscreen mode Exit fullscreen mode

Make both appimages executable

chmod +x linuxdeploy-x86_64.AppImage appimagetool-x86_64.AppImage
Enter fullscreen mode Exit fullscreen mode

2. Wheel

uv build --wheel
Enter fullscreen mode Exit fullscreen mode
mkdir -p build/wheels
curl -L -o build/wheels/proxy_tools-0.1.0.tar.gz \
  https://pypi.org/packages/source/p/proxy-tools/proxy_tools-0.1.0.tar.gz
Enter fullscreen mode Exit fullscreen mode
ls -l build/wheels/proxy_tools-0.1.0.tar.gz
file build/wheels/proxy_tools-0.1.0.tar.gz
Enter fullscreen mode Exit fullscreen mode

proxy-tools has no wheel on PyPI; build one from sdist so --no-build later works

uv build --wheel --out-dir build/wheels build/wheels/proxy_tools-0.1.0.tar.gz
Enter fullscreen mode Exit fullscreen mode

Verify static dir is present inside wheel

unzip -l dist/*.whl | grep static
Enter fullscreen mode Exit fullscreen mode

Export Locked requirements

mkdir -p build
uv export --frozen --no-dev --no-emit-project --extra desktop --no-hashes -o build/requirements.txt
Enter fullscreen mode Exit fullscreen mode

3. uild the bundled runtime

Assemble the AppDir Layout

APP_ID="io.github.viraj_sh.uniclare_client"

mkdir -p AppDir/usr/share/applications
mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps
mkdir -p AppDir/usr/share/metainfo
Enter fullscreen mode Exit fullscreen mode

Copy .desktop .metainfo.xml and .svg in the AppDir

# .desktop → root AND usr/share/applications/
cp "packaging/appimage/$APP_ID.desktop" "AppDir/$APP_ID.desktop"
cp "packaging/appimage/$APP_ID.desktop" "AppDir/usr/share/applications/$APP_ID.desktop"

# .svg → root AND usr/share/icons/hicolor/scalable/apps/
cp "packaging/appimage/$APP_ID.svg" "AppDir/$APP_ID.svg"
cp "packaging/appimage/$APP_ID.svg" "AppDir/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg"

# metainfo → usr/share/metainfo/ only
cp "packaging/appimage/$APP_ID.metainfo.xml" "AppDir/usr/share/metainfo/$APP_ID.metainfo.xml"
Enter fullscreen mode Exit fullscreen mode

Create AppRun

cat > AppDir/AppRun <<'EOF'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
U="$HERE/usr/lib/unidesk"
unset PYTHONHOME PYTHONSTARTUP
export PYTHONPATH="$U/site-packages"
export PYTHONNOUSERSITE=1 PYTHONDONTWRITEBYTECODE=1 PYTHONSAFEPATH=1
export LD_LIBRARY_PATH="$HERE/usr/lib:$HERE/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
export PYWEBVIEW_GUI=qt QT_API=pyqt6 QTWEBENGINE_DISABLE_SANDBOX=1
export QTWEBENGINEPROCESS_PATH="$U/site-packages/PyQt6/Qt6/libexec/QtWebEngineProcess"
export QTWEBENGINE_RESOURCES_PATH="$U/site-packages/PyQt6/Qt6/resources"
exec "$U/python/bin/python3" -m uniclare_client.interfaces.desktop "$@"
EOF

chmod +x AppDir/AppRun
Enter fullscreen mode Exit fullscreen mode

Copy interpreter

PY_ROOT=$(dirname $(dirname $(readlink -f $(uv python find 3.12 --no-project))))
echo "PY_ROOT=$PY_ROOT"
test -x "$PY_ROOT/bin/python3" && echo "OK: interpreter exists"
Enter fullscreen mode Exit fullscreen mode
mkdir -p AppDir/usr/lib/unidesk/python
cp -a "$PY_ROOT"/. AppDir/usr/lib/unidesk/python/
Enter fullscreen mode Exit fullscreen mode
uv pip install --python AppDir/usr/lib/unidesk/python/bin/python3 \
  --target AppDir/usr/lib/unidesk/site-packages \
  --no-deps --no-build \
  --find-links build/wheels \
  -r build/requirements.txt dist/uniclare_client-3.0.0-py3-none-any.whl
Enter fullscreen mode Exit fullscreen mode

4. Fix QtWebEngineProcess permissions (required, otherwise white screen)

find AppDir -name "QtWebEngineProcess" -exec chmod +x {} \;
Enter fullscreen mode Exit fullscreen mode

5. Package AppImage

./appimagetool-x86_64.AppImage AppDir uniclare_client.AppImage
Enter fullscreen mode Exit fullscreen mode

Steps 1-3 stay exactly as they are. Replace steps 4 and 5 with the ones below. Your du output shows the weight is in site-packages (540M, which is PyQt6 and the Chromium engine) and the bundled Python (111M, which carries tests, tcl/tk, static libs and pip).

One correction to my earlier script: it deleted the whole Qt6/translations folder, and that folder holds qtwebengine_locales. That could have caused the white screen again, so the version below keeps en-US.pak.

4. Prune the AppDir (new step, save as prune.sh and run bash prune.sh)

set -e
U=AppDir/usr/lib/unidesk
PYLIB=$U/python/lib/python3.12
SP=$U/site-packages
QT=$SP/PyQt6/Qt6
QTLIB=$QT/lib

############ A. Bundled Python ############
rm -rf $PYLIB/{test,idlelib,tkinter,turtledemo,lib2to3,ensurepip,pydoc_data}
rm -f  $PYLIB/turtle.py
rm -f  $PYLIB/lib-dynload/_tkinter*.so $PYLIB/lib-dynload/_test*.so \
       $PYLIB/lib-dynload/_ctypes_test*.so $PYLIB/lib-dynload/xxlimited*.so
rm -rf $PYLIB/site-packages/pip*
rm -rf $U/python/{include,share}
rm -rf $U/python/lib/{tcl*,tk*,itcl*,thread*}
rm -f  $U/python/lib/libtcl* $U/python/lib/libtk*
rm -f  $U/python/bin/{pip*,idle*,pydoc*,2to3*,python3.12-config}
find $U/python -name "*.a" -delete

############ B. Generic site-packages junk ############
find $SP -type d -name tests -prune -exec rm -rf {} +
find $SP -name "*.pyi" -delete
rm -rf $SP/PyQt6/bindings $SP/PyQt6/uic/widget-plugins

############ C. Qt: translations (keep only the WebEngine en-US locale) ############
find $QT/translations -type f ! -path '*qtwebengine_locales/en-US.pak' -delete
find $QT/translations -type d -empty -delete
rm -f $QT/resources/qtwebengine_devtools_resources.pak   # only needed for debug=True / remote devtools
rm -rf $QT/qml $QT/bin

############ D. Qt: plugins you don't need ############
rm -rf $QT/plugins/{designer,sqldrivers,multimedia,qmltooling,sensors,position,geoservices,texttospeech,qmllint}

############ E. Qt: remove unused Python bindings ############
KEEP=" QtCore QtGui QtWidgets QtNetwork QtWebChannel QtWebEngineCore QtWebEngineWidgets QtPrintSupport QtDBus QtOpenGL QtOpenGLWidgets QtSvg "
for f in $SP/PyQt6/*.abi3.so; do
  m=$(basename "$f"); m=${m%%.*}
  case "$KEEP" in *" $m "*) ;; *) rm -f "$f" "$SP/PyQt6/$m.pyi" ;; esac
done

############ F. Qt: delete C++ libs nothing depends on (dependency closure) ############
declare -A need
queue=( $SP/PyQt6/*.abi3.so "$QT/libexec/QtWebEngineProcess" )
while IFS= read -r p; do queue+=("$p"); done < <(find "$QT/plugins" -name '*.so')

while ((${#queue[@]})); do
  f=${queue[0]}; queue=("${queue[@]:1}")
  for n in $(patchelf --print-needed "$f" 2>/dev/null | grep '^libQt6' || true); do
    b=${n%%.so*}
    [[ -n ${need[$b]:-} ]] && continue
    need[$b]=1
    [[ -e $QTLIB/$n ]] && queue+=("$QTLIB/$n")
  done
done

for f in $QTLIB/libQt6*; do
  b=$(basename "$f"); b=${b%%.so*}
  [[ -z ${need[$b]:-} ]] && { echo "removing $b"; rm -f "$f"; }
done

############ G. Permissions (same as before) ############
find AppDir -name "QtWebEngineProcess" -exec chmod +x {} \;

du -sh AppDir
Enter fullscreen mode Exit fullscreen mode

Test before packaging. If it fails, paste me the error output.

./AppDir/AppRun
Enter fullscreen mode Exit fullscreen mode

5. Package with better compression

./appimagetool-x86_64.AppImage --comp zstd \
  --mksquashfs-opt -Xcompression-level --mksquashfs-opt 19 \
  --mksquashfs-opt -b --mksquashfs-opt 1M \
  AppDir uniclare_client.AppImage
Enter fullscreen mode Exit fullscreen mode

If you want it even smaller and don't mind a slightly slower startup, use --comp xz and drop the zstd-specific options.

What to expect

The 651M AppDir should shrink to roughly 400-450M, and the AppImage to about 130-160 MB. Most of what remains is libQt6WebEngineCore and the Chromium resources, which you can't remove while using Qt WebEngine.

What I'd like to see next

To find any remaining big items specific to your app, paste the output of:

du -sh AppDir/usr/lib/unidesk/site-packages/* | sort -h | tail -15
du -sh AppDir/usr/lib/unidesk/site-packages/PyQt6/Qt6/* | sort -h | tail
uv python find 3.12 --no-project
Enter fullscreen mode Exit fullscreen mode

The last command confirms the copied Python is a uv-managed standalone build and not the system Python. If it prints something under /usr, that alone would explain part of the 111M.

Top comments (0)