Files
php-qml/examples/todo/Makefile

97 lines
3.7 KiB
Makefile
Raw Normal View History

Phase 3 sub-commit 3: examples/todo POC app, built via the makers Standalone Composer/CMake project under examples/todo/ derived from the skeleton, demonstrating every Phase 3 architectural primitive in a non-trivial app. All cross-side wiring is maker-generated; no handwritten bridge glue. Generated and customised: - src/Entity/Todo.php — make:bridge:resource Todo (UUIDv7 id) - src/Controller/TodoController.php — make:bridge:resource Todo (CRUD) - src/Controller/MarkAllDoneController.php — make:bridge:command MarkAllDone, body filled in to flip done=true on every row - qml/TodoList.qml — make:bridge:resource Todo (starter ListView) - qml/TodoWindow.qml — make:bridge:window Todo, body customised to embed a read-only mirror of the same ReactiveListModel The Phase 1 ping demo is dropped from this app — it doesn't fit the todo flow and nothing in Main.qml references it. Main.qml is the real list UI: - Add input + button (POST /api/todos with optimistic-friendly key). - Per-row CheckBox + delete button (PATCH/DELETE via todoModel.invoke() with `pending` role driving opacity). - "Mark all done" button (POST /api/mark-all-done). - "Open second window" button (Component { TodoWindow {} } pattern). Build / run delegated to the same Makefile shape as the skeleton, with SCRIPT_DIR/QT_BIN updated for the renamed binary (build/qml/todo). composer.json's path repo points at ../../../framework/php (one level deeper than the skeleton's path repo). Verified end-to-end with offscreen QPA: POST/PATCH/DELETE on /api/todos all round-trip, /api/mark-all-done flips every row, Mercure dual- publishes on every change. Clean shutdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:22:36 +02:00
# php-qml — Todo example app — Make targets.
.DEFAULT_GOAL := help
SYMFONY_DIR := symfony
QML_SRC_DIR := qml
BUILD_DIR := build/qml
QT_BIN := $(BUILD_DIR)/todo
.PHONY: help
help: ## Show available targets
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-12s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.PHONY: install
install: ## Install Composer dependencies for the Symfony app
cd $(SYMFONY_DIR) && composer install
.PHONY: build
build: ## Build the Qt host
cmake -S $(QML_SRC_DIR) -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Debug
cmake --build $(BUILD_DIR) --parallel
.PHONY: dev
dev: build ## Run the todo app in dev mode (FrankenPHP --watch + Qt host)
./scripts/dev.sh
.PHONY: doctor
doctor: ## Run bridge:doctor inside the Symfony app
cd $(SYMFONY_DIR) && bin/console bridge:doctor
.PHONY: doctor-connect
doctor-connect: ## Run bridge:doctor with backend connectivity probe
cd $(SYMFONY_DIR) && bin/console bridge:doctor --connect
.PHONY: clean
clean: ## Remove build artefacts
rm -rf $(BUILD_DIR)
.PHONY: integration
integration: ## Run the bridge-integration test (FrankenPHP boot + HTTP/SSE round-trip + crash-recover)
./tests/integration.sh
test: bundled-mode supervisor integration test (faked AppImage layout) Stages a fake AppImage layout in /tmp without a real .AppImage build: $ROOT/usr/bin/<app> — copy of the host binary $ROOT/usr/bin/frankenphp — symlink to system frankenphp $ROOT/usr/share/<app>/symfony — staged --no-dev composer copy $ROOT/usr/share/<app>/Caddyfile The staged Symfony tree is `chmod -R a-w` to actually exercise the read-only-mount cache/log redirect (Kernel::getCacheDir + APP_CACHE_DIR override) — without the override, Symfony would fail to mkdir var/cache/prod and migrations would error out. Then runs the host with BRIDGE_URL unset (forces bundled mode), polls /healthz, and asserts: - status=ok + bundle="PhpQml\Bridge\Publisher" — proves the HealthController deep-load (predecessor commit) actually autowired Publisher, i.e. BridgeBundle is reachable. - User data dir's var/cache exists — APP_CACHE_DIR override fired. - Staged tree's var/cache/prod is empty — Symfony didn't write into the read-only mount. Together this catches every v0.1.0 shakedown bug in CI: - doubled bin/frankenphp path (resolveFrankenphpBin) - composer path-repo symlink dangling (staging-symfony's symlink:false sed) - read-only mount cache failure (Kernel + supervisor env-vars) - bundle autoload broken (HealthController canary) Implementation gotcha (caught during dev): the host binary must be COPIED into the staged layout, not symlinked. Qt's applicationDirPath() reads /proc/self/exe which dereferences symlinks, so a symlinked host would resolve to the original build/ dir and the supervisor would hunt for frankenphp + symfony there instead of the staged tree. Real AppImages copy the binary, mimicking that here. Wiring: - examples/todo/Makefile: extracted the staging-symfony logic out of the appimage target into its own staging-symfony target. New integration-bundled target depends on `build` + `staging-symfony` and runs tests/bundled-supervisor.sh. quality target now invokes integration-bundled after the existing dev-mode integration test. - .gitea/workflows/ci.yml: new "Bundled-mode supervisor integration test" step right after the dev-mode integration step. Closes the v0.1.1 follow-up "Bundled-mode integration test" tracked in PLAN.md §13. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:36:21 +02:00
.PHONY: integration-bundled
integration-bundled: build staging-symfony ## Bundled-mode integration test (faked AppImage layout, no .AppImage build needed)
./tests/bundled-supervisor.sh
.PHONY: perf
perf: ## Run the AppImage perf smoke test (PLAN.md §11 budgets)
./tests/perfsmoke.sh build/Todo-x86_64.AppImage
test: bundled-mode supervisor integration test (faked AppImage layout) Stages a fake AppImage layout in /tmp without a real .AppImage build: $ROOT/usr/bin/<app> — copy of the host binary $ROOT/usr/bin/frankenphp — symlink to system frankenphp $ROOT/usr/share/<app>/symfony — staged --no-dev composer copy $ROOT/usr/share/<app>/Caddyfile The staged Symfony tree is `chmod -R a-w` to actually exercise the read-only-mount cache/log redirect (Kernel::getCacheDir + APP_CACHE_DIR override) — without the override, Symfony would fail to mkdir var/cache/prod and migrations would error out. Then runs the host with BRIDGE_URL unset (forces bundled mode), polls /healthz, and asserts: - status=ok + bundle="PhpQml\Bridge\Publisher" — proves the HealthController deep-load (predecessor commit) actually autowired Publisher, i.e. BridgeBundle is reachable. - User data dir's var/cache exists — APP_CACHE_DIR override fired. - Staged tree's var/cache/prod is empty — Symfony didn't write into the read-only mount. Together this catches every v0.1.0 shakedown bug in CI: - doubled bin/frankenphp path (resolveFrankenphpBin) - composer path-repo symlink dangling (staging-symfony's symlink:false sed) - read-only mount cache failure (Kernel + supervisor env-vars) - bundle autoload broken (HealthController canary) Implementation gotcha (caught during dev): the host binary must be COPIED into the staged layout, not symlinked. Qt's applicationDirPath() reads /proc/self/exe which dereferences symlinks, so a symlinked host would resolve to the original build/ dir and the supervisor would hunt for frankenphp + symfony there instead of the staged tree. Real AppImages copy the binary, mimicking that here. Wiring: - examples/todo/Makefile: extracted the staging-symfony logic out of the appimage target into its own staging-symfony target. New integration-bundled target depends on `build` + `staging-symfony` and runs tests/bundled-supervisor.sh. quality target now invokes integration-bundled after the existing dev-mode integration test. - .gitea/workflows/ci.yml: new "Bundled-mode supervisor integration test" step right after the dev-mode integration step. Closes the v0.1.1 follow-up "Bundled-mode integration test" tracked in PLAN.md §13. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:36:21 +02:00
.PHONY: staging-symfony
staging-symfony: ## Stage a --no-dev composer copy of symfony for AppImage / bundled-mode tests
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
# 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
appimage: copy the path-repo bundle into vendor/ instead of symlinking The Makefile's appimage target ran composer install with the path repo configured as `"symlink": true`. Composer created a symlink at vendor/php-qml/bridge → <BUNDLE_ABS>. rsync into the AppDir preserved the symlink, whose target path doesn't exist on the user's machine. At runtime: Caddy + frankenphp boot fine, /healthz returns 200 (no bundle services touched), but every API request fails with: Warning: include(.../symfony/vendor/composer/../php-qml/bridge/src/ BridgeBundle.php): Failed to open stream: No such file or directory …and the migrations step fails identically on first launch. COMPOSER_MIRROR_PATH_REPOS=1 is the documented env-var lever, but explicit `"symlink": true` in composer.json takes precedence over it (verified the env var alone leaves the symlink in place). Dropping the env var; instead, sed the symlink option to `false` in the staging composer.json, alongside the existing URL rewrite. Composer.json source-of-truth keeps `symlink: true` so dev-mode installs are still hot-reloadable against framework/php source. Only the staging copy used for AppImage assembly is mirrored. Verified locally: `vendor/php-qml/bridge` is now a real directory after composer install; `BridgeBundle.php` exists as a regular file. Note for follow-up (out of scope here): perfsmoke didn't catch this because /healthz doesn't touch any BridgeBundle services. Worth extending perfsmoke to also exercise an actual API endpoint so packaging regressions of this shape fail loudly in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:01:57 +02:00
# from the staging dir, AND flip symlink:true → false so composer copies
# the bundle into vendor/ — symlinks would survive the rsync into the
# AppDir but their targets wouldn't exist on the user's machine.
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
BUNDLE_ABS="$$(cd $(SYMFONY_DIR)/../../../framework/php && pwd)"; \
sed -i "s|\"../../../framework/php\"|\"$$BUNDLE_ABS\"|" build/staging-symfony/composer.json
appimage: copy the path-repo bundle into vendor/ instead of symlinking The Makefile's appimage target ran composer install with the path repo configured as `"symlink": true`. Composer created a symlink at vendor/php-qml/bridge → <BUNDLE_ABS>. rsync into the AppDir preserved the symlink, whose target path doesn't exist on the user's machine. At runtime: Caddy + frankenphp boot fine, /healthz returns 200 (no bundle services touched), but every API request fails with: Warning: include(.../symfony/vendor/composer/../php-qml/bridge/src/ BridgeBundle.php): Failed to open stream: No such file or directory …and the migrations step fails identically on first launch. COMPOSER_MIRROR_PATH_REPOS=1 is the documented env-var lever, but explicit `"symlink": true` in composer.json takes precedence over it (verified the env var alone leaves the symlink in place). Dropping the env var; instead, sed the symlink option to `false` in the staging composer.json, alongside the existing URL rewrite. Composer.json source-of-truth keeps `symlink: true` so dev-mode installs are still hot-reloadable against framework/php source. Only the staging copy used for AppImage assembly is mirrored. Verified locally: `vendor/php-qml/bridge` is now a real directory after composer install; `BridgeBundle.php` exists as a regular file. Note for follow-up (out of scope here): perfsmoke didn't catch this because /healthz doesn't touch any BridgeBundle services. Worth extending perfsmoke to also exercise an actual API endpoint so packaging regressions of this shape fail loudly in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:01:57 +02:00
sed -i 's|"symlink": true|"symlink": false|' build/staging-symfony/composer.json
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
rm -f build/staging-symfony/composer.lock
cd build/staging-symfony && composer install --no-dev --no-interaction --classmap-authoritative
test: bundled-mode supervisor integration test (faked AppImage layout) Stages a fake AppImage layout in /tmp without a real .AppImage build: $ROOT/usr/bin/<app> — copy of the host binary $ROOT/usr/bin/frankenphp — symlink to system frankenphp $ROOT/usr/share/<app>/symfony — staged --no-dev composer copy $ROOT/usr/share/<app>/Caddyfile The staged Symfony tree is `chmod -R a-w` to actually exercise the read-only-mount cache/log redirect (Kernel::getCacheDir + APP_CACHE_DIR override) — without the override, Symfony would fail to mkdir var/cache/prod and migrations would error out. Then runs the host with BRIDGE_URL unset (forces bundled mode), polls /healthz, and asserts: - status=ok + bundle="PhpQml\Bridge\Publisher" — proves the HealthController deep-load (predecessor commit) actually autowired Publisher, i.e. BridgeBundle is reachable. - User data dir's var/cache exists — APP_CACHE_DIR override fired. - Staged tree's var/cache/prod is empty — Symfony didn't write into the read-only mount. Together this catches every v0.1.0 shakedown bug in CI: - doubled bin/frankenphp path (resolveFrankenphpBin) - composer path-repo symlink dangling (staging-symfony's symlink:false sed) - read-only mount cache failure (Kernel + supervisor env-vars) - bundle autoload broken (HealthController canary) Implementation gotcha (caught during dev): the host binary must be COPIED into the staged layout, not symlinked. Qt's applicationDirPath() reads /proc/self/exe which dereferences symlinks, so a symlinked host would resolve to the original build/ dir and the supervisor would hunt for frankenphp + symfony there instead of the staged tree. Real AppImages copy the binary, mimicking that here. Wiring: - examples/todo/Makefile: extracted the staging-symfony logic out of the appimage target into its own staging-symfony target. New integration-bundled target depends on `build` + `staging-symfony` and runs tests/bundled-supervisor.sh. quality target now invokes integration-bundled after the existing dev-mode integration test. - .gitea/workflows/ci.yml: new "Bundled-mode supervisor integration test" step right after the dev-mode integration step. Closes the v0.1.1 follow-up "Bundled-mode integration test" tracked in PLAN.md §13. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:36:21 +02:00
.PHONY: appimage
appimage: build staging-symfony ## Package as a single-file Linux AppImage at build/Todo-x86_64.AppImage
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
../../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 \
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
--output build/Todo-x86_64.AppImage \
$${APPIMAGE_UPDATE_INFO:+--update-info "$$APPIMAGE_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
@echo
@echo "AppImage built. Test with: ./build/Todo-x86_64.AppImage"
Phase 3 sub-commit 3: examples/todo POC app, built via the makers Standalone Composer/CMake project under examples/todo/ derived from the skeleton, demonstrating every Phase 3 architectural primitive in a non-trivial app. All cross-side wiring is maker-generated; no handwritten bridge glue. Generated and customised: - src/Entity/Todo.php — make:bridge:resource Todo (UUIDv7 id) - src/Controller/TodoController.php — make:bridge:resource Todo (CRUD) - src/Controller/MarkAllDoneController.php — make:bridge:command MarkAllDone, body filled in to flip done=true on every row - qml/TodoList.qml — make:bridge:resource Todo (starter ListView) - qml/TodoWindow.qml — make:bridge:window Todo, body customised to embed a read-only mirror of the same ReactiveListModel The Phase 1 ping demo is dropped from this app — it doesn't fit the todo flow and nothing in Main.qml references it. Main.qml is the real list UI: - Add input + button (POST /api/todos with optimistic-friendly key). - Per-row CheckBox + delete button (PATCH/DELETE via todoModel.invoke() with `pending` role driving opacity). - "Mark all done" button (POST /api/mark-all-done). - "Open second window" button (Component { TodoWindow {} } pattern). Build / run delegated to the same Makefile shape as the skeleton, with SCRIPT_DIR/QT_BIN updated for the renamed binary (build/qml/todo). composer.json's path repo points at ../../../framework/php (one level deeper than the skeleton's path repo). Verified end-to-end with offscreen QPA: POST/PATCH/DELETE on /api/todos all round-trip, /api/mark-all-done flips every row, Mercure dual- publishes on every change. Clean shutdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:22:36 +02:00
.PHONY: quality
v0.2.0 (13/N): qmltestrunner harness + CI wiring + close out v0.2.0 plan Closes the testing-strategy row of PLAN.md §13 v0.2.0 and parks the two remaining items with rationales. Shipped: - framework/qml/tests/{CMakeLists.txt, main.cpp, tst_smoke.qml} Qt Quick Test scaffold: QUICK_TEST_MAIN bootstrap + one smoke test proving the harness loads. New tests land as tst_<feature>.qml in the same dir; qmltestrunner auto-discovers them. Built only when -DBUILD_TESTING=ON (production AppImages stay clean). - skeleton + example/todo Makefiles: `make qmltest` target invokes the configure → build → ctest dance. `make quality` now depends on qmltest. - .gitea/workflows/ci.yml: `QML unit tests` step after qmllint in the Quality job. Out-of-tree build dir (build-tests) so the CTest run doesn't pollute the cached release build. Verified locally: configure + build + ctest pass, both smoke assertions pass, runs in 0.5s. Closed in PLAN.md §13 v0.2.0 with rationale (no code change): - Build-time Symfony cache warmup → moved to v0.3.0. The obvious approach (cache:warmup at build, copy at first launch) doesn't save any time because Symfony bakes absolute kernel.project_dir into the compiled cache, and the AppImage's FUSE mount path changes every launch — every cached path is stale on launch N+1. Doing it properly requires virtualising getProjectDir(), symlink fix-up, multi-app namespacing — its own minor's worth of design. - ReactiveObject cursor pagination → closed N/A. ReactiveObject already has pending / invoke() / Idempotency-Key correlation / version-gap detection at parity with ReactiveListModel; the only feature it lacks is *pagination*, which is meaningless for a single-entity model. That fully closes the v0.2.0 plan as documented. Remaining v0.2.0 items in PLAN.md §13 are the audit-ends already shipped earlier in the cycle (interfaces / BridgeOp / BridgeBundleInfo / Maker DRY / --with-dto / port negotiation / pre-migration backup / bridge:export / periodic auto-update / native-dialogs doc / event maker / read-model maker / qmltestrunner) plus the two parked items documented above. Ready to tag when the user gives the word. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:02:30 +02:00
quality: build qmltest ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint, qmltest, integration (dev + bundled)
cd ../../framework/php && composer quality
Phase 3 sub-commit 3: examples/todo POC app, built via the makers Standalone Composer/CMake project under examples/todo/ derived from the skeleton, demonstrating every Phase 3 architectural primitive in a non-trivial app. All cross-side wiring is maker-generated; no handwritten bridge glue. Generated and customised: - src/Entity/Todo.php — make:bridge:resource Todo (UUIDv7 id) - src/Controller/TodoController.php — make:bridge:resource Todo (CRUD) - src/Controller/MarkAllDoneController.php — make:bridge:command MarkAllDone, body filled in to flip done=true on every row - qml/TodoList.qml — make:bridge:resource Todo (starter ListView) - qml/TodoWindow.qml — make:bridge:window Todo, body customised to embed a read-only mirror of the same ReactiveListModel The Phase 1 ping demo is dropped from this app — it doesn't fit the todo flow and nothing in Main.qml references it. Main.qml is the real list UI: - Add input + button (POST /api/todos with optimistic-friendly key). - Per-row CheckBox + delete button (PATCH/DELETE via todoModel.invoke() with `pending` role driving opacity). - "Mark all done" button (POST /api/mark-all-done). - "Open second window" button (Component { TodoWindow {} } pattern). Build / run delegated to the same Makefile shape as the skeleton, with SCRIPT_DIR/QT_BIN updated for the renamed binary (build/qml/todo). composer.json's path repo points at ../../../framework/php (one level deeper than the skeleton's path repo). Verified end-to-end with offscreen QPA: POST/PATCH/DELETE on /api/todos all round-trip, /api/mark-all-done flips every row, Mercure dual- publishes on every change. Clean shutdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:22:36 +02:00
cmake --build $(BUILD_DIR) --target all_qmllint
./tests/integration.sh
test: bundled-mode supervisor integration test (faked AppImage layout) Stages a fake AppImage layout in /tmp without a real .AppImage build: $ROOT/usr/bin/<app> — copy of the host binary $ROOT/usr/bin/frankenphp — symlink to system frankenphp $ROOT/usr/share/<app>/symfony — staged --no-dev composer copy $ROOT/usr/share/<app>/Caddyfile The staged Symfony tree is `chmod -R a-w` to actually exercise the read-only-mount cache/log redirect (Kernel::getCacheDir + APP_CACHE_DIR override) — without the override, Symfony would fail to mkdir var/cache/prod and migrations would error out. Then runs the host with BRIDGE_URL unset (forces bundled mode), polls /healthz, and asserts: - status=ok + bundle="PhpQml\Bridge\Publisher" — proves the HealthController deep-load (predecessor commit) actually autowired Publisher, i.e. BridgeBundle is reachable. - User data dir's var/cache exists — APP_CACHE_DIR override fired. - Staged tree's var/cache/prod is empty — Symfony didn't write into the read-only mount. Together this catches every v0.1.0 shakedown bug in CI: - doubled bin/frankenphp path (resolveFrankenphpBin) - composer path-repo symlink dangling (staging-symfony's symlink:false sed) - read-only mount cache failure (Kernel + supervisor env-vars) - bundle autoload broken (HealthController canary) Implementation gotcha (caught during dev): the host binary must be COPIED into the staged layout, not symlinked. Qt's applicationDirPath() reads /proc/self/exe which dereferences symlinks, so a symlinked host would resolve to the original build/ dir and the supervisor would hunt for frankenphp + symfony there instead of the staged tree. Real AppImages copy the binary, mimicking that here. Wiring: - examples/todo/Makefile: extracted the staging-symfony logic out of the appimage target into its own staging-symfony target. New integration-bundled target depends on `build` + `staging-symfony` and runs tests/bundled-supervisor.sh. quality target now invokes integration-bundled after the existing dev-mode integration test. - .gitea/workflows/ci.yml: new "Bundled-mode supervisor integration test" step right after the dev-mode integration step. Closes the v0.1.1 follow-up "Bundled-mode integration test" tracked in PLAN.md §13. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:36:21 +02:00
$(MAKE) integration-bundled
v0.2.0 (13/N): qmltestrunner harness + CI wiring + close out v0.2.0 plan Closes the testing-strategy row of PLAN.md §13 v0.2.0 and parks the two remaining items with rationales. Shipped: - framework/qml/tests/{CMakeLists.txt, main.cpp, tst_smoke.qml} Qt Quick Test scaffold: QUICK_TEST_MAIN bootstrap + one smoke test proving the harness loads. New tests land as tst_<feature>.qml in the same dir; qmltestrunner auto-discovers them. Built only when -DBUILD_TESTING=ON (production AppImages stay clean). - skeleton + example/todo Makefiles: `make qmltest` target invokes the configure → build → ctest dance. `make quality` now depends on qmltest. - .gitea/workflows/ci.yml: `QML unit tests` step after qmllint in the Quality job. Out-of-tree build dir (build-tests) so the CTest run doesn't pollute the cached release build. Verified locally: configure + build + ctest pass, both smoke assertions pass, runs in 0.5s. Closed in PLAN.md §13 v0.2.0 with rationale (no code change): - Build-time Symfony cache warmup → moved to v0.3.0. The obvious approach (cache:warmup at build, copy at first launch) doesn't save any time because Symfony bakes absolute kernel.project_dir into the compiled cache, and the AppImage's FUSE mount path changes every launch — every cached path is stale on launch N+1. Doing it properly requires virtualising getProjectDir(), symlink fix-up, multi-app namespacing — its own minor's worth of design. - ReactiveObject cursor pagination → closed N/A. ReactiveObject already has pending / invoke() / Idempotency-Key correlation / version-gap detection at parity with ReactiveListModel; the only feature it lacks is *pagination*, which is meaningless for a single-entity model. That fully closes the v0.2.0 plan as documented. Remaining v0.2.0 items in PLAN.md §13 are the audit-ends already shipped earlier in the cycle (interfaces / BridgeOp / BridgeBundleInfo / Maker DRY / --with-dto / port negotiation / pre-migration backup / bridge:export / periodic auto-update / native-dialogs doc / event maker / read-model maker / qmltestrunner) plus the two parked items documented above. Ready to tag when the user gives the word. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:02:30 +02:00
.PHONY: qmltest
qmltest: ## Run QML unit tests (Qt::QuickTest via qmltestrunner)
cmake -S ../../framework/qml -B ../../framework/qml/build-tests -DBUILD_TESTING=ON
cmake --build ../../framework/qml/build-tests --target qml_unit_tests --parallel
QT_QPA_PLATFORM=offscreen ctest --test-dir ../../framework/qml/build-tests --output-on-failure -R qml_unit_tests