diff --git a/PLAN.md b/PLAN.md index bd3f41d..993903e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -751,6 +751,59 @@ Phase 3 turns the framework from "the smallest reactive resource" into "a real a - Provision the macOS self-hosted runner before this phase starts — it gates the macOS build. - Document the build pipeline and the runner topology. +#### Phase 4 detailed scope + +Phase 4 is genuinely big — bundled-mode startup is a host-architecture change, and the per-OS packaging trifecta carries operational dependencies (Apple Developer cert + notarization for macOS, Authenticode + a Windows runner for Windows) that can't be solved from a Linux dev machine. **Phase 4 is split into three sub-phases — only 4a (Linux) ships now**; 4b (macOS) and 4c (Windows) wait until their runners and credentials exist. + +**Sub-phase split:** + +| Sub-phase | Platform | Hard prerequisites | +| --- | --- | --- | +| **4a** | Linux AppImage + bundled mode + Linux release CI + AppImageUpdate + perf-smoke harness | none (covered in this dev environment) | +| **4b** | macOS `.app` + `.dmg` + Sparkle 2 + notarization | self-hosted macOS runner, Apple Developer cert ($99/yr) | +| **4c** | Windows NSIS + WinSparkle + Authenticode | self-hosted Windows runner, code-signing cert | + +Sub-phases 4b and 4c are scoped in their own `Phase 4b` / `Phase 4c` entries in this section once their prerequisites are met. The framework code stays portable — bundled-mode plumbing in 4a is platform-agnostic, only the packaging layer is platform-specific. + +**Phase 4a sub-commits (each ends runnable):** + +1. **Bundled-mode startup in `BackendConnection`.** Mode is auto-detected: `BRIDGE_URL` env set → dev mode (today's behaviour). `BRIDGE_URL` unset → bundled mode, where the host: + - resolves the user app data dir per OS (`$XDG_DATA_HOME/php-qml-app` on Linux), + - ensures `var/data.sqlite`, `var/cache/`, `var/log/` exist there, + - generates a per-session 32-byte random secret and writes it to a child-process env var, + - spawns `bin/frankenphp` next to the host binary (overridable via `BRIDGE_FRANKENPHP_BIN`), + - waits for `/healthz`, + - on first-ever launch, runs `bin/console doctrine:migrations:migrate -n` against the user's DB before opening the SSE connection. + + The token-rotation signal already wired in §3 *Edge cases* fires when the supervisor restarts the child mid-session; subsequent commits exercise it. Skeleton + example pick up bundled mode by default with no config when run outside dev mode. +2. **AppImage recipe.** `packaging/linux/build-appimage.sh` script that produces a single-file `.AppImage`: + - `cmake --install` the host into a staging dir, + - copy the bundled `frankenphp` binary + the Symfony app tree (`composer install --no-dev`) into the AppDir, + - run `linuxdeployqt` to gather Qt runtime, + - run `appimagetool` to seal it. + + The example app gets a target `make appimage` that invokes the script with the example's bits. Hard-coded versions for `linuxdeployqt` and `appimagetool` (downloaded into a tools dir, gitignored). +3. **Linux release CI.** `.gitea/workflows/release.yml` triggered by `v*` tags. Matrix initially has only Linux (macOS/Windows added when their sub-phases land). Builds the AppImage, signs `SHA256SUMS` with the GPG release key, uploads everything to a Gitea Release. CI's `quality` workflow stays as-is. +4. **AppImageUpdate + appcast.** `latest.json` published alongside the release, describing the version + URL + sha256. The host links against `libappimageupdate` and exposes `BackendConnection.checkForUpdates()` (no-op in dev mode). User triggers manually via menu (Phase 5 will polish to a periodic check). +5. **Performance-smoke harness + phase closure.** A CI job that runs the example app's bundled binary headlessly (offscreen QPA), asserts cold-start ≤ 2 s, idle memory ≤ 200 MB, list-render ≤ 250 ms (PLAN.md §11). Numbers reported per-build. PLAN.md updated to mark 4a closed. + +**Out of scope for 4a (deferred to 4b / 4c / Phase 5):** + +- macOS `.app` bundle, codesign, notarization, Sparkle 2 integration. +- Windows NSIS, Authenticode, WinSparkle integration. +- Multi-arch (Linux ARM64 / Windows ARM) — wait for user demand. +- `make:bridge:event`, `make:bridge:read-model` — Phase 3.x. +- `qmltestrunner`-driven QML unit tests — Phase 3.x or Phase 5. + +**Done criteria for 4a:** + +- `make appimage` produces a runnable single-file `.AppImage` of the todo example. +- The AppImage launches without any `BRIDGE_URL` configured, spawns its embedded FrankenPHP, runs first-launch migrations into `~/.local/share/php-qml-todo/var/data.sqlite`, and shows the todo UI. +- Killing the bundled FrankenPHP from outside the AppImage triggers the supervisor restart in `BackendConnection`; `tokenRotated` fires; the QML side recovers. +- A `v*` tag pushes a Linux AppImage + signed `SHA256SUMS` + appcast to a Gitea Release. +- `BackendConnection.checkForUpdates()` invoked from the menu finds a newer release and updates in place. +- The performance-smoke harness reports cold-start / memory / render-time numbers within budget on every release build. + ### Phase 5 — DX polish - Project skeleton via Composer / a small CLI to scaffold a new app.