Compare commits
7
Commits
be3fecf64e
..
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597e74edcf | ||
|
|
1c231b1bac | ||
|
|
06b2289ed3 | ||
|
|
341bcacafe | ||
|
|
813b064cc1 | ||
|
|
7e734fec66 | ||
|
|
3c027255c8 |
No files matched your search
@@ -95,3 +95,7 @@ jobs:
|
|||||||
- name: Bridge-integration test (HTTP/SSE round-trip + crash-recover)
|
- name: Bridge-integration test (HTTP/SSE round-trip + crash-recover)
|
||||||
working-directory: examples/todo
|
working-directory: examples/todo
|
||||||
run: ./tests/integration.sh
|
run: ./tests/integration.sh
|
||||||
|
|
||||||
|
- name: Bundled-mode supervisor integration test
|
||||||
|
working-directory: examples/todo
|
||||||
|
run: make integration-bundled
|
||||||
+21
-1
@@ -10,6 +10,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|||||||
|
|
||||||
- (none yet — next changes land here)
|
- (none yet — next changes land here)
|
||||||
|
|
||||||
|
## [0.1.1] — 2026-05-03
|
||||||
|
|
||||||
|
Bugfix release closing the four follow-ups identified during the v0.1.0 shakedown. No new public API surface; `/healthz` response gains an additive `bundle` field (existing JSON consumers ignore unknown keys).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Wipe Symfony cache on bundled-mode launch.** Symfony's compiled container bakes `kernel.project_dir` as an absolute path. In bundled mode that path lives inside the AppImage's FUSE mount (`/tmp/.mount_<random>`), which is regenerated every launch. So the cache from launch N referenced mount-N's path; launch N+1 (different mount) hit `InvalidDirectory` from doctrine-migrations on the first launch-2 (and similar at any kernel.project_dir-sensitive lookup). `BackendConnection::initBundledMode` now `rmdir`s the cache before each spawn. Costs ~1-2s of warmup per launch; build-time cache warmup is the permanent fix (PLAN.md §13 v0.2.0). The bundled-supervisor integration test gained a 2nd-launch-from-fresh-staging step so this regresses if forgotten.
|
||||||
|
- **`HealthController` deep-loads the bundle.** Constructor-injects `Publisher` so `/healthz` returns 200 only when `BridgeBundle` is fully container-resolvable. v0.1.0's `/healthz` returned 200 against half-loaded bundles — both the path-repo symlink dangling at runtime and the read-only-cache failure shipped green through perfsmoke as a result. Response body now includes `bundle: "PhpQml\\Bridge\\Publisher"` as the canary value.
|
||||||
|
- **Caddyfile formatting.** `framework/skeleton/Caddyfile` and `examples/todo/Caddyfile` reformatted with `caddy fmt`. The "Caddyfile input is not formatted; run 'caddy fmt --overwrite'" warning that fired on every FrankenPHP boot is gone.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Bundled-mode supervisor integration test** (`examples/todo/tests/bundled-supervisor.sh`, `make integration-bundled`). Stages a fake AppImage layout in `/tmp` (host binary copied — Qt's `applicationDirPath()` dereferences symlinks via `/proc/self/exe`, so the real layout has to be mimicked closely; staged Symfony tree is `chmod -R a-w` to actually exercise the read-only-mount cache redirect) and exercises the supervisor end-to-end without needing a real `.AppImage` build. Asserts `/healthz` deep-load + cache redirect. Wired into `.gitea/workflows/ci.yml` after the existing dev-mode integration test.
|
||||||
|
- **Skeleton AppImage parity.** `framework/skeleton/Makefile` gains `staging-symfony` + `appimage` targets mirroring `examples/todo/Makefile`'s. New `framework/skeleton/packaging/skeleton.{desktop,png}` provide minimal AppImage assembly inputs. `bin/php-qml-init` now: (a) renames packaging files to match the scaffolded app name, (b) rewrites the `.desktop` file's `Name`/`Exec`/`Icon`, (c) substitutes the new `BUNDLE_SRC` and `PACKAGING` Makefile variables to either absolute framework paths (default) or vendored `.bridge` / `.bridge-packaging` paths (`--vendor`). Scaffolded apps inherit `make appimage` working out of the box.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- `BackendConnection::m_port` stays hardcoded to 8765 — port-collision between two installed php-qml apps is a real bug surfaced during v0.1.1 prep, but the fix touches every consumer that hardcodes 8765 (perfsmoke, the new bundled-supervisor test) so it's tracked as a v0.2.0 item rather than a v0.1.x bugfix.
|
||||||
|
|
||||||
## [0.1.0] — 2026-05-03
|
## [0.1.0] — 2026-05-03
|
||||||
|
|
||||||
First public preview. Phases 0 through 4a in PLAN.md are complete plus the Phase 5 DX-polish sub-commits. Linux is the only packaged target; macOS and Windows are deferred to 4b / 4c. Tagging is the user's call (release CI runs on `v*` tags).
|
First public preview. Phases 0 through 4a in PLAN.md are complete plus the Phase 5 DX-polish sub-commits. Linux is the only packaged target; macOS and Windows are deferred to 4b / 4c. Tagging is the user's call (release CI runs on `v*` tags).
|
||||||
@@ -43,5 +62,6 @@ First public preview. Phases 0 through 4a in PLAN.md are complete plus the Phase
|
|||||||
- The bundle ships without `composer.lock` (it's a library); the skeleton and the todo example carry their own.
|
- The bundle ships without `composer.lock` (it's a library); the skeleton and the todo example carry their own.
|
||||||
- Licensed under **LGPL-3.0-or-later** (`LICENSE` + `LICENSE.GPL` at the repo root). Chosen to align with Qt 6's LGPLv3 licensing — see PLAN.md §12 for the relinkability obligations the AppImage build already honours.
|
- Licensed under **LGPL-3.0-or-later** (`LICENSE` + `LICENSE.GPL` at the repo root). Chosen to align with Qt 6's LGPLv3 licensing — see PLAN.md §12 for the relinkability obligations the AppImage build already honours.
|
||||||
|
|
||||||
[Unreleased]: https://src.bundespruefstelle.ch/magdev/php-qml/compare/v0.1.0...HEAD
|
[Unreleased]: https://src.bundespruefstelle.ch/magdev/php-qml/compare/v0.1.1...HEAD
|
||||||
|
[0.1.1]: https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.1
|
||||||
[0.1.0]: https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.0
|
[0.1.0]: https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# php-qml — Plan for a Symfony/FrankenPHP + Qt/QML Desktop Framework
|
# php-qml — Plan for a Symfony/FrankenPHP + Qt/QML Desktop Framework
|
||||||
|
|
||||||
> **Status (2026-05):** v0.1.0 shipped 2026-05-03 (LGPL-3.0-or-later). Linux AppImage runs end-to-end. Planning is now version-based — see §13.
|
> **Status (2026-05):** v0.1.0 shipped 2026-05-03 (LGPL-3.0-or-later). v0.1.1 ready to tag — closes the four shakedown follow-ups. Planning is version-based — see §13.
|
||||||
>
|
>
|
||||||
> **Where else to look:**
|
> **Where else to look:**
|
||||||
>
|
>
|
||||||
@@ -531,14 +531,14 @@ Per-phase scope detail is preserved in `CHANGELOG.md` (per-version summary) and
|
|||||||
|
|
||||||
First public preview. Linux AppImage. Full entry in `CHANGELOG.md`; binaries at <https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.0>.
|
First public preview. Linux AppImage. Full entry in `CHANGELOG.md`; binaries at <https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.0>.
|
||||||
|
|
||||||
### v0.1.1 — next bugfix
|
### v0.1.1 — bugfix release (ready to tag)
|
||||||
|
|
||||||
Open follow-ups from v0.1.0 shakedown:
|
All four shakedown follow-ups landed:
|
||||||
|
|
||||||
- **perfsmoke gap.** `/healthz` doesn't exercise any `BridgeBundle` services, so a broken bundle (the v0.1.0 path-repo-symlink + read-only-cache bugs both shipped green through perfsmoke). Extend perfsmoke to hit a real `#[BridgeResource]` API endpoint after the supervisor reports healthy.
|
- **perfsmoke gap closed.** `HealthController` now constructor-injects `Publisher`; `/healthz` returns 200 only when `BridgeBundle` is fully container-resolvable. The `bundle` field in the response is the canary value perfsmoke + the bundled-mode integration test both check.
|
||||||
- **`bin/php-qml-init` parity.** Scaffolded apps copy `framework/skeleton/`, but the AppImage-relevant fixes (path-repo `symlink: false` sed, writable cache/log env-vars, `Kernel::getCacheDir/getLogDir` overrides) were applied to `examples/todo/`. Either move them into the skeleton (so init-scaffolded apps inherit them) or have `php-qml-init` apply them. Otherwise every scaffolded app re-discovers the same bugs.
|
- **Bundled-mode supervisor integration test.** `examples/todo/tests/bundled-supervisor.sh` (run via `make integration-bundled`) stages a fake AppImage layout in `/tmp` and exercises the whole supervisor codepath (`resolveFrankenphpBin` → `runMigrations` → `spawnChild` → cache/log redirect to user data dir) without needing a real `.AppImage` build. Wired into ci.yml. Catches every v0.1.0 shakedown bug.
|
||||||
- **Bundled-mode integration test.** The current bridge-integration test only exercises dev mode (`BRIDGE_URL` set). The whole bundled supervisor codepath (`resolveFrankenphpBin`, `runMigrations`, `spawnChild`, supervisor restart) is only validated by perfsmoke against a real AppImage. A faked-AppImage-layout integration test would catch most of the v0.1.0 shakedown bugs in CI rather than in user reports.
|
- **Skeleton AppImage parity.** `framework/skeleton/Makefile` gains `staging-symfony` + `appimage` targets mirroring the example's; `framework/skeleton/packaging/` ships starter `.desktop` + `.png`; `bin/php-qml-init` rewrites `BUNDLE_SRC` / `PACKAGING` Make variables and renames packaging files at scaffold time. `--vendor` mode also vendors `packaging/linux/` to `.bridge-packaging/`. Scaffolded apps inherit a working `make appimage` flow.
|
||||||
- **Caddyfile formatting.** FrankenPHP logs `Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies` on every boot. Cosmetic but noisy in the dev console.
|
- **Caddyfile fmt.** `framework/skeleton/Caddyfile` and `examples/todo/Caddyfile` reformatted per `caddy fmt`; the "Caddyfile input is not formatted" boot warning is gone.
|
||||||
|
|
||||||
### v0.2.0 — next minor
|
### v0.2.0 — next minor
|
||||||
|
|
||||||
@@ -557,6 +557,7 @@ Pulls in the originally-Phase-3/5-deferred items that don't need new operational
|
|||||||
|
|
||||||
**Operations (§12):**
|
**Operations (§12):**
|
||||||
|
|
||||||
|
- **Bundled-mode port negotiation.** `BackendConnection::m_port` is hardcoded to 8765 with no env override or negotiation, so two php-qml apps installed on the same machine collide on first launch (whichever loses the race goes Offline). Fix: bind a transient `QTcpServer` to `QHostAddress::LocalHost` port 0, grab `serverPort()`, hand it to FrankenPHP via the `PORT` env var. Needs a port-discovery mechanism for tests/perfsmoke that currently hardcode 8765 — likely write the chosen port to a sentinel file under the user data dir on supervisor activation. Surfaced from a v0.1.1 follow-up question; deferred to v0.2.0 because the test/consumer migration is wider than v0.1.x scope.
|
||||||
- **Pre-migration auto-backup** (§12, *Migrations on schema change*). Supervisor copies `var/data.sqlite` to `var/data.sqlite.<timestamp>.bak` before invoking `doctrine:migrations:migrate`; trims to N most recent.
|
- **Pre-migration auto-backup** (§12, *Migrations on schema change*). Supervisor copies `var/data.sqlite` to `var/data.sqlite.<timestamp>.bak` before invoking `doctrine:migrations:migrate`; trims to N most recent.
|
||||||
- **`bridge:export` console command + UI hook** (§12, *Data backup / export*). Lets users copy their data out before machine moves or risky migrations.
|
- **`bridge:export` console command + UI hook** (§12, *Data backup / export*). Lets users copy their data out before machine moves or risky migrations.
|
||||||
- **Periodic auto-update check.** Phase 5 noted this as a polish item but didn't ship; v0.1.0 only has menu-triggered manual checks.
|
- **Periodic auto-update check.** Phase 5 noted this as a polish item but didn't ship; v0.1.0 only has menu-triggered manual checks.
|
||||||
|
|||||||
+37
-1
@@ -146,11 +146,31 @@ sed -i \
|
|||||||
-e "s|php-qml — skeleton|php-qml — $NAME|g" \
|
-e "s|php-qml — skeleton|php-qml — $NAME|g" \
|
||||||
"$TARGET/qml/Main.qml"
|
"$TARGET/qml/Main.qml"
|
||||||
|
|
||||||
# Makefile: $(BUILD_DIR)/skeleton → $(BUILD_DIR)/$NAME.
|
# Makefile: rewrite identifiers for the appimage target — binary name,
|
||||||
|
# packaging filenames, AppImage output filename. The path-repo + packaging
|
||||||
|
# absolute paths are handled later (after we know vendor vs absolute mode).
|
||||||
sed -i \
|
sed -i \
|
||||||
-e "s|\$(BUILD_DIR)/skeleton|\$(BUILD_DIR)/$NAME|g" \
|
-e "s|\$(BUILD_DIR)/skeleton|\$(BUILD_DIR)/$NAME|g" \
|
||||||
|
-e "s|--app-name skeleton|--app-name $NAME|g" \
|
||||||
|
-e "s|packaging/skeleton.desktop|packaging/$NAME.desktop|g" \
|
||||||
|
-e "s|packaging/skeleton.png|packaging/$NAME.png|g" \
|
||||||
|
-e "s|build/Skeleton-x86_64.AppImage|build/$PASCAL-x86_64.AppImage|g" \
|
||||||
"$TARGET/Makefile"
|
"$TARGET/Makefile"
|
||||||
|
|
||||||
|
# Rename packaging files to match the app name + rewrite the Exec/Icon
|
||||||
|
# fields in the .desktop file (XDG-launched binary lookup uses these).
|
||||||
|
if [ -f "$TARGET/packaging/skeleton.desktop" ]; then
|
||||||
|
mv "$TARGET/packaging/skeleton.desktop" "$TARGET/packaging/$NAME.desktop"
|
||||||
|
sed -i \
|
||||||
|
-e "s|^Name=php-qml Skeleton|Name=php-qml $PASCAL|" \
|
||||||
|
-e "s|^Exec=skeleton|Exec=$NAME|" \
|
||||||
|
-e "s|^Icon=skeleton|Icon=$NAME|" \
|
||||||
|
"$TARGET/packaging/$NAME.desktop"
|
||||||
|
fi
|
||||||
|
if [ -f "$TARGET/packaging/skeleton.png" ]; then
|
||||||
|
mv "$TARGET/packaging/skeleton.png" "$TARGET/packaging/$NAME.png"
|
||||||
|
fi
|
||||||
|
|
||||||
# .vscode/launch.json: binary path + config label both mention `skeleton`.
|
# .vscode/launch.json: binary path + config label both mention `skeleton`.
|
||||||
if [ -f "$TARGET/.vscode/launch.json" ]; then
|
if [ -f "$TARGET/.vscode/launch.json" ]; then
|
||||||
sed -i \
|
sed -i \
|
||||||
@@ -168,6 +188,8 @@ if [ "$VENDOR" -eq 1 ]; then
|
|||||||
mkdir -p "$TARGET/.bridge"
|
mkdir -p "$TARGET/.bridge"
|
||||||
say "vendoring framework/qml → $NAME/.bridge-qml/"
|
say "vendoring framework/qml → $NAME/.bridge-qml/"
|
||||||
mkdir -p "$TARGET/.bridge-qml"
|
mkdir -p "$TARGET/.bridge-qml"
|
||||||
|
say "vendoring framework/packaging → $NAME/.bridge-packaging/"
|
||||||
|
mkdir -p "$TARGET/.bridge-packaging"
|
||||||
if command -v rsync >/dev/null 2>&1; then
|
if command -v rsync >/dev/null 2>&1; then
|
||||||
rsync -a --delete \
|
rsync -a --delete \
|
||||||
--exclude 'vendor/' --exclude '.phpunit.cache/' \
|
--exclude 'vendor/' --exclude '.phpunit.cache/' \
|
||||||
@@ -176,18 +198,23 @@ if [ "$VENDOR" -eq 1 ]; then
|
|||||||
rsync -a --delete \
|
rsync -a --delete \
|
||||||
--exclude 'build/' \
|
--exclude 'build/' \
|
||||||
"$FRAMEWORK/framework/qml/" "$TARGET/.bridge-qml/"
|
"$FRAMEWORK/framework/qml/" "$TARGET/.bridge-qml/"
|
||||||
|
rsync -a --delete \
|
||||||
|
"$FRAMEWORK/packaging/linux/" "$TARGET/.bridge-packaging/"
|
||||||
else
|
else
|
||||||
cp -R "$FRAMEWORK/framework/php/." "$TARGET/.bridge/"
|
cp -R "$FRAMEWORK/framework/php/." "$TARGET/.bridge/"
|
||||||
cp -R "$FRAMEWORK/framework/qml/." "$TARGET/.bridge-qml/"
|
cp -R "$FRAMEWORK/framework/qml/." "$TARGET/.bridge-qml/"
|
||||||
|
cp -R "$FRAMEWORK/packaging/linux/." "$TARGET/.bridge-packaging/"
|
||||||
rm -rf "$TARGET/.bridge/vendor" "$TARGET/.bridge-qml/build" 2>/dev/null || true
|
rm -rf "$TARGET/.bridge/vendor" "$TARGET/.bridge-qml/build" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
BUNDLE_URL="../.bridge"
|
BUNDLE_URL="../.bridge"
|
||||||
# qml/CMakeLists.txt lives at $TARGET/qml/, vendored qml module at
|
# qml/CMakeLists.txt lives at $TARGET/qml/, vendored qml module at
|
||||||
# $TARGET/.bridge-qml/, so the relative path from the consumer is ../.bridge-qml.
|
# $TARGET/.bridge-qml/, so the relative path from the consumer is ../.bridge-qml.
|
||||||
QML_FW_PATH="\${CMAKE_CURRENT_SOURCE_DIR}/../.bridge-qml"
|
QML_FW_PATH="\${CMAKE_CURRENT_SOURCE_DIR}/../.bridge-qml"
|
||||||
|
PACKAGING_PATH=".bridge-packaging"
|
||||||
else
|
else
|
||||||
BUNDLE_URL="$FRAMEWORK/framework/php"
|
BUNDLE_URL="$FRAMEWORK/framework/php"
|
||||||
QML_FW_PATH="$FRAMEWORK/framework/qml"
|
QML_FW_PATH="$FRAMEWORK/framework/qml"
|
||||||
|
PACKAGING_PATH="$FRAMEWORK/packaging/linux"
|
||||||
fi
|
fi
|
||||||
say "path-repo → $BUNDLE_URL"
|
say "path-repo → $BUNDLE_URL"
|
||||||
# Replace the original "../../php" path-repo URL. The skeleton's
|
# Replace the original "../../php" path-repo URL. The skeleton's
|
||||||
@@ -206,6 +233,15 @@ sed -i \
|
|||||||
-e "s|\${CMAKE_CURRENT_SOURCE_DIR}/../../qml|$QML_FW_PATH|g" \
|
-e "s|\${CMAKE_CURRENT_SOURCE_DIR}/../../qml|$QML_FW_PATH|g" \
|
||||||
"$TARGET/qml/CMakeLists.txt"
|
"$TARGET/qml/CMakeLists.txt"
|
||||||
|
|
||||||
|
# Makefile: BUNDLE_SRC + PACKAGING were framework-tree relative; rewrite
|
||||||
|
# to absolute (or the vendored path). Both are matched against the literal
|
||||||
|
# values in the skeleton Makefile.
|
||||||
|
say "appimage paths → bundle=$BUNDLE_URL packaging=$PACKAGING_PATH"
|
||||||
|
sed -i \
|
||||||
|
-e "s|^BUNDLE_SRC := ../../php\$|BUNDLE_SRC := $BUNDLE_URL|" \
|
||||||
|
-e "s|^PACKAGING := ../../packaging/linux\$|PACKAGING := $PACKAGING_PATH|" \
|
||||||
|
"$TARGET/Makefile"
|
||||||
|
|
||||||
# ── Composer install + first-run migrations ──────────────────────────
|
# ── Composer install + first-run migrations ──────────────────────────
|
||||||
if [ "$SKIP_INSTALL" -eq 1 ]; then
|
if [ "$SKIP_INSTALL" -eq 1 ]; then
|
||||||
say "skipping composer install (--skip-install)"
|
say "skipping composer install (--skip-install)"
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ bin/console make:bridge:resource Todo --int-id
|
|||||||
```
|
```
|
||||||
|
|
||||||
When to use which:
|
When to use which:
|
||||||
|
|
||||||
- **UUIDv7 (default)** — distributed-friendly, exposes no insertion-order leak, sortable. Good for anything an end-user might sync between machines.
|
- **UUIDv7 (default)** — distributed-friendly, exposes no insertion-order leak, sortable. Good for anything an end-user might sync between machines.
|
||||||
- **Integer ID** — easier to debug from a shell; smaller wire size; no client-side ID generation needed. Pick this for purely-local single-machine apps.
|
- **Integer ID** — easier to debug from a shell; smaller wire size; no client-side ID generation needed. Pick this for purely-local single-machine apps.
|
||||||
|
|
||||||
@@ -179,6 +180,7 @@ bin/console make:bridge:window Settings
|
|||||||
```
|
```
|
||||||
|
|
||||||
The generated QML window:
|
The generated QML window:
|
||||||
|
|
||||||
- Imports `PhpQml.Bridge`.
|
- Imports `PhpQml.Bridge`.
|
||||||
- Wraps content in `AppShell` (so it shows the same Reconnecting / Offline chrome as the main window).
|
- Wraps content in `AppShell` (so it shows the same Reconnecting / Offline chrome as the main window).
|
||||||
- Has its own RestClient + a `Connections { target: SingleInstance ... }` block for launch-arg forwarding.
|
- Has its own RestClient + a `Connections { target: SingleInstance ... }` block for launch-arg forwarding.
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
# - dev mode → env unset, defaults below match symfony/.env
|
# - dev mode → env unset, defaults below match symfony/.env
|
||||||
# - bundled mode → BackendConnection sets PORT and MERCURE_*_JWT_KEY
|
# - bundled mode → BackendConnection sets PORT and MERCURE_*_JWT_KEY
|
||||||
# before launching FrankenPHP.
|
# before launching FrankenPHP.
|
||||||
|
|
||||||
{
|
{
|
||||||
auto_https off
|
auto_https off
|
||||||
admin off
|
admin off
|
||||||
|
|||||||
+11
-3
@@ -40,12 +40,16 @@ clean: ## Remove build artefacts
|
|||||||
integration: ## Run the bridge-integration test (FrankenPHP boot + HTTP/SSE round-trip + crash-recover)
|
integration: ## Run the bridge-integration test (FrankenPHP boot + HTTP/SSE round-trip + crash-recover)
|
||||||
./tests/integration.sh
|
./tests/integration.sh
|
||||||
|
|
||||||
|
.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
|
.PHONY: perf
|
||||||
perf: ## Run the AppImage perf smoke test (PLAN.md §11 budgets)
|
perf: ## Run the AppImage perf smoke test (PLAN.md §11 budgets)
|
||||||
./tests/perfsmoke.sh build/Todo-x86_64.AppImage
|
./tests/perfsmoke.sh build/Todo-x86_64.AppImage
|
||||||
|
|
||||||
.PHONY: appimage
|
.PHONY: staging-symfony
|
||||||
appimage: build ## Package as a single-file Linux AppImage at build/Todo-x86_64.AppImage
|
staging-symfony: ## Stage a --no-dev composer copy of symfony for AppImage / bundled-mode tests
|
||||||
# Composer install --no-dev in a staging copy of symfony so the
|
# Composer install --no-dev in a staging copy of symfony so the
|
||||||
# dev tree (with maker-bundle etc.) is left untouched.
|
# dev tree (with maker-bundle etc.) is left untouched.
|
||||||
rm -rf build/staging-symfony
|
rm -rf build/staging-symfony
|
||||||
@@ -62,6 +66,9 @@ appimage: build ## Package as a single-file Linux AppImage at build/Todo-x86_64.
|
|||||||
sed -i 's|"symlink": true|"symlink": false|' build/staging-symfony/composer.json
|
sed -i 's|"symlink": true|"symlink": false|' build/staging-symfony/composer.json
|
||||||
rm -f build/staging-symfony/composer.lock
|
rm -f build/staging-symfony/composer.lock
|
||||||
cd build/staging-symfony && composer install --no-dev --no-interaction --classmap-authoritative
|
cd build/staging-symfony && composer install --no-dev --no-interaction --classmap-authoritative
|
||||||
|
|
||||||
|
.PHONY: appimage
|
||||||
|
appimage: build staging-symfony ## Package as a single-file Linux AppImage at build/Todo-x86_64.AppImage
|
||||||
../../packaging/linux/build-appimage.sh \
|
../../packaging/linux/build-appimage.sh \
|
||||||
--app-name todo \
|
--app-name todo \
|
||||||
--host-binary $(QT_BIN) \
|
--host-binary $(QT_BIN) \
|
||||||
@@ -76,7 +83,8 @@ appimage: build ## Package as a single-file Linux AppImage at build/Todo-x86_64.
|
|||||||
@echo "AppImage built. Test with: ./build/Todo-x86_64.AppImage"
|
@echo "AppImage built. Test with: ./build/Todo-x86_64.AppImage"
|
||||||
|
|
||||||
.PHONY: quality
|
.PHONY: quality
|
||||||
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint, integration
|
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint, integration (dev + bundled)
|
||||||
cd ../../framework/php && composer quality
|
cd ../../framework/php && composer quality
|
||||||
cmake --build $(BUILD_DIR) --target all_qmllint
|
cmake --build $(BUILD_DIR) --target all_qmllint
|
||||||
./tests/integration.sh
|
./tests/integration.sh
|
||||||
|
$(MAKE) integration-bundled
|
||||||
Executable
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bundled-mode integration test (v0.1.1).
|
||||||
|
#
|
||||||
|
# Exercises the bundled-mode supervisor codepath end-to-end *without*
|
||||||
|
# requiring a real AppImage build:
|
||||||
|
#
|
||||||
|
# - resolveFrankenphpBin (BackendConnection.cpp) — finds frankenphp
|
||||||
|
# as a sibling of the host binary at usr/bin/frankenphp.
|
||||||
|
# - resolveSymfonyDir / resolveCaddyfilePath — finds the staged
|
||||||
|
# Symfony tree + Caddyfile under usr/share/<app>/.
|
||||||
|
# - runMigrations + spawnChild — supervisor drives the doctrine
|
||||||
|
# migrate, spawns frankenphp, polls /healthz.
|
||||||
|
# - Kernel::getCacheDir / getLogDir override — Symfony writes to
|
||||||
|
# the user data dir, not the (chmod -w) staged tree.
|
||||||
|
# - HealthController deep-load — /healthz response includes a
|
||||||
|
# `bundle` field proving BridgeBundle was autoloaded.
|
||||||
|
#
|
||||||
|
# Catches the v0.1.0 shakedown bugs (doubled bin/frankenphp path,
|
||||||
|
# composer path-repo symlink dangling at runtime, read-only mount
|
||||||
|
# var/cache failure) faster than perfsmoke against a real .AppImage.
|
||||||
|
#
|
||||||
|
# Designed for `make integration-bundled`. Expects the regular
|
||||||
|
# `make build` artefacts to exist; runs `make staging-symfony`
|
||||||
|
# itself if the staged tree isn't present.
|
||||||
|
#
|
||||||
|
# Skip-conditions:
|
||||||
|
# - port 8765 already in use (don't trample a dev instance)
|
||||||
|
# - frankenphp not on PATH
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
BUILD_DIR="$APP_DIR/build/qml"
|
||||||
|
HOST_BIN="$BUILD_DIR/todo"
|
||||||
|
STAGING="$APP_DIR/build/staging-symfony"
|
||||||
|
CADDYFILE="$APP_DIR/Caddyfile"
|
||||||
|
|
||||||
|
APP_NAME=todo
|
||||||
|
PORT=8765
|
||||||
|
|
||||||
|
step() { echo "→ $*"; }
|
||||||
|
fail() { echo "✗ FAIL: $*" >&2; exit 1; }
|
||||||
|
skip() { echo "⊘ SKIP: $*" >&2; exit 0; }
|
||||||
|
|
||||||
|
# ── Pre-flight ─────────────────────────────────────────────────────────
|
||||||
|
[ -x "$HOST_BIN" ] || fail "host binary not built — run 'make build' first ($HOST_BIN)"
|
||||||
|
command -v frankenphp >/dev/null 2>&1 || skip "frankenphp not on PATH"
|
||||||
|
[ -d "$STAGING" ] || { step "no staging-symfony, building it"; (cd "$APP_DIR" && make staging-symfony >/dev/null); }
|
||||||
|
|
||||||
|
if (echo > "/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then
|
||||||
|
skip "port $PORT already in use (dev instance running?)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Stage a fake AppImage layout in a temp dir ─────────────────────────
|
||||||
|
ROOT="$(mktemp -d)"
|
||||||
|
DATA_DIR="$(mktemp -d)"
|
||||||
|
trap 'cleanup' EXIT INT TERM
|
||||||
|
|
||||||
|
PID=""
|
||||||
|
cleanup() {
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
|
||||||
|
kill -TERM "$PID" 2>/dev/null || true
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
kill -0 "$PID" 2>/dev/null || break
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
kill -KILL "$PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# Restore writable so rm -rf doesn't choke.
|
||||||
|
[ -d "$ROOT/usr/share/$APP_NAME/symfony" ] && \
|
||||||
|
chmod -R u+w "$ROOT/usr/share/$APP_NAME/symfony" 2>/dev/null || true
|
||||||
|
rm -rf "$ROOT" "$DATA_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
step "stage AppImage layout at $ROOT"
|
||||||
|
mkdir -p "$ROOT/usr/bin" "$ROOT/usr/share/$APP_NAME"
|
||||||
|
# Host binary must be copied, not symlinked: Qt's applicationDirPath()
|
||||||
|
# reads /proc/self/exe which dereferences symlinks, so a symlinked host
|
||||||
|
# would resolve to the build/ dir and the supervisor would look for
|
||||||
|
# frankenphp + symfony there instead of in the staged layout. Real
|
||||||
|
# AppImages copy the binary, mimicking that here.
|
||||||
|
cp "$HOST_BIN" "$ROOT/usr/bin/$APP_NAME"
|
||||||
|
ln -s "$(command -v frankenphp)" "$ROOT/usr/bin/frankenphp"
|
||||||
|
cp -a "$STAGING/." "$ROOT/usr/share/$APP_NAME/symfony/"
|
||||||
|
cp "$CADDYFILE" "$ROOT/usr/share/$APP_NAME/Caddyfile"
|
||||||
|
|
||||||
|
# Make the staged Symfony tree read-only so the cache/log redirect is
|
||||||
|
# actually exercised — without the Kernel::getCacheDir/getLogDir override,
|
||||||
|
# Symfony tries to mkdir var/cache here and fails.
|
||||||
|
chmod -R a-w "$ROOT/usr/share/$APP_NAME/symfony"
|
||||||
|
|
||||||
|
# ── Launch the host ────────────────────────────────────────────────────
|
||||||
|
step "launch host (bundled mode, offscreen, isolated XDG dirs)"
|
||||||
|
LOG="$DATA_DIR/host.log"
|
||||||
|
env -u BRIDGE_URL \
|
||||||
|
XDG_DATA_HOME="$DATA_DIR/share" \
|
||||||
|
XDG_CACHE_HOME="$DATA_DIR/cache" \
|
||||||
|
XDG_CONFIG_HOME="$DATA_DIR/config" \
|
||||||
|
QT_QPA_PLATFORM=offscreen \
|
||||||
|
"$ROOT/usr/bin/$APP_NAME" > "$LOG" 2>&1 &
|
||||||
|
PID=$!
|
||||||
|
|
||||||
|
# ── Poll /healthz ──────────────────────────────────────────────────────
|
||||||
|
step "wait for /healthz"
|
||||||
|
DEADLINE=$(( $(date +%s) + 30 ))
|
||||||
|
HEALTHZ_BODY=""
|
||||||
|
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||||
|
if ! kill -0 "$PID" 2>/dev/null; then
|
||||||
|
sed 's/^/ /' "$LOG" >&2 || true
|
||||||
|
fail "host died during boot"
|
||||||
|
fi
|
||||||
|
if HEALTHZ_BODY="$(curl -fsS -m 1 "http://127.0.0.1:$PORT/healthz" 2>/dev/null)"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
[ -n "$HEALTHZ_BODY" ] || { sed 's/^/ /' "$LOG" >&2 || true; fail "/healthz never responded within 30s"; }
|
||||||
|
|
||||||
|
# ── Verify bundle deep-load ────────────────────────────────────────────
|
||||||
|
step "/healthz body: $HEALTHZ_BODY"
|
||||||
|
echo "$HEALTHZ_BODY" | grep -q '"status":"ok"' \
|
||||||
|
|| fail "/healthz didn't return status:ok"
|
||||||
|
echo "$HEALTHZ_BODY" | grep -q '"bundle":"PhpQml\\\\Bridge\\\\Publisher"' \
|
||||||
|
|| fail "/healthz missing bundle field — HealthController deep-load broken"
|
||||||
|
|
||||||
|
# ── Verify the cache/log redirect actually fired ───────────────────────
|
||||||
|
step "verify Symfony wrote cache to user data dir, not the read-only staging"
|
||||||
|
# Qt's QStandardPaths::AppDataLocation on Linux is $XDG_DATA_HOME/<org>/<app>,
|
||||||
|
# org="php-qml" comes from main.cpp setOrganizationName, app="todo" from setApplicationName.
|
||||||
|
USER_DATA="$DATA_DIR/share/php-qml/$APP_NAME"
|
||||||
|
[ -d "$USER_DATA/var/cache" ] \
|
||||||
|
|| fail "user-data var/cache missing at $USER_DATA — APP_CACHE_DIR override didn't fire"
|
||||||
|
# And not into the staged tree (which is chmod -w anyway):
|
||||||
|
if [ -d "$ROOT/usr/share/$APP_NAME/symfony/var/cache/prod" ] && \
|
||||||
|
[ "$(ls -A "$ROOT/usr/share/$APP_NAME/symfony/var/cache/prod" 2>/dev/null)" ]; then
|
||||||
|
fail "Symfony wrote into the read-only staging tree — Kernel::getCacheDir override broken"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Second launch: same XDG_DATA_HOME, fresh staging mount ─────────────
|
||||||
|
# Real AppImages get a fresh /tmp/.mount_<random> per launch but reuse the
|
||||||
|
# user data dir, so any cached absolute path from launch N is stale by N+1.
|
||||||
|
# Tear down the running host, re-run from a NEW staging dir (mimicking the
|
||||||
|
# fresh-mount situation), assert /healthz comes back up.
|
||||||
|
step "tear down + relaunch from fresh staging (regression: cache-baked-mount-path)"
|
||||||
|
kill -TERM "$PID" 2>/dev/null || true
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
kill -0 "$PID" 2>/dev/null || break
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
kill -KILL "$PID" 2>/dev/null || true
|
||||||
|
PID=""
|
||||||
|
chmod -R u+w "$ROOT/usr/share/$APP_NAME/symfony" 2>/dev/null
|
||||||
|
rm -rf "$ROOT"
|
||||||
|
ROOT="$(mktemp -d)"
|
||||||
|
mkdir -p "$ROOT/usr/bin" "$ROOT/usr/share/$APP_NAME"
|
||||||
|
cp "$HOST_BIN" "$ROOT/usr/bin/$APP_NAME"
|
||||||
|
ln -s "$(command -v frankenphp)" "$ROOT/usr/bin/frankenphp"
|
||||||
|
cp -a "$STAGING/." "$ROOT/usr/share/$APP_NAME/symfony/"
|
||||||
|
cp "$CADDYFILE" "$ROOT/usr/share/$APP_NAME/Caddyfile"
|
||||||
|
chmod -R a-w "$ROOT/usr/share/$APP_NAME/symfony"
|
||||||
|
|
||||||
|
LOG2="$DATA_DIR/host2.log"
|
||||||
|
env -u BRIDGE_URL \
|
||||||
|
XDG_DATA_HOME="$DATA_DIR/share" \
|
||||||
|
XDG_CACHE_HOME="$DATA_DIR/cache" \
|
||||||
|
XDG_CONFIG_HOME="$DATA_DIR/config" \
|
||||||
|
QT_QPA_PLATFORM=offscreen \
|
||||||
|
"$ROOT/usr/bin/$APP_NAME" > "$LOG2" 2>&1 &
|
||||||
|
PID=$!
|
||||||
|
|
||||||
|
DEADLINE=$(( $(date +%s) + 30 ))
|
||||||
|
HEALTHZ2_BODY=""
|
||||||
|
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||||
|
if ! kill -0 "$PID" 2>/dev/null; then
|
||||||
|
sed 's/^/ /' "$LOG2" >&2 || true
|
||||||
|
fail "host died during 2nd boot"
|
||||||
|
fi
|
||||||
|
if HEALTHZ2_BODY="$(curl -fsS -m 1 "http://127.0.0.1:$PORT/healthz" 2>/dev/null)"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
[ -n "$HEALTHZ2_BODY" ] || { sed 's/^/ /' "$LOG2" >&2 || true; fail "/healthz never responded on 2nd launch — stale cache?"; }
|
||||||
|
echo "$HEALTHZ2_BODY" | grep -q '"status":"ok"' \
|
||||||
|
|| fail "2nd-launch /healthz didn't return status:ok"
|
||||||
|
|
||||||
|
step "All bundled-supervisor assertions passed (incl. 2nd-launch cache wipe)."
|
||||||
@@ -4,18 +4,33 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace PhpQml\Bridge\Controller;
|
namespace PhpQml\Bridge\Controller;
|
||||||
|
|
||||||
|
use PhpQml\Bridge\Publisher;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Readiness probe used by the Qt host to detect when the backend is up.
|
* Readiness probe used by the Qt host to detect when the backend is up.
|
||||||
* See PLAN.md §3 (*Startup*, step 4).
|
* See PLAN.md §3 (*Startup*, step 4).
|
||||||
|
*
|
||||||
|
* Publisher is injected purely as a deep-health canary: if the bridge
|
||||||
|
* bundle's autoload or container wiring is broken (e.g. a packaging build
|
||||||
|
* with a dangling vendor path-repo symlink), this controller can't even
|
||||||
|
* be constructed, so /healthz fails 500 instead of misleadingly returning
|
||||||
|
* 200 against a half-loaded bundle.
|
||||||
*/
|
*/
|
||||||
final class HealthController
|
final class HealthController
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Publisher $publisher,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/healthz', name: 'php_qml_bridge_healthz', methods: ['GET'])]
|
#[Route('/healthz', name: 'php_qml_bridge_healthz', methods: ['GET'])]
|
||||||
public function __invoke(): JsonResponse
|
public function __invoke(): JsonResponse
|
||||||
{
|
{
|
||||||
return new JsonResponse(['status' => 'ok']);
|
return new JsonResponse([
|
||||||
|
'status' => 'ok',
|
||||||
|
'bundle' => $this->publisher::class,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +109,9 @@ void BackendConnection::initBundledMode()
|
|||||||
|
|
||||||
m_dataDir = userDataDir();
|
m_dataDir = userDataDir();
|
||||||
QDir().mkpath(m_dataDir + "/var/log");
|
QDir().mkpath(m_dataDir + "/var/log");
|
||||||
|
// Wipe Symfony cache: kernel.project_dir bakes the AppImage FUSE mount path
|
||||||
|
// (different every launch), so cache from a previous launch is always stale.
|
||||||
|
QDir(m_dataDir + "/var/cache").removeRecursively();
|
||||||
QDir().mkpath(m_dataDir + "/var/cache");
|
QDir().mkpath(m_dataDir + "/var/cache");
|
||||||
|
|
||||||
setToken(randomSecret(32));
|
setToken(randomSecret(32));
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
# before launching FrankenPHP.
|
# before launching FrankenPHP.
|
||||||
#
|
#
|
||||||
# Caddyfile {$VAR:default} syntax substitutes env vars at parse time.
|
# Caddyfile {$VAR:default} syntax substitutes env vars at parse time.
|
||||||
|
|
||||||
{
|
{
|
||||||
auto_https off
|
auto_https off
|
||||||
admin off
|
admin off
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ SYMFONY_DIR := symfony
|
|||||||
QML_SRC_DIR := qml
|
QML_SRC_DIR := qml
|
||||||
BUILD_DIR := build/qml
|
BUILD_DIR := build/qml
|
||||||
QT_BIN := $(BUILD_DIR)/skeleton
|
QT_BIN := $(BUILD_DIR)/skeleton
|
||||||
|
# Path to framework/php (path-repo source) and packaging/linux (build-appimage.sh).
|
||||||
|
# Both are framework-tree relative; bin/php-qml-init rewrites them at scaffold time
|
||||||
|
# to either an absolute framework path (default) or a vendored copy under .bridge/.
|
||||||
|
BUNDLE_SRC := ../../php
|
||||||
|
PACKAGING := ../../packaging/linux
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## Show available targets
|
help: ## Show available targets
|
||||||
@@ -34,7 +39,43 @@ doctor-connect: ## Run bridge:doctor with backend connectivity probe
|
|||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: ## Remove build artefacts
|
clean: ## Remove build artefacts
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR) build/staging-symfony
|
||||||
|
|
||||||
|
.PHONY: staging-symfony
|
||||||
|
staging-symfony: ## Stage a --no-dev composer copy of symfony for AppImage assembly
|
||||||
|
# See examples/todo/Makefile for the rationale; same logic, mirrored here
|
||||||
|
# so scaffolded apps inherit a working AppImage flow without copy-paste.
|
||||||
|
# BUNDLE_SRC may be absolute (after `php-qml-init` rewrites it for a
|
||||||
|
# scaffolded app) or relative-to-symfony (framework default `../../php`);
|
||||||
|
# the case-statement handles both.
|
||||||
|
rm -rf build/staging-symfony
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude='vendor/' \
|
||||||
|
--exclude='var/cache/' --exclude='var/log/' \
|
||||||
|
$(SYMFONY_DIR)/ build/staging-symfony/
|
||||||
|
set -e; case "$(BUNDLE_SRC)" in \
|
||||||
|
/*) BUNDLE_ABS="$(BUNDLE_SRC)" ;; \
|
||||||
|
*) BUNDLE_ABS="$$(cd $(SYMFONY_DIR)/$(BUNDLE_SRC) && pwd)" ;; \
|
||||||
|
esac; \
|
||||||
|
sed -i "s|\"$(BUNDLE_SRC)\"|\"$$BUNDLE_ABS\"|" build/staging-symfony/composer.json
|
||||||
|
sed -i 's|"symlink": true|"symlink": false|' build/staging-symfony/composer.json
|
||||||
|
rm -f build/staging-symfony/composer.lock
|
||||||
|
cd build/staging-symfony && composer install --no-dev --no-interaction --classmap-authoritative
|
||||||
|
|
||||||
|
.PHONY: appimage
|
||||||
|
appimage: build staging-symfony ## Package as a single-file Linux AppImage at build/Skeleton-x86_64.AppImage
|
||||||
|
$(PACKAGING)/build-appimage.sh \
|
||||||
|
--app-name skeleton \
|
||||||
|
--host-binary $(QT_BIN) \
|
||||||
|
--symfony-dir build/staging-symfony \
|
||||||
|
--frankenphp $${FRANKENPHP:-frankenphp} \
|
||||||
|
--caddyfile Caddyfile \
|
||||||
|
--desktop packaging/skeleton.desktop \
|
||||||
|
--icon packaging/skeleton.png \
|
||||||
|
--output build/Skeleton-x86_64.AppImage \
|
||||||
|
$${APPIMAGE_UPDATE_INFO:+--update-info "$$APPIMAGE_UPDATE_INFO"}
|
||||||
|
@echo
|
||||||
|
@echo "AppImage built. Test with: ./build/Skeleton-x86_64.AppImage"
|
||||||
|
|
||||||
.PHONY: quality
|
.PHONY: quality
|
||||||
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint, maker snapshots
|
quality: build ## Run PHPStan, php-cs-fixer (check), PHPUnit, qmllint, maker snapshots
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=php-qml Skeleton
|
||||||
|
Comment=php-qml application scaffold (replace with your own)
|
||||||
|
Exec=skeleton
|
||||||
|
Icon=skeleton
|
||||||
|
Categories=Utility;
|
||||||
|
Terminal=false
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 209 B |
Reference in new issue
Block a user