#!/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" # Debian 13 has Qt6 only; linuxdeploy-plugin-qt's default qmake lookup looks # for `qmake` (Qt5 era) or `/usr/lib/qt5/bin/qmake` (neither exists). Force it # to the Qt6 binary explicitly so plugin staging works. 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; } # Prune the unwanted binary from each AppDir so linuxdeploy bundles only the # libs needed by the remaining binary (otherwise both AppImages carry both # binaries; spec §5.3 step 3 implies "AppDir-batch" should be batch-only). rm "$APPDIR_GUI/usr/bin/glabels-batch-qt" rm "$APPDIR_BATCH/usr/bin/glabels-qt" # Also prune the GUI desktop files from the batch AppDir: linuxdeploy's # --create-desktop-file doesn't suppress pre-existing desktop files, and # the upstream desktop files reference Exec=glabels-qt (now pruned). find "$APPDIR_BATCH/usr/share/applications" -name '*.desktop' -delete 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 is CLI-only; reuse the upstream SVG icon so --create-desktop-file has # an icon to reference (linuxdeploy errors if the Icon= entry has no match). BATCH_ICON_FILE="$APPDIR_BATCH/usr/share/icons/hicolor/scalable/apps/glabels.svg" cd "$BUILD_DIR" APPIMAGE_EXTRACT_AND_RUN=1 \ "$LINUXDEPLOY_BIN" \ --appdir "$APPDIR_BATCH" \ --plugin qt \ --executable "$APPDIR_BATCH/usr/bin/glabels-batch-qt" \ --icon-file "$BATCH_ICON_FILE" \ --icon-filename 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"