feat: add build-appimages.sh with inline smoke tests T3, T4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 11:14:58 -04:00
parent 13d4047fe1
commit d5fb872518
+161
View File
@@ -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"