#!/bin/bash # Bundles a fully self-contained Python + GTK4 + gtk4-layer-shell + pycairo # runtime into $1, closing over the *entire* shared-library dependency graph # via ldd (not just evdev/python-xlib like the original build). This is for # targets like NixOS that have no FHS /usr/bin/python3 or system GTK4 on # PATH at all, so relying on "the host probably has this" doesn't hold. # # Includes the target's own ld.so + libc so it doesn't depend on the host # glibc version either - AppRun invokes that ld.so explicitly with # --library-path instead of a normal dynamic-linked exec. set -euo pipefail RT="$1" mkdir -p "${RT}/lib" "${RT}/bin" "${RT}/typelibs" PYBIN="$(python3 -c 'import sys; print(sys.executable)')" STDLIB="$(python3 -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')" echo "Bundling python3 (${PYBIN}) + stdlib (${STDLIB})..." cp "${PYBIN}" "${RT}/bin/python3" mkdir -p "${RT}/python-lib" cp -r "${STDLIB}" "${RT}/python-lib/" DIST_PKGS="/usr/lib/python3/dist-packages" SEEDS=( "${PYBIN}" "${DIST_PKGS}/gi/_gi.cpython-313-x86_64-linux-gnu.so" "${DIST_PKGS}/gi/_gi_cairo.cpython-313-x86_64-linux-gnu.so" "${DIST_PKGS}/cairo/_cairo.cpython-313-x86_64-linux-gnu.so" "/usr/lib/x86_64-linux-gnu/libgtk-4.so.1" "/usr/lib/x86_64-linux-gnu/libgtk4-layer-shell.so.0" ) echo "Copying gi, cairo, evdev, Xlib Python packages..." cp -r "${DIST_PKGS}/gi" "${RT}/" cp -r "${DIST_PKGS}/cairo" "${RT}/" cp -r "${DIST_PKGS}/evdev" "${RT}/" cp -r "${DIST_PKGS}/Xlib" "${RT}/" for so in "${DIST_PKGS}"/evdev/*.so; do SEEDS+=("$so") done echo "Resolving full shared-library closure via ldd..." declare -A SEEN QUEUE=("${SEEDS[@]}") while [ "${#QUEUE[@]}" -gt 0 ]; do f="${QUEUE[0]}" QUEUE=("${QUEUE[@]:1}") realf="$(readlink -f "$f")" fname="$(basename "$f")" realname="$(basename "$realf")" # ldd/DT_NEEDED reference libraries by their SONAME (e.g. # libgirepository-1.0.so.1), which on Debian is often just a symlink to a # fully-versioned real file (libgirepository-1.0.so.1.0.0). readlink -f # collapses that symlink, so without this alias the dynamic linker can't # find the file under the name it's actually asked for at runtime. if [ "$fname" != "$realname" ] && [ ! -e "${RT}/lib/${fname}" ]; then ln -s "$realname" "${RT}/lib/${fname}" fi [ -n "${SEEN[$realf]:-}" ] && continue SEEN["$realf"]=1 cp -n "$realf" "${RT}/lib/${realname}" 2>/dev/null || true for tok in $(ldd "$realf" 2>/dev/null | grep -o '/[^ ]*'); do QUEUE+=("$tok") done done echo "Bundled ${#SEEN[@]} shared libraries." echo "Resolving full typelib closure via GIRepository dependency graph..." TYPELIB_DIR="/usr/lib/x86_64-linux-gnu/girepository-1.0" # Hardcoding the namespace list by hand previously missed transitive deps # (Graphene-1.0, freetype2-2.0, xlib-2.0 were absent, breaking `import gi` # on Gtk.init() with "Typelib file for namespace 'Graphene' not found"). # Instead ask GIRepository itself what Gtk/GdkX11/GdkWayland/Gtk4LayerShell # transitively require, so the list can't silently drift from reality. NAMESPACES="$(python3 -c ' import gi from gi.repository import GIRepository r = GIRepository.Repository.get_default() roots = [("Gtk", "4.0"), ("GdkX11", "4.0"), ("GdkWayland", "4.0"), ("Gtk4LayerShell", "1.0")] seen = {} stack = [] for ns, ver in roots: r.require(ns, ver, 0) seen[ns] = ver stack.append(ns) i = 0 while i < len(stack): ns = stack[i] i += 1 for d in r.get_immediate_dependencies(ns): dep_ns, dep_ver = d.rsplit("-", 1) if dep_ns not in seen: r.require(dep_ns, dep_ver, 0) seen[dep_ns] = dep_ver stack.append(dep_ns) for ns, ver in sorted(seen.items()): print(f"{ns}-{ver}") ' 2>/dev/null)" for name in ${NAMESPACES}; do src="${TYPELIB_DIR}/${name}.typelib" [ -f "$src" ] && cp "$src" "${RT}/typelibs/" done echo "Bundled typelibs: ${NAMESPACES}" echo "Runtime bundle assembled in ${RT}"