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>
This commit is contained in:
@@ -40,6 +40,33 @@ clean: ## Remove build artefacts
|
||||
integration: ## Run the bridge-integration test (FrankenPHP boot + HTTP/SSE round-trip + crash-recover)
|
||||
./tests/integration.sh
|
||||
|
||||
.PHONY: appimage
|
||||
appimage: build ## Package as a single-file Linux AppImage at build/Todo-x86_64.AppImage
|
||||
# Composer install --no-dev in a staging copy of symfony so the
|
||||
# dev tree (with maker-bundle etc.) is left untouched.
|
||||
rm -rf build/staging-symfony
|
||||
rsync -a --delete \
|
||||
--exclude='vendor/' \
|
||||
--exclude='var/cache/' --exclude='var/log/' \
|
||||
$(SYMFONY_DIR)/ build/staging-symfony/
|
||||
# Rewrite the path repo to absolute so composer can find the bundle
|
||||
# from the staging dir (different relative depth than the source tree).
|
||||
BUNDLE_ABS="$$(cd $(SYMFONY_DIR)/../../../framework/php && pwd)"; \
|
||||
sed -i "s|\"../../../framework/php\"|\"$$BUNDLE_ABS\"|" build/staging-symfony/composer.json
|
||||
rm -f build/staging-symfony/composer.lock
|
||||
cd build/staging-symfony && composer install --no-dev --no-interaction --classmap-authoritative
|
||||
../../packaging/linux/build-appimage.sh \
|
||||
--app-name todo \
|
||||
--host-binary $(QT_BIN) \
|
||||
--symfony-dir build/staging-symfony \
|
||||
--frankenphp $${FRANKENPHP:-frankenphp} \
|
||||
--caddyfile Caddyfile \
|
||||
--desktop packaging/todo.desktop \
|
||||
--icon packaging/todo.png \
|
||||
--output build/Todo-x86_64.AppImage
|
||||
@echo
|
||||
@echo "AppImage built. Test with: ./build/Todo-x86_64.AppImage"
|
||||
|
||||
.PHONY: quality
|
||||
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint, integration
|
||||
cd ../../framework/php && composer quality
|
||||
|
||||
8
examples/todo/packaging/todo.desktop
Normal file
8
examples/todo/packaging/todo.desktop
Normal file
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=php-qml Todo
|
||||
Comment=php-qml POC todo app
|
||||
Exec=todo
|
||||
Icon=todo
|
||||
Categories=Utility;
|
||||
Terminal=false
|
||||
BIN
examples/todo/packaging/todo.png
Normal file
BIN
examples/todo/packaging/todo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 209 B |
2
packaging/linux/.gitignore
vendored
Normal file
2
packaging/linux/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
tools/
|
||||
*.AppImage
|
||||
181
packaging/linux/build-appimage.sh
Executable file
181
packaging/linux/build-appimage.sh
Executable file
@@ -0,0 +1,181 @@
|
||||
#!/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=""
|
||||
|
||||
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 ;;
|
||||
-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
|
||||
# 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"
|
||||
install -m 0755 "$HOST_BIN" "$APPDIR/usr/bin/$APP_NAME"
|
||||
install -m 0755 "$FRANKENPHP" "$APPDIR/usr/bin/frankenphp"
|
||||
install -m 0644 "$CADDYFILE" "$APPDIR/usr/share/$APP_NAME/Caddyfile"
|
||||
|
||||
# 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"
|
||||
"$APPIMAGETOOL" --runtime-file "$RUNTIME_FILE" "$APPDIR" "$OUTPUT"
|
||||
|
||||
echo
|
||||
echo "✓ AppImage at $OUTPUT"
|
||||
ls -la "$OUTPUT"
|
||||
Reference in New Issue
Block a user