You're right—my previous answer assumed you already had an AppDir. Since you want to start completely from scratch, the most straightforward path is to reuse the exact same self-contained runtime construction logic from your AppImage tutorial, but instead of packaging the result into an AppImage, directly place that runtime directory into the .deb file structure.

The core idea is: build the Python environment first, then package it with dpkg-deb. Below are the complete steps.

Step 1: Install build tools and runtime dependencies

In Ubuntu, you need dpkg-deb (used to package .deb), uv (used to manage Python and build wheels), and some basic system libraries (for Qt/WebEngine runtime).

sudo apt update sudo apt install -y dpkg-dev fakeroot curl patchelf file libgl1 libglib2.0-0 libfontconfig1 libx11-6 libxkbcommon0 libdbus-1-3 

Install uv (if not already installed):

curl -LsSf https://astral.sh/uv/install.sh | sh source $HOME/.local/bin/env 

Step 2: Prepare a self-contained Python runtime (completely from scratch)

This step replicates the runtime construction logic from your AppImage tutorial, but installs into a local directory instead of AppDir.

PKG="uniclare-client" VER="3.0.0" ARCH="amd64" RUNTIME="runtime" # 替代 AppDir,用来存放自包含运行时 rm -rf "$RUNTIME" mkdir -p "$RUNTIME/usr/lib/unidesk" 

Get the Python interpreter (using the Python 3.12 managed by uv):

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" # 复制解释器到运行时目录 cp -a "$PY_ROOT"/. "$RUNTIME/usr/lib/unidesk/python/" 

