Files
php-qml/packaging/linux/build-appimage.sh

199 lines
7.5 KiB
Bash
Raw Normal View History

Phase 4a sub-commit 2: AppImage recipe (build-appimage.sh + make appimage) packaging/linux/build-appimage.sh produces a single-file Linux AppImage from a built host + Symfony tree + FrankenPHP binary. Auto-downloads (cached in tools/, gitignored) the three pieces of upstream tooling: - linuxdeploy + linuxdeploy-plugin-qt — gathers Qt runtime and QML modules into the AppDir, and bundles the offscreen platform plugin via EXTRA_PLATFORM_PLUGINS so headless CI can smoke it. - appimagetool — squashes the AppDir into the .AppImage. - runtime-x86_64 — appimagetool's prepended runtime stub, fetched once and passed via --runtime-file (ad-hoc downloads stalled on some networks). The two stages are kept separate (linuxdeploy stages, then we invoke appimagetool ourselves) so failures are observable rather than swallowed by linuxdeploy's bundled-tool path. AppDir layout matches BackendConnection's resolve* fallbacks: AppDir/usr/bin/<app> AppDir/usr/bin/frankenphp AppDir/usr/share/<app>/symfony/ AppDir/usr/share/<app>/Caddyfile examples/todo gets `make appimage`: stages a no-dev composer install into build/staging-symfony, points the path repo at the bundle's absolute path so Composer can find php-qml/bridge from the staging dir, then drives build-appimage.sh. Output: build/Todo-x86_64.AppImage (~104 MB). Verified locally: `make appimage` produces a working AppImage; mount + inspect + extract all clean. Headless run requires the bundled offscreen plugin (now wired); a real desktop launches it normally. Includes a 64×64 placeholder PNG icon (todo.png) and a minimal .desktop file for the example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:42:51 +02:00
#!/usr/bin/env bash
# build-appimage.sh — package a php-qml app as a single-file Linux
# AppImage using linuxdeploy + linuxdeploy-plugin-qt.
#
# Usage:
# build-appimage.sh \
# --app-name todo \
# --host-binary build/qml/todo \
# --symfony-dir symfony \
# --frankenphp /usr/local/bin/frankenphp \
# --caddyfile Caddyfile \
# --desktop packaging/todo.desktop \
# --icon packaging/todo.png \
# --output build/Todo-x86_64.AppImage
#
# Layout produced inside the AppImage:
# AppDir/usr/bin/<app-name> — Qt host
# AppDir/usr/bin/frankenphp — bundled FrankenPHP
# AppDir/usr/share/<app-name>/symfony/ — Composer-installed app
# AppDir/usr/share/<app-name>/Caddyfile — bundled Caddy config
# AppDir/usr/lib/... — Qt + system deps (linuxdeploy)
#
# BackendConnection's resolve* methods already look in
# applicationDirPath()/../share/<app>/... so this layout works.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TOOLS_DIR="$SCRIPT_DIR/tools"
APP_NAME=""
HOST_BIN=""
SYMFONY_DIR=""
FRANKENPHP=""
CADDYFILE=""
DESKTOP=""
ICON=""
OUTPUT=""
Phase 4a sub-commit 4: AppImageUpdate sidecar + appcast + checkForUpdates() Wires in the option-(a) sidecar approach: the AppImage carries a bundled AppImageUpdate AppImage and an embedded update-info string in the .upd_info ELF section. BackendConnection drives both the check and the apply via QProcess. BackendConnection: - Q_INVOKABLE checkForUpdates() Bundled mode only. Spawns AppImageUpdate.AppImage with --check-for-update <APPIMAGE>. Exits 0 → noUpdatesAvailable, 1 → updatesAvailable, anything else → updateCheckFailed. Dev mode: emits updateCheckFailed("…dev-mode only"). - Q_INVOKABLE applyUpdate() Bundled mode only. Spawns AppImageUpdate.AppImage with --remove-old <APPIMAGE>. Replaces the running AppImage in place; user must restart. Emits updateApplied or updateApplyFailed. - Sidecar path resolves to applicationDirPath()/AppImageUpdate.AppImage by default, overridable via BRIDGE_APPIMAGEUPDATE_BIN. - APPIMAGE env (set by the AppImage runtime) determines the target file. Outside an AppImage both methods fail loudly. build-appimage.sh: - Auto-downloads AppImageUpdate-x86_64.AppImage into the cached tools dir and copies it into AppDir/usr/bin/AppImageUpdate.AppImage. - New --update-info flag, forwarded to appimagetool's -u so the .upd_info ELF section carries an "zsync|<URL>" string the sidecar will fetch. examples/todo Makefile forwards APPIMAGE_UPDATE_INFO env to the script as --update-info. release.yml: - Builds the AppImage with APPIMAGE_UPDATE_INFO set to the canonical Gitea Releases asset URL for this tag. - Installs zsync, runs zsyncmake to generate Todo-x86_64.AppImage.zsync. - Generates a JSON appcast (latest.json) with version / url / sha256 / size / zsync URL / released_at — useful as an HTTP-fetchable fallback for clients that prefer a structured manifest. - SHA256SUMS now covers AppImage + zsync + latest.json. - Uploads all four assets to the Gitea Release. AppImage size grows from ~104 MB to ~152 MB with the sidecar bundled. Embedding verified: objdump shows .upd_info populated with the expected zsync URL after a local build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:58:02 +02:00
UPDATE_INFO=""
Phase 4a sub-commit 2: AppImage recipe (build-appimage.sh + make appimage) packaging/linux/build-appimage.sh produces a single-file Linux AppImage from a built host + Symfony tree + FrankenPHP binary. Auto-downloads (cached in tools/, gitignored) the three pieces of upstream tooling: - linuxdeploy + linuxdeploy-plugin-qt — gathers Qt runtime and QML modules into the AppDir, and bundles the offscreen platform plugin via EXTRA_PLATFORM_PLUGINS so headless CI can smoke it. - appimagetool — squashes the AppDir into the .AppImage. - runtime-x86_64 — appimagetool's prepended runtime stub, fetched once and passed via --runtime-file (ad-hoc downloads stalled on some networks). The two stages are kept separate (linuxdeploy stages, then we invoke appimagetool ourselves) so failures are observable rather than swallowed by linuxdeploy's bundled-tool path. AppDir layout matches BackendConnection's resolve* fallbacks: AppDir/usr/bin/<app> AppDir/usr/bin/frankenphp AppDir/usr/share/<app>/symfony/ AppDir/usr/share/<app>/Caddyfile examples/todo gets `make appimage`: stages a no-dev composer install into build/staging-symfony, points the path repo at the bundle's absolute path so Composer can find php-qml/bridge from the staging dir, then drives build-appimage.sh. Output: build/Todo-x86_64.AppImage (~104 MB). Verified locally: `make appimage` produces a working AppImage; mount + inspect + extract all clean. Headless run requires the bundled offscreen plugin (now wired); a real desktop launches it normally. Includes a 64×64 placeholder PNG icon (todo.png) and a minimal .desktop file for the example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:42:51 +02:00
usage() {
cat <<USAGE >&2
Usage: $0 \\
--app-name <name> \\
--host-binary <path> \\
--symfony-dir <path> \\
--frankenphp <path> \\
--caddyfile <path> \\
--desktop <path> \\
--icon <path> \\
--output <path.AppImage>
USAGE
exit 1
}
while [ $# -gt 0 ]; do
case "$1" in
--app-name) APP_NAME="$2"; shift 2 ;;
--host-binary) HOST_BIN="$2"; shift 2 ;;
--symfony-dir) SYMFONY_DIR="$2"; shift 2 ;;
--frankenphp) FRANKENPHP="$2"; shift 2 ;;
--caddyfile) CADDYFILE="$2"; shift 2 ;;
--desktop) DESKTOP="$2"; shift 2 ;;
--icon) ICON="$2"; shift 2 ;;
--output) OUTPUT="$2"; shift 2 ;;
Phase 4a sub-commit 4: AppImageUpdate sidecar + appcast + checkForUpdates() Wires in the option-(a) sidecar approach: the AppImage carries a bundled AppImageUpdate AppImage and an embedded update-info string in the .upd_info ELF section. BackendConnection drives both the check and the apply via QProcess. BackendConnection: - Q_INVOKABLE checkForUpdates() Bundled mode only. Spawns AppImageUpdate.AppImage with --check-for-update <APPIMAGE>. Exits 0 → noUpdatesAvailable, 1 → updatesAvailable, anything else → updateCheckFailed. Dev mode: emits updateCheckFailed("…dev-mode only"). - Q_INVOKABLE applyUpdate() Bundled mode only. Spawns AppImageUpdate.AppImage with --remove-old <APPIMAGE>. Replaces the running AppImage in place; user must restart. Emits updateApplied or updateApplyFailed. - Sidecar path resolves to applicationDirPath()/AppImageUpdate.AppImage by default, overridable via BRIDGE_APPIMAGEUPDATE_BIN. - APPIMAGE env (set by the AppImage runtime) determines the target file. Outside an AppImage both methods fail loudly. build-appimage.sh: - Auto-downloads AppImageUpdate-x86_64.AppImage into the cached tools dir and copies it into AppDir/usr/bin/AppImageUpdate.AppImage. - New --update-info flag, forwarded to appimagetool's -u so the .upd_info ELF section carries an "zsync|<URL>" string the sidecar will fetch. examples/todo Makefile forwards APPIMAGE_UPDATE_INFO env to the script as --update-info. release.yml: - Builds the AppImage with APPIMAGE_UPDATE_INFO set to the canonical Gitea Releases asset URL for this tag. - Installs zsync, runs zsyncmake to generate Todo-x86_64.AppImage.zsync. - Generates a JSON appcast (latest.json) with version / url / sha256 / size / zsync URL / released_at — useful as an HTTP-fetchable fallback for clients that prefer a structured manifest. - SHA256SUMS now covers AppImage + zsync + latest.json. - Uploads all four assets to the Gitea Release. AppImage size grows from ~104 MB to ~152 MB with the sidecar bundled. Embedding verified: objdump shows .upd_info populated with the expected zsync URL after a local build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:58:02 +02:00
--update-info) UPDATE_INFO="$2"; shift 2 ;;
Phase 4a sub-commit 2: AppImage recipe (build-appimage.sh + make appimage) packaging/linux/build-appimage.sh produces a single-file Linux AppImage from a built host + Symfony tree + FrankenPHP binary. Auto-downloads (cached in tools/, gitignored) the three pieces of upstream tooling: - linuxdeploy + linuxdeploy-plugin-qt — gathers Qt runtime and QML modules into the AppDir, and bundles the offscreen platform plugin via EXTRA_PLATFORM_PLUGINS so headless CI can smoke it. - appimagetool — squashes the AppDir into the .AppImage. - runtime-x86_64 — appimagetool's prepended runtime stub, fetched once and passed via --runtime-file (ad-hoc downloads stalled on some networks). The two stages are kept separate (linuxdeploy stages, then we invoke appimagetool ourselves) so failures are observable rather than swallowed by linuxdeploy's bundled-tool path. AppDir layout matches BackendConnection's resolve* fallbacks: AppDir/usr/bin/<app> AppDir/usr/bin/frankenphp AppDir/usr/share/<app>/symfony/ AppDir/usr/share/<app>/Caddyfile examples/todo gets `make appimage`: stages a no-dev composer install into build/staging-symfony, points the path repo at the bundle's absolute path so Composer can find php-qml/bridge from the staging dir, then drives build-appimage.sh. Output: build/Todo-x86_64.AppImage (~104 MB). Verified locally: `make appimage` produces a working AppImage; mount + inspect + extract all clean. Headless run requires the bundled offscreen plugin (now wired); a real desktop launches it normally. Includes a 64×64 placeholder PNG icon (todo.png) and a minimal .desktop file for the example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:42:51 +02:00
-h|--help) usage ;;
*) echo "unknown arg: $1" >&2; usage ;;
esac
done
for v in APP_NAME HOST_BIN SYMFONY_DIR FRANKENPHP CADDYFILE DESKTOP ICON OUTPUT; do
[ -n "${!v}" ] || { echo "missing --${v,,}" >&2; usage; }
done
# Resolve to absolute paths so a `cd` later doesn't break references.
HOST_BIN="$(readlink -f "$HOST_BIN")"
SYMFONY_DIR="$(readlink -f "$SYMFONY_DIR")"
FRANKENPHP="$(readlink -f "$FRANKENPHP")"
CADDYFILE="$(readlink -f "$CADDYFILE")"
DESKTOP="$(readlink -f "$DESKTOP")"
ICON="$(readlink -f "$ICON")"
OUTPUT_DIR="$(dirname "$OUTPUT")"
mkdir -p "$OUTPUT_DIR"
OUTPUT="$(readlink -f "$OUTPUT_DIR")/$(basename "$OUTPUT")"
[ -x "$HOST_BIN" ] || { echo "host binary not executable: $HOST_BIN" >&2; exit 1; }
[ -d "$SYMFONY_DIR" ] || { echo "symfony dir missing: $SYMFONY_DIR" >&2; exit 1; }
[ -x "$FRANKENPHP" ] || { echo "frankenphp not executable: $FRANKENPHP" >&2; exit 1; }
[ -f "$CADDYFILE" ] || { echo "caddyfile missing: $CADDYFILE" >&2; exit 1; }
[ -f "$DESKTOP" ] || { echo "desktop file missing: $DESKTOP" >&2; exit 1; }
[ -f "$ICON" ] || { echo "icon missing: $ICON" >&2; exit 1; }
mkdir -p "$TOOLS_DIR"
step() { echo "==> $*"; }
# ── Tooling ───────────────────────────────────────────────────────────
LD="$TOOLS_DIR/linuxdeploy-x86_64.AppImage"
LD_QT="$TOOLS_DIR/linuxdeploy-plugin-qt-x86_64.AppImage"
APPIMAGETOOL="$TOOLS_DIR/appimagetool-x86_64.AppImage"
if [ ! -x "$LD" ]; then
step "Download linuxdeploy"
curl -fsSL -o "$LD" \
https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x "$LD"
fi
if [ ! -x "$LD_QT" ]; then
step "Download linuxdeploy-plugin-qt"
curl -fsSL -o "$LD_QT" \
https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
chmod +x "$LD_QT"
fi
if [ ! -x "$APPIMAGETOOL" ]; then
step "Download appimagetool"
curl -fsSL -o "$APPIMAGETOOL" \
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x "$APPIMAGETOOL"
fi
Phase 4a sub-commit 4: AppImageUpdate sidecar + appcast + checkForUpdates() Wires in the option-(a) sidecar approach: the AppImage carries a bundled AppImageUpdate AppImage and an embedded update-info string in the .upd_info ELF section. BackendConnection drives both the check and the apply via QProcess. BackendConnection: - Q_INVOKABLE checkForUpdates() Bundled mode only. Spawns AppImageUpdate.AppImage with --check-for-update <APPIMAGE>. Exits 0 → noUpdatesAvailable, 1 → updatesAvailable, anything else → updateCheckFailed. Dev mode: emits updateCheckFailed("…dev-mode only"). - Q_INVOKABLE applyUpdate() Bundled mode only. Spawns AppImageUpdate.AppImage with --remove-old <APPIMAGE>. Replaces the running AppImage in place; user must restart. Emits updateApplied or updateApplyFailed. - Sidecar path resolves to applicationDirPath()/AppImageUpdate.AppImage by default, overridable via BRIDGE_APPIMAGEUPDATE_BIN. - APPIMAGE env (set by the AppImage runtime) determines the target file. Outside an AppImage both methods fail loudly. build-appimage.sh: - Auto-downloads AppImageUpdate-x86_64.AppImage into the cached tools dir and copies it into AppDir/usr/bin/AppImageUpdate.AppImage. - New --update-info flag, forwarded to appimagetool's -u so the .upd_info ELF section carries an "zsync|<URL>" string the sidecar will fetch. examples/todo Makefile forwards APPIMAGE_UPDATE_INFO env to the script as --update-info. release.yml: - Builds the AppImage with APPIMAGE_UPDATE_INFO set to the canonical Gitea Releases asset URL for this tag. - Installs zsync, runs zsyncmake to generate Todo-x86_64.AppImage.zsync. - Generates a JSON appcast (latest.json) with version / url / sha256 / size / zsync URL / released_at — useful as an HTTP-fetchable fallback for clients that prefer a structured manifest. - SHA256SUMS now covers AppImage + zsync + latest.json. - Uploads all four assets to the Gitea Release. AppImage size grows from ~104 MB to ~152 MB with the sidecar bundled. Embedding verified: objdump shows .upd_info populated with the expected zsync URL after a local build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:58:02 +02:00
APPIMAGEUPDATE="$TOOLS_DIR/AppImageUpdate-x86_64.AppImage"
if [ ! -x "$APPIMAGEUPDATE" ]; then
step "Download AppImageUpdate sidecar"
curl -fsSL -o "$APPIMAGEUPDATE" \
https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage
chmod +x "$APPIMAGEUPDATE"
fi
Phase 4a sub-commit 2: AppImage recipe (build-appimage.sh + make appimage) packaging/linux/build-appimage.sh produces a single-file Linux AppImage from a built host + Symfony tree + FrankenPHP binary. Auto-downloads (cached in tools/, gitignored) the three pieces of upstream tooling: - linuxdeploy + linuxdeploy-plugin-qt — gathers Qt runtime and QML modules into the AppDir, and bundles the offscreen platform plugin via EXTRA_PLATFORM_PLUGINS so headless CI can smoke it. - appimagetool — squashes the AppDir into the .AppImage. - runtime-x86_64 — appimagetool's prepended runtime stub, fetched once and passed via --runtime-file (ad-hoc downloads stalled on some networks). The two stages are kept separate (linuxdeploy stages, then we invoke appimagetool ourselves) so failures are observable rather than swallowed by linuxdeploy's bundled-tool path. AppDir layout matches BackendConnection's resolve* fallbacks: AppDir/usr/bin/<app> AppDir/usr/bin/frankenphp AppDir/usr/share/<app>/symfony/ AppDir/usr/share/<app>/Caddyfile examples/todo gets `make appimage`: stages a no-dev composer install into build/staging-symfony, points the path repo at the bundle's absolute path so Composer can find php-qml/bridge from the staging dir, then drives build-appimage.sh. Output: build/Todo-x86_64.AppImage (~104 MB). Verified locally: `make appimage` produces a working AppImage; mount + inspect + extract all clean. Headless run requires the bundled offscreen plugin (now wired); a real desktop launches it normally. Includes a 64×64 placeholder PNG icon (todo.png) and a minimal .desktop file for the example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:42:51 +02:00
# Pre-cache the runtime file appimagetool needs; downloading it on every
# build was wedging some networks during squashfs.
RUNTIME_FILE="$TOOLS_DIR/runtime-x86_64"
if [ ! -f "$RUNTIME_FILE" ]; then
step "Download AppImage runtime"
curl -fsSL -o "$RUNTIME_FILE" \
https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64
fi
# Some sandboxed CI images can't run AppImage's FUSE mount. Allow the
# user to pre-extract them and skip the FUSE machinery via env hint.
export APPIMAGE_EXTRACT_AND_RUN="${APPIMAGE_EXTRACT_AND_RUN:-1}"
# ── Stage AppDir ──────────────────────────────────────────────────────
APPDIR="$(mktemp -d)/AppDir"
trap 'rm -rf "$(dirname "$APPDIR")"' EXIT INT TERM
step "Stage AppDir at $APPDIR"
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/$APP_NAME"
Phase 4a sub-commit 4: AppImageUpdate sidecar + appcast + checkForUpdates() Wires in the option-(a) sidecar approach: the AppImage carries a bundled AppImageUpdate AppImage and an embedded update-info string in the .upd_info ELF section. BackendConnection drives both the check and the apply via QProcess. BackendConnection: - Q_INVOKABLE checkForUpdates() Bundled mode only. Spawns AppImageUpdate.AppImage with --check-for-update <APPIMAGE>. Exits 0 → noUpdatesAvailable, 1 → updatesAvailable, anything else → updateCheckFailed. Dev mode: emits updateCheckFailed("…dev-mode only"). - Q_INVOKABLE applyUpdate() Bundled mode only. Spawns AppImageUpdate.AppImage with --remove-old <APPIMAGE>. Replaces the running AppImage in place; user must restart. Emits updateApplied or updateApplyFailed. - Sidecar path resolves to applicationDirPath()/AppImageUpdate.AppImage by default, overridable via BRIDGE_APPIMAGEUPDATE_BIN. - APPIMAGE env (set by the AppImage runtime) determines the target file. Outside an AppImage both methods fail loudly. build-appimage.sh: - Auto-downloads AppImageUpdate-x86_64.AppImage into the cached tools dir and copies it into AppDir/usr/bin/AppImageUpdate.AppImage. - New --update-info flag, forwarded to appimagetool's -u so the .upd_info ELF section carries an "zsync|<URL>" string the sidecar will fetch. examples/todo Makefile forwards APPIMAGE_UPDATE_INFO env to the script as --update-info. release.yml: - Builds the AppImage with APPIMAGE_UPDATE_INFO set to the canonical Gitea Releases asset URL for this tag. - Installs zsync, runs zsyncmake to generate Todo-x86_64.AppImage.zsync. - Generates a JSON appcast (latest.json) with version / url / sha256 / size / zsync URL / released_at — useful as an HTTP-fetchable fallback for clients that prefer a structured manifest. - SHA256SUMS now covers AppImage + zsync + latest.json. - Uploads all four assets to the Gitea Release. AppImage size grows from ~104 MB to ~152 MB with the sidecar bundled. Embedding verified: objdump shows .upd_info populated with the expected zsync URL after a local build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:58:02 +02:00
install -m 0755 "$HOST_BIN" "$APPDIR/usr/bin/$APP_NAME"
install -m 0755 "$FRANKENPHP" "$APPDIR/usr/bin/frankenphp"
install -m 0755 "$APPIMAGEUPDATE" "$APPDIR/usr/bin/AppImageUpdate.AppImage"
install -m 0644 "$CADDYFILE" "$APPDIR/usr/share/$APP_NAME/Caddyfile"
Phase 4a sub-commit 2: AppImage recipe (build-appimage.sh + make appimage) packaging/linux/build-appimage.sh produces a single-file Linux AppImage from a built host + Symfony tree + FrankenPHP binary. Auto-downloads (cached in tools/, gitignored) the three pieces of upstream tooling: - linuxdeploy + linuxdeploy-plugin-qt — gathers Qt runtime and QML modules into the AppDir, and bundles the offscreen platform plugin via EXTRA_PLATFORM_PLUGINS so headless CI can smoke it. - appimagetool — squashes the AppDir into the .AppImage. - runtime-x86_64 — appimagetool's prepended runtime stub, fetched once and passed via --runtime-file (ad-hoc downloads stalled on some networks). The two stages are kept separate (linuxdeploy stages, then we invoke appimagetool ourselves) so failures are observable rather than swallowed by linuxdeploy's bundled-tool path. AppDir layout matches BackendConnection's resolve* fallbacks: AppDir/usr/bin/<app> AppDir/usr/bin/frankenphp AppDir/usr/share/<app>/symfony/ AppDir/usr/share/<app>/Caddyfile examples/todo gets `make appimage`: stages a no-dev composer install into build/staging-symfony, points the path repo at the bundle's absolute path so Composer can find php-qml/bridge from the staging dir, then drives build-appimage.sh. Output: build/Todo-x86_64.AppImage (~104 MB). Verified locally: `make appimage` produces a working AppImage; mount + inspect + extract all clean. Headless run requires the bundled offscreen plugin (now wired); a real desktop launches it normally. Includes a 64×64 placeholder PNG icon (todo.png) and a minimal .desktop file for the example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:42:51 +02:00
# Symfony tree: rsync without dev artefacts. Ensure composer ran with
# --no-dev before building; this script doesn't re-run composer install.
rsync -a --delete \
--exclude='var/cache' \
--exclude='var/log' \
--exclude='var/data.sqlite*' \
--exclude='vendor/symfony/maker-bundle' \
"$SYMFONY_DIR/" "$APPDIR/usr/share/$APP_NAME/symfony/"
# ── Run linuxdeploy + Qt plugin ───────────────────────────────────────
# linuxdeploy refuses to run as root; not an issue under normal users.
step "linuxdeploy --plugin qt --output appimage"
cd "$(dirname "$APPDIR")"
# linuxdeploy reads QML_SOURCES_PATHS to find QML modules to bundle.
# Point it at the example's QML so its plugin gets picked up.
QML_SOURCES_PATHS=""
for d in "$SYMFONY_DIR/../qml" "$SYMFONY_DIR/../../qml"; do
[ -d "$d" ] && QML_SOURCES_PATHS="$d"
done
# Stage the AppDir + Qt deps first; then call appimagetool ourselves
# so the slow squashfs step is observable (linuxdeploy's bundled
# appimagetool download path was opaque on first run).
QML_SOURCES_PATHS="$QML_SOURCES_PATHS" \
EXTRA_PLATFORM_PLUGINS=libqoffscreen.so \
LD_LIBRARY_PATH="" \
"$LD" \
--appdir "$APPDIR" \
--executable "$APPDIR/usr/bin/$APP_NAME" \
--desktop-file "$DESKTOP" \
--icon-file "$ICON" \
--plugin qt
step "appimagetool — squash AppDir into single-file AppImage"
Phase 4a sub-commit 4: AppImageUpdate sidecar + appcast + checkForUpdates() Wires in the option-(a) sidecar approach: the AppImage carries a bundled AppImageUpdate AppImage and an embedded update-info string in the .upd_info ELF section. BackendConnection drives both the check and the apply via QProcess. BackendConnection: - Q_INVOKABLE checkForUpdates() Bundled mode only. Spawns AppImageUpdate.AppImage with --check-for-update <APPIMAGE>. Exits 0 → noUpdatesAvailable, 1 → updatesAvailable, anything else → updateCheckFailed. Dev mode: emits updateCheckFailed("…dev-mode only"). - Q_INVOKABLE applyUpdate() Bundled mode only. Spawns AppImageUpdate.AppImage with --remove-old <APPIMAGE>. Replaces the running AppImage in place; user must restart. Emits updateApplied or updateApplyFailed. - Sidecar path resolves to applicationDirPath()/AppImageUpdate.AppImage by default, overridable via BRIDGE_APPIMAGEUPDATE_BIN. - APPIMAGE env (set by the AppImage runtime) determines the target file. Outside an AppImage both methods fail loudly. build-appimage.sh: - Auto-downloads AppImageUpdate-x86_64.AppImage into the cached tools dir and copies it into AppDir/usr/bin/AppImageUpdate.AppImage. - New --update-info flag, forwarded to appimagetool's -u so the .upd_info ELF section carries an "zsync|<URL>" string the sidecar will fetch. examples/todo Makefile forwards APPIMAGE_UPDATE_INFO env to the script as --update-info. release.yml: - Builds the AppImage with APPIMAGE_UPDATE_INFO set to the canonical Gitea Releases asset URL for this tag. - Installs zsync, runs zsyncmake to generate Todo-x86_64.AppImage.zsync. - Generates a JSON appcast (latest.json) with version / url / sha256 / size / zsync URL / released_at — useful as an HTTP-fetchable fallback for clients that prefer a structured manifest. - SHA256SUMS now covers AppImage + zsync + latest.json. - Uploads all four assets to the Gitea Release. AppImage size grows from ~104 MB to ~152 MB with the sidecar bundled. Embedding verified: objdump shows .upd_info populated with the expected zsync URL after a local build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:58:02 +02:00
APPIMAGETOOL_ARGS=(--runtime-file "$RUNTIME_FILE")
if [ -n "$UPDATE_INFO" ]; then
# Embed AppImageUpdate update-info string (e.g.
# "zsync|https://example.com/Todo-x86_64.AppImage.zsync") into the
# .upd_info ELF section so the bundled sidecar can find new releases.
APPIMAGETOOL_ARGS+=(-u "$UPDATE_INFO")
fi
"$APPIMAGETOOL" "${APPIMAGETOOL_ARGS[@]}" "$APPDIR" "$OUTPUT"
Phase 4a sub-commit 2: AppImage recipe (build-appimage.sh + make appimage) packaging/linux/build-appimage.sh produces a single-file Linux AppImage from a built host + Symfony tree + FrankenPHP binary. Auto-downloads (cached in tools/, gitignored) the three pieces of upstream tooling: - linuxdeploy + linuxdeploy-plugin-qt — gathers Qt runtime and QML modules into the AppDir, and bundles the offscreen platform plugin via EXTRA_PLATFORM_PLUGINS so headless CI can smoke it. - appimagetool — squashes the AppDir into the .AppImage. - runtime-x86_64 — appimagetool's prepended runtime stub, fetched once and passed via --runtime-file (ad-hoc downloads stalled on some networks). The two stages are kept separate (linuxdeploy stages, then we invoke appimagetool ourselves) so failures are observable rather than swallowed by linuxdeploy's bundled-tool path. AppDir layout matches BackendConnection's resolve* fallbacks: AppDir/usr/bin/<app> AppDir/usr/bin/frankenphp AppDir/usr/share/<app>/symfony/ AppDir/usr/share/<app>/Caddyfile examples/todo gets `make appimage`: stages a no-dev composer install into build/staging-symfony, points the path repo at the bundle's absolute path so Composer can find php-qml/bridge from the staging dir, then drives build-appimage.sh. Output: build/Todo-x86_64.AppImage (~104 MB). Verified locally: `make appimage` produces a working AppImage; mount + inspect + extract all clean. Headless run requires the bundled offscreen plugin (now wired); a real desktop launches it normally. Includes a 64×64 placeholder PNG icon (todo.png) and a minimal .desktop file for the example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:42:51 +02:00
echo
echo "✓ AppImage at $OUTPUT"
ls -la "$OUTPUT"