DEV Community

viraj-sh
viraj-sh

Posted on

Create .deb package of python project

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
Enter fullscreen mode Exit fullscreen mode

Install uv (if not already installed):

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

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"
Enter fullscreen mode Exit fullscreen mode

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/"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Test that the runtime starts properly (before packaging):

./runtime/AppRun
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Step 10: Install and test

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

# 运行
uniclare-client
Enter fullscreen mode Exit fullscreen mode

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

Uninstall:

sudo dpkg -r uniclare-client
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)