Build the project wheel and dependency wheels (assuming you're at the project root and have pyproject.toml):

uv build --wheel mkdir -p build/wheels # 如果项目依赖中有 PyPI 上只有 sdist 的包(如 proxy-tools),按你原教程的方式构建 wheel # uv build --wheel --out-dir build/wheels build/wheels/proxy_tools-0.1.0.tar.gz # 导出锁定依赖(不含项目自身) uv export --frozen --no-dev --no-emit-project --extra desktop --no-hashes -o build/requirements.txt 

Install all Python dependencies into the runtime (using the just-copied interpreter, installing to site-packages):

uv pip install --python "$RUNTIME/usr/lib/unidesk/python/bin/python3" \ --target "$RUNTIME/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 

Now runtime/usr/lib/unidesk/ contains the Python interpreter and all Python packages (including PyQt6 and WebEngine).

Step 3: (Strongly recommended) Prune the runtime

If you skip this step, the .deb will be very large (your AppImage tutorial mentions it can be reduced from 651M to around 400–450M). The pruning script from your original tutorial can be used directly, just change U=AppDir/... to U=runtime/usr/lib/unidesk/....

Create prune.sh (content based on your AppImage tutorial, replacing paths):

cat > prune.sh <<'PRUNE' set -e U=runtime/usr/lib/unidesk PYLIB=$U/python/lib/python3.12 SP=$U/site-packages QT=$SP/PyQt6/Qt6 QTLIB=$QT/lib # A. 精简内置 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. 通用 site-packages 清理 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 翻译(只保留 WebEngine en-US) 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 rm -rf $QT/qml $QT/bin # D. Qt 插件裁剪 rm -rf $QT/plugins/{designer,sqldrivers,multimedia,qmltooling,sensors,position,geoservices,texttospeech,qmllint} # E. 精简 PyQt6 绑定 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 C++ 库 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]:-} ]] && rm -f "$f" done # G. 权限 find runtime -name "QtWebEngineProcess" -exec chmod +x {} \; PRUNE bash prune.sh 

Step 4: Create the AppRun launcher (inside the runtime directory)

This is the entry point for the runtime; /usr/bin/uniclare-client will call it later.

cat > "$RUNTIME/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 "$RUNTIME/AppRun" 

Test that the runtime starts properly (before packaging):

./runtime/AppRun 

Step 5: Build the .deb directory structure

Now create the Debian package structure and move the runtime in.

PKG="uniclare-client" VER="3.0.0" ARCH="amd64" APP_ID="io.github.viraj_sh.uniclare_client" rm -rf deb-build mkdir -p deb-build/$PKG/DEBIAN mkdir -p deb-build/$PKG/opt/$PKG mkdir -p deb-build/$PKG/usr/bin mkdir -p deb-build/$PKG/usr/share/applications mkdir -p deb-build/$PKG/usr/share/icons/hicolor/scalable/apps mkdir -p deb-build/$PKG/usr/share/metainfo # 把整个 runtime 放进 /opt/uniclare-client/ cp -a runtime/. deb-build/$PKG/opt/$PKG/ 

At this point, the runtime inside the .deb is located at /opt/uniclare-client/, and its AppRun is at /opt/uniclare-client/AppRun.

Step 6: Add desktop integration files

Use the .desktop, .svg, and .metainfo.xml from your project (paths based on your original tutorial):

install -Dm644 "packaging/appimage/$APP_ID.desktop" \ "deb-build/$PKG/usr/share/applications/$APP_ID.desktop" install -Dm644 "packaging/appimage/$APP_ID.svg" \ "deb-build/$PKG/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg" install -Dm644 "packaging/appimage/$APP_ID.metainfo.xml" \ "deb-build/$PKG/usr/share/metainfo/$APP_ID.metainfo.xml" 

Modify the Exec line in the .desktop file to point to the Debian package's startup command:

sed -i 's|^Exec=.*|Exec=/usr/bin/uniclare-client|' \ "deb-build/$PKG/usr/share/applications/$APP_ID.desktop" 

Step 7: Create the /usr/bin launcher

cat > "deb-build/$PKG/usr/bin/uniclare-client" <<'EOF' #!/bin/sh exec /opt/uniclare-client/AppRun "$@" EOF chmod 755 "deb-build/$PKG/usr/bin/uniclare-client" 

Step 8: Write the DEBIAN/control file

cat > "deb-build/$PKG/DEBIAN/control" <<EOF Package: $PKG Version: $VER Section: utils Priority: optional Architecture: $ARCH Maintainer: viraj-sh <viraj-sh@users.noreply.github.com> Depends: libc6, libgl1, libglib2.0-0, libfontconfig1, libx11-6, libxkbcommon0, libdbus-1-3 Description: Uniclare Client View data that is hidden on the Uniclare App or Student Portal. EOF 

Note: You do not need to list python3 or python3-pyqt6 in Depends because you've bundled Python and PyQt6 yourself, so you don't rely on system packages. The listed system libraries are the basic dependencies needed for Qt/WebEngine to run, and they are present on virtually every Ubuntu desktop system.

Step 9: Set permissions and build

chmod 755 deb-build/$PKG/DEBIAN chmod 644 deb-build/$PKG/DEBIAN/control find deb-build/$PKG -type d -exec chmod 755 {} \; chmod 755 deb-build/$PKG/usr/bin/uniclare-client dpkg-deb --build --root-owner-group deb-build/$PKG "uniclare-client_${VER}_${ARCH}.deb" 

Step 10: Install and test

sudo dpkg -i "uniclare-client_${VER}_${ARCH}.deb" # 如果有依赖缺失: sudo apt install -f # 运行 uniclare-client 

You should also be able to find and launch it from the application menu.

Uninstall:

sudo dpkg -r uniclare-client 

Key differences from your original tutorial

Item AppImage tutorial This .deb procedure
Runtime location AppDir/ runtime/ (only a name change)
Runtime placement in package / (AppImage root) /opt/uniclare-client/
Entry point AppRun executed directly AppRun called via /usr/bin/uniclare-client
Packaging tool appimagetool dpkg-deb

The core "from scratch building Python + PyQt6 + WebEngine runtime" logic is completely identical to your AppImage tutorial; only the final packaging container is different. As long as ./runtime/AppRun can start the app normally, the .deb will also work normally.