Initial commit: Korin Timer
Build AppImage / build-appimage (push) Successful in 42s

This commit is contained in:
2026-08-03 20:42:26 +02:00
commit 7edbd09538
29 changed files with 3238 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
set -e
HERE="$(dirname "$(readlink -f "${0}")")"
RT="${HERE}/usr/lib/titletimer/rt"
SRC="${HERE}/usr/lib/titletimer/src"
# Fully self-contained: our own Python + GTK4 + gtk4-layer-shell + PyGObject
# + pycairo + evdev + python-xlib + ld.so/libc, none of it from the host.
# Needed on targets like NixOS with no FHS python3/GTK4 on PATH at all.
export PYTHONHOME="${RT}/python-lib"
export PYTHONPATH="${RT}:${RT}/python-lib/python3.13:${RT}/python-lib/python3.13/lib-dynload:${SRC}"
export GI_TYPELIB_PATH="${RT}/typelibs"
export LD_LIBRARY_PATH="${RT}/lib"
export GDK_BACKEND=x11
exec "${RT}/lib/ld-linux-x86-64.so.2" --library-path "${RT}/lib" "${RT}/bin/python3" -m titletimer "$@"
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# Builds Korin-Timer.AppImage as a FULLY self-contained
# bundle: its own Python interpreter + stdlib, GTK4, gtk4-layer-shell,
# PyGObject, pycairo, evdev, python-xlib, and every transitive shared
# library they need (~75 .so files) including ld.so + libc themselves.
#
# Why: this targets NixOS, which has no FHS /usr/bin/python3 or system
# GTK4 on PATH at all - "the host probably already has this" (the usual
# AppImage assumption) doesn't hold there. Bundling everything, including
# the dynamic linker, means it doesn't depend on the host's glibc version
# either.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="${ROOT}/build"
APPDIR="${BUILD_DIR}/AppDir"
rm -rf "${APPDIR}"
mkdir -p "${APPDIR}/usr/lib/titletimer/src"
mkdir -p "${APPDIR}/usr/share/applications"
mkdir -p "${APPDIR}/usr/share/icons/hicolor/64x64/apps"
mkdir -p "${APPDIR}/usr/share/icons/hicolor/128x128/apps"
mkdir -p "${APPDIR}/usr/share/icons/hicolor/256x256/apps"
cp -r "${ROOT}/src/titletimer" "${APPDIR}/usr/lib/titletimer/src/"
cp "${ROOT}/packaging/AppRun" "${APPDIR}/AppRun"
chmod +x "${APPDIR}/AppRun"
cp "${ROOT}/packaging/korin-timer.desktop" "${APPDIR}/"
cp "${ROOT}/packaging/korin-timer.desktop" "${APPDIR}/usr/share/applications/"
# Pre-sized on the dev host (packaging/icon-*.png) rather than resized here at
# build time - keeps this script's dependencies to what it already needs
# (no ImageMagick/GdkPixbuf-CLI requirement inside the build container).
cp "${ROOT}/packaging/icon-256.png" "${APPDIR}/titletimer.png"
cp "${ROOT}/packaging/icon-64.png" "${APPDIR}/usr/share/icons/hicolor/64x64/apps/titletimer.png"
cp "${ROOT}/packaging/icon-128.png" "${APPDIR}/usr/share/icons/hicolor/128x128/apps/titletimer.png"
cp "${ROOT}/packaging/icon-256.png" "${APPDIR}/usr/share/icons/hicolor/256x256/apps/titletimer.png"
bash "${ROOT}/packaging/bundle-runtime.sh" "${APPDIR}/usr/lib/titletimer/rt"
OUT="${BUILD_DIR}/Korin-Timer.AppImage"
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
if [ ! -x "${APPIMAGETOOL}" ]; then
echo "Downloading appimagetool..."
curl -L -o "${APPIMAGETOOL}" \
https://github.com/AppImage/appimagetool/releases/latest/download/appimagetool-x86_64.AppImage
chmod +x "${APPIMAGETOOL}"
fi
if ARCH=x86_64 "${APPIMAGETOOL}" "${APPDIR}" "${OUT}" 2>/tmp/appimagetool.err; then
echo "Built ${OUT}"
else
# appimagetool's AppImage is itself a static-PIE ELF (no PT_INTERP). Some
# restricted/sandboxed environments can't exec that binary format at all
# (execve fails outright, unrelated to FUSE) - if so, fall back to
# assembling the AppImage by hand: a type-2 runtime stub concatenated with
# a plain squashfs of the AppDir is exactly what appimagetool does
# internally anyway.
echo "appimagetool failed to run directly (see /tmp/appimagetool.err)." >&2
echo "Falling back to manual runtime+squashfs assembly..." >&2
RUNTIME="${BUILD_DIR}/runtime-x86_64"
if [ ! -f "${RUNTIME}" ]; then
curl -L -o "${RUNTIME}" \
https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64
fi
SQUASHFS="${BUILD_DIR}/AppDir.squashfs"
rm -f "${SQUASHFS}"
mksquashfs "${APPDIR}" "${SQUASHFS}" -root-owned -noappend -quiet
cat "${RUNTIME}" "${SQUASHFS}" > "${OUT}"
chmod +x "${OUT}"
echo "Built ${OUT} (manual assembly)"
fi
+110
View File
@@ -0,0 +1,110 @@
#!/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}"
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Korin Timer
Comment=2-step title switching timer for Elsword
Exec=AppRun
Icon=titletimer
Categories=Game;Utility;
Terminal=false