#!/bin/bash # Build every distribution channel of testium: # 1. Manual PDF -> dist/testium-manual-.pdf # 2. Wheel -> dist/testium--py3-none-any.whl (PEP 427 name) # 3. PyInstaller binary -> dist/testium- # 4. Flatpak bundle -> dist/testium-.flatpak # 5. AppImage -> dist/Testium--x86_64.AppImage (original name) # release_note.txt is copied to dist/ up front (with a warning if it has no # entry for the current version). # # By default, a step is skipped if its artifact already exists in dist/. # Pass --clean to remove existing dist/ artifacts and rebuild everything. # # Parallelism: the wheel is built first (the AppImage installs it), then the # manual, PyInstaller, Flatpak and AppImage builds run concurrently. The shared # venv at test/tmp/.venv is only WRITTEN during the serial prep phase (the # `pip install` of build/sphinx/pyinstaller); the parallel builds only read it, # so there is no concurrent-pip race. Pass --serial to build one step at a time # (useful when debugging or on a resource-constrained machine). Per-step output # of the parallel phase is captured under dist/.build-logs/.log and the # log of any failing step is printed at the end. # # Pass --ram to redirect the per-channel build scratch (PyInstaller workpath, # AppImage AppDir) and TMPDIR/PIP_CACHE_DIR to /dev/shm, and skip UPX. Big # speedup on slow/flash storage. Flatpak is excluded (its rofiles-fuse can't # mount on /dev/shm), so it still builds on disk. On a RAM-limited machine # combine with --serial (e.g. ./build_all.sh --ram --serial). # # All artifacts are collected (copied) under /dist/. Original outputs in # src/dist/, package/*/dist/, doc/manual/ are left in place. Wheel and AppImage # keep their original names (which already contain the version); manual, # pyinstaller and flatpak are renamed to testium(-manual)-(.suff). # # Re-uses scripts/build_env.sh and scripts/set_env.sh — the same pair invoked # by run.sh — so the venv at test/tmp/.venv stays the single source of Python # dependencies. `build`, `pyinstaller`, `sphinx` and `linuxdoc` are installed # into that venv on demand if not already there. Flatpak and AppImage build in # their own container/sandbox; their build.sh scripts have their own toolchain # checks. set -e CLEAN=0 SERIAL=0 RAM=0 for arg in "$@"; do case "$arg" in --clean|-c) CLEAN=1 ;; --serial) SERIAL=1 ;; --ram) RAM=1 ;; *) echo "Unknown option: $arg" >&2; exit 1 ;; esac done SCRIPT_DIR=$(realpath "$(dirname "$0")") VERSION=$(cat "$SCRIPT_DIR/src/VERSION") DIST_DIR="$SCRIPT_DIR/dist" mkdir -p "$DIST_DIR" if [ "$CLEAN" -eq 1 ]; then echo "-- clean: removing existing dist artifacts for version $VERSION" rm -f "$DIST_DIR/testium-manual-${VERSION}.pdf" rm -f "$DIST_DIR"/testium-${VERSION}-*.whl rm -f "$DIST_DIR/testium-${VERSION}" rm -f "$DIST_DIR/testium-${VERSION}.flatpak" rm -f "$DIST_DIR"/Testium-${VERSION}-*.AppImage fi # Release note: copy it to dist/ and warn (but don't fail) if it has no entry # for the current version. RELEASE_NOTE_SRC="$SCRIPT_DIR/release_note.txt" RELEASE_NOTE="$DIST_DIR/release_note.txt" cp -f "$RELEASE_NOTE_SRC" "$RELEASE_NOTE" if ! grep -qE "^version $VERSION([^.0-9]|$)" "$RELEASE_NOTE_SRC"; then echo "WARNING: release_note.txt has no entry for version $VERSION." >&2 fi export PY_VENV_NAME=".venv" export PY_VENV_DIR="$SCRIPT_DIR/test/tmp/$PY_VENV_NAME" export REQ_PATH="$SCRIPT_DIR/src/requirements.txt" bash "$SCRIPT_DIR/scripts/build_env.sh" source "$SCRIPT_DIR/scripts/set_env.sh" # ---------- RAM mode: put build scratch on tmpfs (--ram) ---------------------- # On slow storage (USB stick, SD card) the per-channel build dirs and temp # churn dominate. --ram redirects the PyInstaller workpath, the AppImage AppDir # and TMPDIR/PIP_CACHE_DIR to /dev/shm, and skips UPX. Flatpak is intentionally # NOT moved: flatpak-builder mounts its state dir with rofiles-fuse, and FUSE # can't mount on /dev/shm (fusermount: Permission denied) — so it builds on # disk. The tmpfs scratch is freed on exit. if [ "$RAM" -eq 1 ]; then RAMROOT="/dev/shm/testium-build-${VERSION}" echo "-- RAM mode: build scratch under $RAMROOT (tmpfs), freed on exit" echo " (flatpak builds on disk — rofiles-fuse can't mount on /dev/shm)" rm -rf "$RAMROOT" mkdir -p "$RAMROOT"/{tmp,pip,pyi-work,appdir} export TMPDIR="$RAMROOT/tmp" export PIP_CACHE_DIR="$RAMROOT/pip" export PYI_WORKPATH="$RAMROOT/pyi-work" # pyinstaller --workpath export APPIMAGE_APPDIR_TMPFS="$RAMROOT/appdir" # AppDir bind-mount export TESTIUM_NO_UPX=1 # skip slow UPX in the spec trap 'rm -rf "$RAMROOT"' EXIT if [ "$SERIAL" -ne 1 ]; then echo " note: with --ram, prefer adding --serial so each step gets the" echo " full tmpfs and you don't risk OOM (flatpak+appimage are ~1 GB each)." fi fi step() { echo echo "================================================================" echo " $1" echo "================================================================" } # Kill a process and its whole descendant tree (children first) — used by the # interrupt handler so SIGINT also stops grandchildren the parallel jobs spawned # (podman container, flatpak-builder, pyinstaller …), not just the subshells. _kill_tree() { local pid=$1 c for c in $(pgrep -P "$pid" 2>/dev/null); do _kill_tree "$c" done kill -TERM "$pid" 2>/dev/null || true } # Set as INT/TERM handler around the parallel wait. Stops every running build # tree, then exits — the EXIT trap (set under --ram) frees the tmpfs scratch. _interrupt() { echo >&2 echo "-- interrupted: stopping running builds…" >&2 local pid for pid in "${!PID2NAME[@]}"; do _kill_tree "$pid" done exit 130 } # ---------- artifact paths ---------------------------------------------------- MANUAL="$DIST_DIR/testium-manual-${VERSION}.pdf" PYI_BIN="$DIST_DIR/testium-${VERSION}" FLATPAK_BUNDLE="$DIST_DIR/testium-${VERSION}.flatpak" wheel_in_dist() { ls -1t "$DIST_DIR"/testium-${VERSION}-*.whl 2>/dev/null | head -1; } appimage_in_dist() { ls -1t "$DIST_DIR"/Testium-${VERSION}-*.AppImage 2>/dev/null | head -1; } # ---------- per-step build functions (assume tools are installed) ------------- build_wheel() { if [ -n "$(wheel_in_dist)" ]; then echo "wheel: already built — skipping"; return 0; fi echo "wheel: building" ( cd "$SCRIPT_DIR/src" rm -rf dist build *.egg-info python -m build --wheel ) local src; src=$(ls -1t "$SCRIPT_DIR/src/dist"/*.whl | head -1) cp -f "$src" "$DIST_DIR/$(basename "$src")" echo "wheel: done" } build_manual() { if [ -f "$MANUAL" ]; then echo "manual: already built — skipping"; return 0; fi echo "manual: building" bash "$SCRIPT_DIR/doc/manual/sphinx/build_doc.sh" cp -f "$SCRIPT_DIR/doc/manual/testium_manual.pdf" "$MANUAL" echo "manual: done" } build_pyinstaller() { if [ -f "$PYI_BIN" ]; then echo "pyinstaller: already built — skipping"; return 0; fi echo "pyinstaller: building" bash "$SCRIPT_DIR/package/pyinstaller/build.sh" cp -f "$SCRIPT_DIR/package/pyinstaller/dist/testium" "$PYI_BIN" echo "pyinstaller: done" } build_flatpak() { if [ -f "$FLATPAK_BUNDLE" ]; then echo "flatpak: already built — skipping"; return 0; fi echo "flatpak: building" ( cd "$SCRIPT_DIR/package/flatpak" bash build.sh ) cp -f "$SCRIPT_DIR/package/flatpak/testium.flatpak" "$FLATPAK_BUNDLE" echo "flatpak: done" } build_appimage() { if [ -n "$(appimage_in_dist)" ]; then echo "appimage: already built — skipping"; return 0; fi echo "appimage: building" ( cd "$SCRIPT_DIR/package/appimage" bash build.sh ) local src; src=$(ls -1t "$SCRIPT_DIR/package/appimage"/*.AppImage 2>/dev/null | head -1) cp -f "$src" "$DIST_DIR/$(basename "$src")" chmod +x "$DIST_DIR/$(basename "$src")" echo "appimage: done" } # ---------- serial prep: tool installs (shared venv) + flatpak runtimes ------- step "Prep: build tools + runtimes (serial — shared venv)" [ -f "$MANUAL" ] || python -m pip install --quiet --upgrade sphinx linuxdoc [ -n "$(wheel_in_dist)" ] || python -m pip install --quiet --upgrade build [ -f "$PYI_BIN" ] || python -m pip install --quiet --upgrade pyinstaller if [ ! -f "$FLATPAK_BUNDLE" ]; then FLATPAK_DEPS=( "org.kde.Platform//6.10" "org.kde.Sdk//6.10" "io.qt.PySide.BaseApp//6.10" ) if ! flatpak remotes --user | grep -q "^flathub"; then echo " Adding Flathub remote" flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo fi for dep in "${FLATPAK_DEPS[@]}"; do if ! flatpak info --user "$dep" &>/dev/null && ! flatpak info --system "$dep" &>/dev/null; then echo " Installing Flatpak dependency: $dep" flatpak install --user --noninteractive flathub "$dep" fi done fi # ---------- serial: wheel (the AppImage installs it) -------------------------- step "1/5 Wheel (version $VERSION)" build_wheel # ---------- build the rest -------------------------------------------------- REST=(manual pyinstaller flatpak appimage) if [ "$SERIAL" -eq 1 ]; then n=2 for name in "${REST[@]}"; do step "$n/5 $name (version $VERSION)" "build_$name" n=$((n + 1)) done else step "2-5/5 manual + pyinstaller + flatpak + appimage (parallel)" LOGDIR="$DIST_DIR/.build-logs" mkdir -p "$LOGDIR" declare -A PID2NAME for name in "${REST[@]}"; do log="$LOGDIR/$name.log" echo " -> launching $name (log: $log)" ( "build_$name" ) >"$log" 2>&1 & PID2NAME[$!]="$name" done # From here until all jobs are reaped, Ctrl+C stops every build tree. trap _interrupt INT TERM # Reap in completion order (wait -n) so each result prints the moment that # build finishes, not when its slot comes up in the array. FAILED=() remaining=${#PID2NAME[@]} while [ "$remaining" -gt 0 ]; do if wait -n -p donepid; then rc=0; else rc=$?; fi name="${PID2NAME[$donepid]:-}" [ -z "$name" ] && continue if [ "$rc" -eq 0 ]; then echo " -> $name: OK" else echo " -> $name: FAILED (rc=$rc)" FAILED+=("$name") fi remaining=$((remaining - 1)) done trap - INT TERM if [ "${#FAILED[@]}" -gt 0 ]; then for name in "${FAILED[@]}"; do echo echo "===================== $name log =====================" cat "$LOGDIR/$name.log" done echo >&2 echo "BUILD FAILED: ${FAILED[*]} (logs under $LOGDIR)" >&2 exit 1 fi fi # ---------- summary ----------------------------------------------------------- step "All packages built" printf " manual : %s\n" "$MANUAL" printf " wheel : %s\n" "$(wheel_in_dist)" printf " pyinstaller : %s\n" "$PYI_BIN" printf " flatpak : %s\n" "$FLATPAK_BUNDLE" printf " appimage : %s\n" "$(appimage_in_dist)" printf " release_note : %s\n" "$RELEASE_NOTE"