1. Install required tools
sudo apt update
sudo apt install git patchelf desktop-file-utils appstream file nodejs npm libfuse2t64 curl
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
Make both appimages executable
chmod +x linuxdeploy-x86_64.AppImage appimagetool-x86_64.AppImage
2. Wheel
uv build --wheel
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
ls -l build/wheels/proxy_tools-0.1.0.tar.gz
file build/wheels/proxy_tools-0.1.0.tar.gz
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
Verify static dir is present inside wheel
unzip -l dist/*.whl | grep static
Export Locked requirements
mkdir -p build
uv export --frozen --no-dev --no-emit-project --extra desktop --no-hashes -o build/requirements.txt
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
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"
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
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"
mkdir -p AppDir/usr/lib/unidesk/python
cp -a "$PY_ROOT"/. AppDir/usr/lib/unidesk/python/
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
4. Fix QtWebEngineProcess permissions (required, otherwise white screen)
find AppDir -name "QtWebEngineProcess" -exec chmod +x {} \;
5. Package AppImage
./appimagetool-x86_64.AppImage AppDir uniclare_client.AppImage
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
Test before packaging. If it fails, paste me the error output.
./AppDir/AppRun
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
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
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)