From d5fb872518e182a42c84eb9398043eaf68ad2254 Mon Sep 17 00:00:00 2001 From: Seth Freiberg Date: Wed, 29 Apr 2026 11:14:58 -0400 Subject: [PATCH] feat: add build-appimages.sh with inline smoke tests T3, T4 Co-Authored-By: Claude Sonnet 4.6 --- scripts/build-appimages.sh | 161 +++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100755 scripts/build-appimages.sh diff --git a/scripts/build-appimages.sh b/scripts/build-appimages.sh new file mode 100755 index 0000000..5d6830a --- /dev/null +++ b/scripts/build-appimages.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Build sethLabels AppImages (GUI + batch). +# +# Pipeline (spec §5.3): +# 1. sanity / guardrail / version-compute (same as build-deb.sh) +# 2. out-of-tree cmake build with CMAKE_INSTALL_PREFIX=/usr +# 3. cmake --install to staging AppDir +# 4. linuxdeploy bundle GUI AppImage +# 5. re-stage AppDir for batch-only, linuxdeploy bundle batch AppImage +# 6. inline smoke tests T3, T4 +# 7. print artifact paths +# +# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.3 +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +echo "==> [1/6] Sanity check build host" +"$REPO_ROOT/scripts/lib/deps-debian.sh" +echo "==> [1/6] Strict-zero guardrail" +"$REPO_ROOT/scripts/check-no-upstream-edits.sh" +echo "==> [1/6] Compute version" +VERSION="$("$REPO_ROOT/scripts/compute-version.sh")" +echo " VERSION = $VERSION" + +# Bootstrap linuxdeploy + plugin-qt; defines $LINUXDEPLOY_BIN and $LINUXDEPLOY_PLUGIN_QT_BIN. +# shellcheck disable=SC1091 +source "$REPO_ROOT/scripts/lib/linuxdeploy.sh" +# linuxdeploy looks for the plugin in PATH; symlink into the cache dir suffices. +PLUGIN_DIR="$(dirname "$LINUXDEPLOY_PLUGIN_QT_BIN")" +PATH="$PLUGIN_DIR:$PATH" +# Plugin file must be named exactly `linuxdeploy-plugin-qt` (no version suffix). +PLUGIN_LINK="$PLUGIN_DIR/linuxdeploy-plugin-qt" +ln -sf "$LINUXDEPLOY_PLUGIN_QT_BIN" "$PLUGIN_LINK" +chmod +x "$PLUGIN_LINK" +# On Debian 13 / Qt6, the qmake symlink may resolve via qtchooser to qt5. +# Force the plugin to use the Qt6 qmake directly. +export QMAKE=/usr/bin/qmake6 + +echo "==> [2/6] Out-of-tree cmake build (install prefix /usr)" +BUILD_DIR="$REPO_ROOT/build/appimage" +APPDIR_GUI="$BUILD_DIR/AppDir-gui" +APPDIR_BATCH="$BUILD_DIR/AppDir-batch" +rm -rf "${BUILD_DIR:?BUILD_DIR must not be empty}" +mkdir -p "$BUILD_DIR" +cmake -S "$REPO_ROOT" -B "$BUILD_DIR" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr +cmake --build "$BUILD_DIR" --parallel + +echo "==> [3/6] Stage install tree to AppDirs" +DESTDIR="$APPDIR_GUI" cmake --install "$BUILD_DIR" +# Batch AppDir gets its own copy so we can prune Qt plugins that GUI needs but batch doesn't. +DESTDIR="$APPDIR_BATCH" cmake --install "$BUILD_DIR" + +# Sanity: both AppDirs must contain both binaries (we strip later, not here). +test -x "$APPDIR_GUI/usr/bin/glabels-qt" || { echo "ERROR: GUI binary missing in AppDir-gui" >&2; exit 1; } +test -x "$APPDIR_BATCH/usr/bin/glabels-batch-qt" || { echo "ERROR: batch binary missing in AppDir-batch" >&2; exit 1; } + +echo "==> [4/6] Bundle GUI AppImage" +DESKTOP_FILE="$APPDIR_GUI/usr/share/applications/glabels-qt.desktop" +ICON_FILE="$APPDIR_GUI/usr/share/icons/hicolor/scalable/apps/glabels.svg" +# Upstream's actual desktop filename may vary — list what's there if missing. +if [ ! -f "$DESKTOP_FILE" ]; then + ALT_DESKTOP=$(find "$APPDIR_GUI/usr/share/applications" -name '*.desktop' | head -1) + if [ -n "$ALT_DESKTOP" ]; then + DESKTOP_FILE="$ALT_DESKTOP" + else + echo "ERROR: no .desktop file found in $APPDIR_GUI/usr/share/applications" >&2 + exit 1 + fi +fi + +cd "$BUILD_DIR" +APPIMAGE_EXTRACT_AND_RUN=1 \ +"$LINUXDEPLOY_BIN" \ + --appdir "$APPDIR_GUI" \ + --plugin qt \ + --executable "$APPDIR_GUI/usr/bin/glabels-qt" \ + --desktop-file "$DESKTOP_FILE" \ + --icon-file "$ICON_FILE" \ + --output appimage + +# linuxdeploy names the AppImage from the desktop file's Name= field (spaces→underscores). +# For this upstream desktop file (Name=gLabels Label Designer 4) that produces +# gLabels_Label_Designer_4-x86_64.AppImage. Use a broad glob and exclude *batch*. +GUI_RAW=$(ls "$BUILD_DIR"/*.AppImage 2>/dev/null | grep -v batch | head -1) +GUI_OUT="$REPO_ROOT/sethlabels-gui-${VERSION}-x86_64.AppImage" +mv "$GUI_RAW" "$GUI_OUT" +chmod +x "$GUI_OUT" +cd "$REPO_ROOT" + +echo "==> [5/6] Bundle batch AppImage" +# Batch doesn't need a desktop file or icon (CLI only). +cd "$BUILD_DIR" +APPIMAGE_EXTRACT_AND_RUN=1 \ +"$LINUXDEPLOY_BIN" \ + --appdir "$APPDIR_BATCH" \ + --plugin qt \ + --executable "$APPDIR_BATCH/usr/bin/glabels-batch-qt" \ + --create-desktop-file \ + --output appimage + +# linuxdeploy names the batch AppImage using the first desktop file it finds (the upstream +# GUI desktop), producing the same name as the GUI build. Since we already moved the GUI +# AppImage out, only one .AppImage remains in BUILD_DIR at this point — pick it directly. +BATCH_RAW=$(ls "$BUILD_DIR"/*.AppImage 2>/dev/null | head -1) +BATCH_OUT="$REPO_ROOT/sethlabels-batch-${VERSION}-x86_64.AppImage" +mv "$BATCH_RAW" "$BATCH_OUT" +chmod +x "$BATCH_OUT" +cd "$REPO_ROOT" + +echo "==> [6/6] Smoke tests" + +# Both AppImages bundle only the xcb Qt platform plugin (linuxdeploy-plugin-qt does not +# include offscreen/minimal). We need a real X display. Use Xvfb if available; if not, +# require DISPLAY to be set by the caller. +SMOKE_XVFB_PID="" +if ! xdpyinfo -display "${DISPLAY:-}" >/dev/null 2>&1; then + if command -v Xvfb >/dev/null 2>&1; then + echo " (starting Xvfb :99 for headless smoke tests)" + Xvfb :99 -screen 0 800x600x24 & + SMOKE_XVFB_PID=$! + export DISPLAY=:99 + sleep 1 + else + echo "WARNING: no DISPLAY and Xvfb not found — smoke tests may fail on xcb platform" >&2 + fi +fi +cleanup_xvfb() { [ -n "$SMOKE_XVFB_PID" ] && kill "$SMOKE_XVFB_PID" 2>/dev/null || true; } +trap cleanup_xvfb EXIT + +# T3: batch AppImage --version exits 0 with non-empty output. +echo " T3: batch --version" +T3_OUT=$(APPIMAGE_EXTRACT_AND_RUN=1 "$BATCH_OUT" --version 2>&1) || { + echo "ERROR: T3 failed — batch AppImage --version exited non-zero" >&2 + echo "$T3_OUT" >&2 + exit 1 +} +# Strip EGL/DRM warnings (libEGL warning: failed to open /dev/dri/...) which are advisory. +T3_VERSION=$(echo "$T3_OUT" | grep -v 'libEGL warning' | head -1) +if [ -z "$T3_VERSION" ]; then + echo "ERROR: T3 failed — batch AppImage --version produced no version line" >&2 + exit 1 +fi +echo " T3: PASS ($T3_VERSION)" + +# T4: GUI AppImage --help exits 0 under headless Xvfb display. +echo " T4: gui --help (DISPLAY=$DISPLAY)" +APPIMAGE_EXTRACT_AND_RUN=1 "$GUI_OUT" --help >"$BUILD_DIR/sethlabels-gui-help.txt" 2>&1 || { + echo "ERROR: T4 failed — GUI AppImage --help exited non-zero" >&2 + cat "$BUILD_DIR/sethlabels-gui-help.txt" >&2 + exit 1 +} +echo " T4: PASS" + +echo "" +echo "Artifacts:" +echo " $GUI_OUT" +echo " $BATCH_OUT"